diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java index 619e6fc062..17a07c54a4 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/ResolverCache.java @@ -77,6 +77,7 @@ public class ResolverCache { private Map externalFileCache = new HashMap<>(); private Map canonicalResolutionCache = new HashMap<>(); private Map canonicalExternalFileCache = new HashMap<>(); + private Map rootReferenceNameCache = new HashMap<>(); private List referencedModelKeys = new ArrayList<>(); private Set resolveValidationMessages; private final ParseOptions parseOptions; @@ -266,6 +267,8 @@ private T resolveRootReference(String file, String definitionPath, String re T result = expectedType.cast(rootTarget); resolutionCache.put(ref, result); canonicalResolutionCache.putIfAbsent(canonicalRef, result); + String definitionName = definitionPath.substring(definitionPath.lastIndexOf('/') + 1); + rootReferenceNameCache.putIfAbsent(canonicalRef, unescapePointer(definitionName)); return result; } @@ -508,6 +511,17 @@ public String getRenamedRef(String originalRef) { return canonicalRenameCache.get(canonicalize(originalRef)); } + /** + * Returns the declared name of a reference resolved from the root document snapshot. + * + * @param ref reference to look up + * @return the declared root reference name, or {@code null} if the reference was not + * resolved from the root document snapshot + */ + public String getRootReferenceName(String ref) { + return rootReferenceNameCache.get(canonicalize(ref)); + } + public void putRenamedRef(String originalRef, String newRef) { renameCache.put(originalRef, newRef); canonicalRenameCache.put(canonicalize(originalRef), newRef); diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java index 682e91e201..119c9a9215 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java @@ -61,6 +61,14 @@ private void warnUnableToLoadReference(String ref) { LOGGER.warn("unable to load model reference from `{}`. It may not be available or the reference isn't a valid model schema", ref); } + private String getRootReferenceName(String ref) { + String rootReferenceName = cache.getRootReferenceName(ref); + if (rootReferenceName != null) { + cache.putRenamedRef(ref, rootReferenceName); + } + return rootReferenceName; + } + private String allocateSchemaName(Map schemas, String baseName, Schema incoming, String incomingRef) { return nameAllocator.allocate(schemas, baseName, incoming, incomingRef, @@ -82,6 +90,11 @@ public String processRefToExternalSchema(String $ref, RefFormat refFormat) { warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + cache.addReferencedKey(rootReferenceName); + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { @@ -403,6 +416,10 @@ public String processRefToExternalResponse(String $ref, RefFormat refFormat) { warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; @@ -479,6 +496,10 @@ public String processRefToExternalRequestBody(String $ref, RefFormat refFormat) warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { @@ -528,6 +549,10 @@ public String processRefToExternalHeader(String $ref, RefFormat refFormat) { warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { @@ -584,6 +609,10 @@ public String processRefToExternalSecurityScheme(String $ref, RefFormat refForma warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { @@ -632,6 +661,10 @@ public String processRefToExternalLink(String $ref, RefFormat refFormat) { warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { @@ -679,6 +712,10 @@ public String processRefToExternalExample(String $ref, RefFormat refFormat) { warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { @@ -725,6 +762,10 @@ public String processRefToExternalParameter(String $ref, RefFormat refFormat) { warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { @@ -799,6 +840,10 @@ public String processRefToExternalCallback(String $ref, RefFormat refFormat) { warnUnableToLoadReference($ref); return $ref; } + String rootReferenceName = getRootReferenceName($ref); + if (rootReferenceName != null) { + return rootReferenceName; + } String newRef; if (openAPI.getComponents() == null) { diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/processors/ExternalRefProcessorTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/processors/ExternalRefProcessorTest.java index b6690104e0..07b440a2ad 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/processors/ExternalRefProcessorTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/processors/ExternalRefProcessorTest.java @@ -118,6 +118,32 @@ public void testResponseAllocatorExaminesOccupiedSuffixesWithoutOverwritingPlace assertSame(testedOpenAPI.getComponents().getResponses().get("sharedResponse_3"), incoming); } + @Test + public void testResponseReusesRootComponentNameDiscoveredDuringLoad() { + final String ref = "https://example.test/api.yaml#/components/responses/Order-response"; + ApiResponse rootSnapshotResponse = new ApiResponse().$ref("./responses/orderResponse.json"); + ApiResponse currentResponse = new ApiResponse().description("resolved response"); + OpenAPI testedOpenAPI = new OpenAPI().components( + new Components().addResponses("Order-response", currentResponse)); + + new Expectations() {{ + cache.getRenamedRef(ref); + cache.loadRef(ref, RefFormat.URL, ApiResponse.class); + result = rootSnapshotResponse; + cache.getRootReferenceName(ref); + result = "Order-response"; + cache.putRenamedRef(ref, "Order-response"); + }}; + + String assignedName = new ExternalRefProcessor(cache, testedOpenAPI) + .processRefToExternalResponse(ref, RefFormat.URL); + + assertEquals(assignedName, "Order-response"); + assertEquals(testedOpenAPI.getComponents().getResponses().size(), 1); + assertSame(testedOpenAPI.getComponents().getResponses().get("Order-response"), currentResponse); + assertEquals(rootSnapshotResponse.get$ref(), "./responses/orderResponse.json"); + } + @Test public void testEqualResolvedResponsesReuseOneKey() { final String ref = "https://example.test/incoming.yaml#/sharedResponse"; diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue2399Test.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue2399Test.java new file mode 100644 index 0000000000..b5ef709bd9 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue2399Test.java @@ -0,0 +1,61 @@ +package io.swagger.v3.parser.test; + +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class Issue2399Test { + + @Test + public void resolvingRootDocumentAliasRetainsReferencedSchema() { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + + SwaggerParseResult result = new OpenAPIV3Parser().readLocation( + "src/test/resources/issue-2399/root-alias.json", null, options); + + assertNotNull(result.getOpenAPI()); + assertTrue(result.getMessages().isEmpty(), "Unexpected parser messages: " + result.getMessages()); + + Map schemas = result.getOpenAPI().getComponents().getSchemas(); + assertEquals(schemas.size(), 2); + assertEquals(schemas.get("Target").getType(), "string"); + assertEquals(schemas.get("Alias").getType(), "string"); + } + + @Test + public void resolveFullyPreservesDeclaredComponentKeyForRootDocumentBackReference() { + ParseOptions options = new ParseOptions(); + options.setResolveResponses(true); + options.setValidateExternalRefs(true); + options.setResolveFully(true); + + SwaggerParseResult result = new OpenAPIV3Parser().readLocation( + "src/test/resources/issue-2399/api.json", null, options); + + assertNotNull(result.getOpenAPI()); + assertTrue(result.getMessages().isEmpty(), "Unexpected parser messages: " + result.getMessages()); + + Map schemas = result.getOpenAPI().getComponents().getSchemas(); + assertEquals(schemas.size(), 4); + assertTrue(schemas.keySet().containsAll(Arrays.asList( + "Order-item", "Order-status", "orderItem", "orderStatus"))); + assertFalse(schemas.containsKey("Order-status_1")); + + Schema responseSchema = result.getOpenAPI().getPaths().get("/orders").getGet() + .getResponses().get("200").getContent().get("application/json").getSchema(); + Schema orderSchema = responseSchema.getItems(); + Schema statusSchema = (Schema) orderSchema.getProperties().get("status"); + assertEquals(statusSchema.getEnum(), Arrays.asList("PENDING", "SHIPPED")); + } +} diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolverCacheTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolverCacheTest.java index 22c96f0c78..89c517b806 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolverCacheTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolverCacheTest.java @@ -359,6 +359,7 @@ public void testRootReferenceUsesObjectPresentAtCacheConstruction() { root + "#/components/schemas/Foo", RefFormat.URL, Schema.class); assertSame(result, originalRootSchema); + assertEquals(cache.getRootReferenceName(root + "#/components/schemas/Foo"), "Foo"); } @Test @@ -430,17 +431,20 @@ public void testUnsupportedAbsentAndWrongTypePointersUseExternalDocument() { }}; ResolverCache cache = new ResolverCache(openAPI, auths, root); - Schema absent = cache.loadRef( - root + "#/components/schemas/External", RefFormat.URL, Schema.class); - Schema deeper = cache.loadRef( - root + "#/components/schemas/Foo/properties/value", RefFormat.URL, Schema.class); - Parameter wrongType = cache.loadRef( - root + "#/components/schemas/Foo", RefFormat.URL, Parameter.class); + String absentRef = root + "#/components/schemas/External"; + String deeperRef = root + "#/components/schemas/Foo/properties/value"; + String wrongTypeRef = root + "#/components/schemas/Foo"; + Schema absent = cache.loadRef(absentRef, RefFormat.URL, Schema.class); + Schema deeper = cache.loadRef(deeperRef, RefFormat.URL, Schema.class); + Parameter wrongType = cache.loadRef(wrongTypeRef, RefFormat.URL, Parameter.class); assertEquals(absent.getType(), "string"); assertEquals(deeper.getType(), "integer"); assertNotNull(wrongType); assertEquals(wrongType.getName(), "id"); + assertNull(cache.getRootReferenceName(absentRef)); + assertNull(cache.getRootReferenceName(deeperRef)); + assertNull(cache.getRootReferenceName(wrongTypeRef)); } @Test diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2399/api.json b/modules/swagger-parser-v3/src/test/resources/issue-2399/api.json new file mode 100644 index 0000000000..700305b8d6 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2399/api.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Order API", + "version": "1.0.0" + }, + "paths": { + "/orders": { + "get": { + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order-item" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Order-item": { + "$ref": "./models/orderItem.json" + }, + "Order-status": { + "$ref": "./models/common/orderStatus.json" + } + } + } +} diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2399/models/common/orderStatus.json b/modules/swagger-parser-v3/src/test/resources/issue-2399/models/common/orderStatus.json new file mode 100644 index 0000000000..1ff22819ac --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2399/models/common/orderStatus.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "enum": ["PENDING", "SHIPPED"] +} diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2399/models/orderItem.json b/modules/swagger-parser-v3/src/test/resources/issue-2399/models/orderItem.json new file mode 100644 index 0000000000..f1e8034dc8 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2399/models/orderItem.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "required": ["id", "status"], + "properties": { + "id": { + "type": "string" + }, + "status": { + "$ref": "../api.json#/components/schemas/Order-status" + } + } +} diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2399/root-alias.json b/modules/swagger-parser-v3/src/test/resources/issue-2399/root-alias.json new file mode 100644 index 0000000000..f002b60a5e --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2399/root-alias.json @@ -0,0 +1,18 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Root alias API", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Target": { + "type": "string" + }, + "Alias": { + "$ref": "./root-alias.json#/components/schemas/Target" + } + } + } +}