-
Notifications
You must be signed in to change notification settings - Fork 41
feat: parse inert resourceDependencies section in application definitions #1940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.epam.aidial.core.config; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonAlias; | ||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||
| import com.fasterxml.jackson.databind.PropertyNamingStrategies; | ||
| import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
| import lombok.Data; | ||
| import lombok.experimental.Accessors; | ||
|
|
||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * A declared resource dependency: a request, not a grant. The record carries no access of its | ||
| * own — at request start Core verifies the originating user's reach and the required consent, | ||
| * and only then bakes the grant into the per-request key the app already holds. | ||
| */ | ||
| @Data | ||
| @Accessors(chain = true) | ||
| @JsonInclude(JsonInclude.Include.NON_NULL) | ||
| @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
| public class ResourceDependency { | ||
|
|
||
| /** The single descriptor kind for every DIAL target; the target type is clear from the path. */ | ||
| public static final String KIND = "dial.resourceLink"; | ||
|
|
||
| private String kind; | ||
|
|
||
| @JsonAlias({"linkId", "link_id"}) | ||
| private String linkId; | ||
|
|
||
| private Target target; | ||
|
|
||
| @JsonInclude(JsonInclude.Include.NON_EMPTY) | ||
| private Set<ResourceAccessType> access = Set.of(); | ||
|
|
||
| @JsonInclude(JsonInclude.Include.NON_DEFAULT) | ||
| private boolean required; | ||
|
|
||
| @Data | ||
| @Accessors(chain = true) | ||
| @JsonInclude(JsonInclude.Include.NON_NULL) | ||
| @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
| public static class Target { | ||
|
|
||
| // Two forms only, nothing else: a concrete global-view path, or a current-user placeholder rooted path. | ||
| private String path; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should it be DIAL resource URL instead of generic name
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The URL should be validated: DIAL Core must be able to deserialize a string to DIAL Resource URL |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| package com.epam.aidial.core.config; | ||
|
|
||
| import com.fasterxml.jackson.databind.MapperFeature; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
| import static org.junit.jupiter.api.Assertions.assertSame; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| /** | ||
| * Parsing and serialization state of a declared resource dependency, and of the | ||
| * {@code resourceDependencies} section on {@link Application}. Target-language validation is | ||
| * write-time validation and is covered by the server-side validator's tests. | ||
| */ | ||
| public class ResourceDependencyTest { | ||
|
|
||
| // Definition bodies reach the POJO through ProxyUtil's mapper, which accepts enum names | ||
| // case-insensitively — so the design's lowercase input ("read", "write") parses, while | ||
| // serialization emits the enum names ("READ", "WRITE"); the normalization is expected. | ||
| private static final ObjectMapper MAPPER = new ObjectMapper() | ||
| .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); | ||
|
|
||
| @Test | ||
| void parsesTheSnakeCaseFormWithDefaults() throws Exception { | ||
| String json = """ | ||
| { | ||
| "kind": "dial.resourceLink", | ||
| "link_id": "lnk_1", | ||
| "target": {"path": "current-user/skills/"}, | ||
| "access": ["read", "write"], | ||
| "required": true | ||
| } | ||
| """; | ||
| ResourceDependency dependency = MAPPER.readValue(json, ResourceDependency.class); | ||
|
|
||
| assertEquals(ResourceDependency.KIND, dependency.getKind()); | ||
| assertEquals("lnk_1", dependency.getLinkId()); | ||
| assertEquals("current-user/skills/", dependency.getTarget().getPath()); | ||
| assertEquals(Set.of(ResourceAccessType.READ, ResourceAccessType.WRITE), dependency.getAccess()); | ||
| assertTrue(dependency.isRequired()); | ||
| } | ||
|
|
||
| @Test | ||
| void parsesTheCamelCaseForm() throws Exception { | ||
| ResourceDependency dependency = MAPPER.readValue( | ||
| "{\"linkId\": \"lnk_2\", \"target\": {\"path\": \"files/public/policies/\"}}", | ||
| ResourceDependency.class); | ||
|
|
||
| assertEquals("lnk_2", dependency.getLinkId()); | ||
| assertEquals("files/public/policies/", dependency.getTarget().getPath()); | ||
| } | ||
|
|
||
| @Test | ||
| void defaultsWhenOptionalFieldsAreAbsent() throws Exception { | ||
| ResourceDependency dependency = MAPPER.readValue( | ||
| "{\"kind\": \"dial.resourceLink\", \"target\": {\"path\": \"current-user/\"}}", | ||
| ResourceDependency.class); | ||
|
|
||
| assertNull(dependency.getLinkId()); | ||
| assertTrue(dependency.getAccess().isEmpty()); | ||
| assertFalse(dependency.isRequired()); | ||
| } | ||
|
|
||
| @Test | ||
| void roundTripsThroughSerialization() throws Exception { | ||
| String json = """ | ||
| { | ||
| "kind": "dial.resourceLink", | ||
| "linkId": "lnk_1", | ||
| "target": {"path": "current-user/skills/"}, | ||
| "access": ["write"], | ||
| "required": true | ||
| } | ||
| """; | ||
| ResourceDependency dependency = MAPPER.readValue(json, ResourceDependency.class); | ||
|
|
||
| assertEquals(dependency, MAPPER.readValue(MAPPER.writeValueAsString(dependency), ResourceDependency.class)); | ||
| } | ||
|
|
||
| @Test | ||
| void omitsDefaultValuesFromSerialization() throws Exception { | ||
| ResourceDependency dependency = new ResourceDependency() | ||
| .setKind(ResourceDependency.KIND) | ||
| .setTarget(new ResourceDependency.Target().setPath("current-user/")); | ||
|
|
||
| String json = MAPPER.writeValueAsString(dependency); | ||
|
|
||
| assertFalse(json.contains("required")); | ||
| assertFalse(json.contains("access")); | ||
| assertEquals("{\"kind\":\"dial.resourceLink\",\"target\":{\"path\":\"current-user/\"}}", json); | ||
| } | ||
|
|
||
| @Test | ||
| void serializesAccessAsEnumNamesWhileAcceptingLowercaseInput() throws Exception { | ||
| ResourceDependency dependency = MAPPER.readValue(""" | ||
| {"kind": "dial.resourceLink", "target": {"path": "files/public/f/"}, "access": ["read", "write"]} | ||
| """, ResourceDependency.class); | ||
|
|
||
| String json = MAPPER.writeValueAsString(dependency); | ||
|
|
||
| assertTrue(json.contains("\"READ\"")); | ||
| assertTrue(json.contains("\"WRITE\"")); | ||
| } | ||
|
|
||
| @Test | ||
| void applicationSectionParsesBothForms() throws Exception { | ||
| String snake = """ | ||
| {"name": "app", "resource_dependencies": [{"link_id": "lnk_1", "target": {"path": "public/folder/"}}]} | ||
| """; | ||
| String camel = """ | ||
| {"name": "app", "resourceDependencies": [{"linkId": "lnk_1", "target": {"path": "public/folder/"}}]} | ||
| """; | ||
|
|
||
| assertEquals(MAPPER.readValue(snake, Application.class).getResourceDependencies(), | ||
| MAPPER.readValue(camel, Application.class).getResourceDependencies()); | ||
| } | ||
|
|
||
| @Test | ||
| void absentSectionYieldsEmptyListAndIsOmittedFromSerialization() throws Exception { | ||
| Application withoutSection = MAPPER.readValue("{\"name\": \"app\"}", Application.class); | ||
|
|
||
| assertTrue(withoutSection.getResourceDependencies().isEmpty()); | ||
| assertFalse(MAPPER.writeValueAsString(withoutSection).contains("resource_dependencies")); | ||
| } | ||
|
|
||
| @Test | ||
| void copyConstructorCarriesTheSection() { | ||
| Application source = new Application() | ||
| .setResourceDependencies(List.of(new ResourceDependency() | ||
| .setKind(ResourceDependency.KIND) | ||
| .setLinkId("lnk_1") | ||
| .setTarget(new ResourceDependency.Target().setPath("current-user/skills/")) | ||
| .setAccess(Set.of(ResourceAccessType.WRITE)) | ||
| .setRequired(true))); | ||
|
|
||
| Application copy = new Application(source); | ||
|
|
||
| assertSame(source.getResourceDependencies(), copy.getResourceDependencies()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13683,6 +13683,10 @@ components: | |
| $ref: "#/components/schemas/MapStringString" | ||
| baseUrl: | ||
| type: string | ||
| resource_dependencies: | ||
| type: array | ||
| items: | ||
| $ref: "#/components/schemas/ResourceDependency" | ||
| ApplicationData: | ||
| type: object | ||
| properties: | ||
|
|
@@ -16474,6 +16478,26 @@ components: | |
| - SIGNED_IN | ||
| - SIGNED_OUT | ||
| - FAILED | ||
| ResourceDependency: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add descriptions per new field |
||
| type: object | ||
| properties: | ||
| access: | ||
| type: array | ||
| items: | ||
| $ref: "#/components/schemas/ResourceAccessType" | ||
| kind: | ||
| type: string | ||
| link_id: | ||
| type: string | ||
| required: | ||
| type: boolean | ||
| target: | ||
| $ref: "#/components/schemas/ResourceDependencyTarget" | ||
| ResourceDependencyTarget: | ||
| type: object | ||
| properties: | ||
| path: | ||
| type: string | ||
| ResourceEvent: | ||
| type: object | ||
| properties: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add javadoc what the field means