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 ba1db7ddef..36e2165fc7 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 @@ -28,15 +28,10 @@ import java.io.File; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URLDecoder; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -402,11 +397,33 @@ public void addReferencedKey(String modelKey) { } public String getRenamedRef(String originalRef) { - return renameCache.get(originalRef); + return renameCache.get(createRenameCacheKey(originalRef)); } public void putRenamedRef(String originalRef, String newRef) { - renameCache.put(originalRef, newRef); + renameCache.put(createRenameCacheKey(originalRef), newRef); + } + + /** + * Creates a cache key for the rename cache for the provided reference. For consistent comparisons the external part + * is always normalized, e.g. ./components.yaml and components.yaml are considered to reference the same file. + * + * @param originalRef The reference for which we want to create a rename cache key + * @return The appropriate cache key for the reference + */ + private String createRenameCacheKey(String originalRef) { + int separatorIndex = originalRef.indexOf("#/"); + if (separatorIndex == -1) { + // Reference doesn't have a local part + return URI.create(originalRef).normalize().toString(); + } else if (separatorIndex == 0) { + // Reference doesn't have an external part + return originalRef; + } else { + // Reference with local and external part + URI externalRefURI = URI.create(originalRef.substring(0, separatorIndex)); + return externalRefURI.normalize() + originalRef.substring(separatorIndex); + } } public Map getResolutionCache() { diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java index cba66fb1b1..c8d68ce620 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.net.URL; +import java.nio.file.Path; import java.util.*; import static java.util.Arrays.asList; @@ -3295,7 +3296,7 @@ public void testIssue1886() { OpenAPI openAPI = parseResult.getOpenAPI(); assertEqualsNoOrder( openAPI.getComponents().getSchemas().keySet(), - Arrays.asList("ArrayPojo", "Enum1", "Enum1_1", "Enum2", "Enum3", "MapPojo", "SetPojo", "SimplePojo", + Arrays.asList("ArrayPojo", "Enum1", "Enum2", "Enum3", "MapPojo", "SetPojo", "SimplePojo", "TransactionsPatchRequestBody", "additional-properties", "array-pojo", "locale-translation-item", "map-pojo", "set-pojo", "simple-pojo", "translation-item") ); @@ -3313,6 +3314,26 @@ public void testIssue2081() { assertEquals(openAPI.getComponents().getSchemas().get("PetCreate").getProperties().size(), 2); } + @Test(description = "Should not create duplicate components when using mixed reference patters") + public void testIssue2217ExternalReferenceResolution() { + OpenAPIV3Parser openApiParser = new OpenAPIV3Parser(); + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setFlatten(true); + + SwaggerParseResult parseResult = openApiParser.readLocation("issue-2217/main.yaml", null, options); + OpenAPI openAPI = parseResult.getOpenAPI(); + + Assert.assertNotNull(openAPI, "OpenAPI should be parsed successfully"); + + // Assert that EmployeeInfo_1 object does not appear in the parsed spec + Assert.assertNotNull(openAPI.getComponents(), "Components should exist"); + if (openAPI.getComponents().getSchemas() != null) { + Assert.assertFalse(openAPI.getComponents().getSchemas().containsKey("EmployeeInfo_1"), + "EmployeeInfo_1 should not be created during external reference resolution"); + } + } + @Test(description = "responses should be inline") public void testFullyResolveResponses() { ParseOptions options = new ParseOptions(); 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 843725c9fa..ff725c90d3 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 @@ -258,4 +258,33 @@ public void testRenameCache() { cache.putRenamedRef("foo", "bar"); assertEquals(cache.getRenamedRef("foo"), "bar"); } + + @Test + public void testRenameCacheNormalizationExternalAndLocalPart() { + ResolverCache cache = new ResolverCache(openAPI, auths, null); + + assertNull(cache.getRenamedRef("./components.yaml#/foo")); + cache.putRenamedRef("./components.yaml#/foo", "bar"); + assertEquals(cache.getRenamedRef("./components.yaml#/foo"), "bar"); + assertEquals(cache.getRenamedRef("components.yaml#/foo"), "bar"); + } + + @Test + public void testRenameCacheNormalizationNoLocalPart() { + ResolverCache cache = new ResolverCache(openAPI, auths, null); + + assertNull(cache.getRenamedRef("./components.yaml")); + cache.putRenamedRef("./components.yaml", "bar"); + assertEquals(cache.getRenamedRef("./components.yaml"), "bar"); + assertEquals(cache.getRenamedRef("components.yaml"), "bar"); + } + + @Test + public void testRenameCacheNormalizationNoExternalPart() { + ResolverCache cache = new ResolverCache(openAPI, auths, null); + + assertNull(cache.getRenamedRef("#/foo")); + cache.putRenamedRef("#/foo", "bar"); + assertEquals(cache.getRenamedRef("#/foo"), "bar"); + } } diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2217/fragments.yaml b/modules/swagger-parser-v3/src/test/resources/issue-2217/fragments.yaml new file mode 100644 index 0000000000..08dd510280 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2217/fragments.yaml @@ -0,0 +1,62 @@ +openapi: "3.0.0" +info: + version: 1.1.0 + title: fr_fragments +paths: {} +components: + schemas: + CreatedByEmployeeId: + type: string + EmployeesInfoPage: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/EmployeeInfo' + pageResponse: + $ref: pagination.yaml#/components/schemas/PageResponse + ArrayOfGroupsInfo: + type: array + items: + $ref: '#/components/schemas/GroupInfo' + EmployeeInfo: + type: object + properties: + employeeId: + type: string + groupTags: + $ref: '#/components/schemas/ArrayOfGroups' + ArrayOfGroups: + type: array + items: + $ref: '#/components/schemas/GroupTags' + GroupTags: + type: object + properties: + groupTag: + $ref: '#/components/schemas/GroupId' + GroupId: + type: string + PostEmployeeRequest: + $ref: '#/components/schemas/EmployeeInfo' + PutEmployeeRequest: + $ref: '#/components/schemas/EmployeeInfo' + EmployeeIdRequest: + type: object + properties: + employeeId: + type: string + responses: + EmployeeInfo200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeInfo' + EmployeeInfo201: + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeInfo' \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-2217/main.yaml b/modules/swagger-parser-v3/src/test/resources/issue-2217/main.yaml new file mode 100644 index 0000000000..f98cb110e2 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-2217/main.yaml @@ -0,0 +1,59 @@ +openapi: "3.0.0" +info: + title: Main test + version: 1.1.0 +paths: + /int/employees/extended: + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: 'fragments.yaml#/components/schemas/EmployeesInfoPage' + /int/employees: + post: + requestBody: + content: + application/json: + schema: + $ref: 'fragments.yaml#/components/schemas/PostEmployeeRequest' + required: true + responses: + "201": + $ref: 'fragments.yaml#/components/responses/EmployeeInfo201' + /int/employees/id: + put: + requestBody: + content: + application/json: + schema: + $ref: 'fragments.yaml#/components/schemas/PutEmployeeRequest' + required: true + responses: + "200": + $ref: 'fragments.yaml#/components/responses/EmployeeInfo200' + "201": + $ref: 'fragments.yaml#/components/responses/EmployeeInfo201' + /int/employees/id/request: + post: + requestBody: + content: + application/json: + schema: + $ref: 'fragments.yaml#/components/schemas/EmployeeIdRequest' + required: true + responses: + "200": + $ref: 'fragments.yaml#/components/responses/EmployeeInfo200' + /int/employees/id/remove: + post: + requestBody: + content: + application/json: + schema: + $ref: 'fragments.yaml#/components/schemas/EmployeeIdRequest' + required: true + responses: + "204": + description: No Content \ No newline at end of file