From 5a85776ea449e45a3713ea6fa80d227e91666348 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Fri, 14 Jun 2024 13:55:18 -0500 Subject: [PATCH 1/6] add targeted sample spec to demonstrate duplicated equivalent refs --- .../v3/parser/test/OpenAPIV3ParserTest.java | 15 ++++++ .../components/schemas/Error.yaml | 13 ++++++ .../components/schemas/Event.yaml | 26 +++++++++++ .../components/schemas/EventList.yaml | 8 ++++ .../oas3.fetched/components/schemas/Href.yaml | 6 +++ .../oas3.fetched/components/schemas/Meta.yaml | 18 ++++++++ .../test/resources/oas3.fetched/openapi3.yaml | 40 ++++++++++++++++ .../connections/connection_id/events.yaml | 39 ++++++++++++++++ .../connection_id/ports/id/events.yaml | 46 +++++++++++++++++++ .../oas3.fetched/paths/devices/id/events.yaml | 39 ++++++++++++++++ .../resources/oas3.fetched/paths/events.yaml | 20 ++++++++ .../oas3.fetched/paths/events/id.yaml | 39 ++++++++++++++++ .../paths/organizations/id/events.yaml | 39 ++++++++++++++++ .../paths/projects/id/events.yaml | 39 ++++++++++++++++ .../oas3.fetched/paths/routes/id/events.yaml | 39 ++++++++++++++++ .../paths/virtual-circuits/id/events.yaml | 39 ++++++++++++++++ 16 files changed, 465 insertions(+) create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Error.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Event.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/EventList.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Href.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Meta.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/openapi3.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/events.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/ports/id/events.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/devices/id/events.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events/id.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/organizations/id/events.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/projects/id/events.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/routes/id/events.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/virtual-circuits/id/events.yaml 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 430ce00ec2..5e1029e9e1 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 @@ -2014,6 +2014,21 @@ public void testRelativePath2() { Assert.assertEquals(readResult.getOpenAPI().getPaths().get("/pet/findByTags").getGet().getResponses().get("default").getContent().get("application/json").getSchema().get$ref(), "#/components/schemas/ErrorModel"); } + @Test + public void testExternalRefsNormalization() throws Exception { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + SwaggerParseResult result = new OpenAPIV3Parser() + .readLocation("src/test/resources/oas3.fetched/openapi3.yaml", null, options); + + OpenAPI openAPI = result.getOpenAPI(); + Schema originalModel = openAPI.getComponents().getSchemas().get("Event"); + Schema duplicateModel = openAPI.getComponents().getSchemas().get("Event_1"); + System.out.println("component schemas found: " + openAPI.getComponents().getSchemas().keySet()); + assertNull(duplicateModel); + assertNotNull(originalModel); + } + private OpenAPI doRelativeFileTest(String location) { OpenAPIV3Parser parser = new OpenAPIV3Parser(); ParseOptions options = new ParseOptions(); diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Error.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Error.yaml new file mode 100644 index 0000000000..f940b13db2 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Error.yaml @@ -0,0 +1,13 @@ +description: Error responses are included with 4xx and 5xx HTTP responses from the + API service. Either "error" or "errors" will be set. +properties: + error: + description: A description of the error that caused the request to fail. + type: string + errors: + description: A list of errors that contributed to the request failing. + items: + description: An error message that contributed to the request failing. + type: string + type: array +type: object diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Event.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Event.yaml new file mode 100644 index 0000000000..9f4effd2e3 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Event.yaml @@ -0,0 +1,26 @@ +properties: + body: + type: string + created_at: + format: date-time + type: string + href: + type: string + id: + format: uuid + type: string + interpolated: + type: string + relationships: + items: + $ref: './Href.yaml' + type: array + state: + type: string + type: + type: string + modified_by: + type: object + ip: + type: string +type: object diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/EventList.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/EventList.yaml new file mode 100644 index 0000000000..75b2d4c509 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/EventList.yaml @@ -0,0 +1,8 @@ +properties: + events: + items: + $ref: './Event.yaml' + type: array + meta: + $ref: './Meta.yaml' +type: object diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Href.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Href.yaml new file mode 100644 index 0000000000..bbe7e22053 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Href.yaml @@ -0,0 +1,6 @@ +properties: + href: + type: string +required: +- href +type: object diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Meta.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Meta.yaml new file mode 100644 index 0000000000..88a52dc17b --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/components/schemas/Meta.yaml @@ -0,0 +1,18 @@ +properties: + first: + $ref: './Href.yaml' + last: + $ref: './Href.yaml' + next: + $ref: './Href.yaml' + previous: + $ref: './Href.yaml' + self: + $ref: './Href.yaml' + total: + type: integer + current_page: + type: integer + last_page: + type: integer +type: object diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/openapi3.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/openapi3.yaml new file mode 100644 index 0000000000..1397f45487 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/openapi3.yaml @@ -0,0 +1,40 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Example Duplicate Refs API + contact: + email: support@example.com + name: Example API Team + description: | + Sample API spec to validate handling of different file paths that reference the same file + license: + name: Equinix Metal + url: https://metal.equinix.com/legal/ + termsOfService: https://metal.equinix.com/legal/ +servers: + - url: https://api.example.com/duplicateRefs +components: + schemas: + Error: + $ref: "./components/schemas/Error.yaml" + Event: + $ref: "./components/schemas/Event.yaml" + EventList: + $ref: "./components/schemas/EventList.yaml" +paths: + /connections/{connection_id}/ports/{id}/events: + $ref: ./paths/connections/connection_id/ports/id/events.yaml + /devices/{id}/events: + $ref: ./paths/devices/id/events.yaml + /events: + $ref: ./paths/events.yaml + /events/{id}: + $ref: ./paths/events/id.yaml + /organizations/{id}/events: + $ref: ./paths/organizations/id/events.yaml + /projects/{id}/events: + $ref: ./paths/projects/id/events.yaml + /routes/{id}/events: + $ref: ./paths/routes/id/events.yaml + /virtual-circuits/{id}/events: + $ref: ./paths/virtual-circuits/id/events.yaml diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/events.yaml new file mode 100644 index 0000000000..e684f0cf10 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/events.yaml @@ -0,0 +1,39 @@ +get: + description: Returns a list of the interconnection events + operationId: findInterconnectionEvents + parameters: + - description: Interconnection UUID + in: path + name: connection_id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../../components/schemas/EventList.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: not found + summary: Retrieve interconnection events + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/ports/id/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/ports/id/events.yaml new file mode 100644 index 0000000000..b96b25ac29 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/connections/connection_id/ports/id/events.yaml @@ -0,0 +1,46 @@ +get: + description: Returns a list of the interconnection port events + operationId: findInterconnectionPortEvents + parameters: + - description: Interconnection UUID + in: path + name: connection_id + required: true + schema: + format: uuid + type: string + - description: Interconnection Port UUID + in: path + name: id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../../../../components/schemas/Event.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../../../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../../../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../../../../components/schemas/Error.yaml" + description: not found + summary: Retrieve interconnection port events + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/devices/id/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/devices/id/events.yaml new file mode 100644 index 0000000000..3f56542376 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/devices/id/events.yaml @@ -0,0 +1,39 @@ +get: + description: Returns a list of events pertaining to a specific device + operationId: findDeviceEvents + parameters: + - description: Device UUID + in: path + name: id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../../components/schemas/EventList.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: not found + summary: Retrieve device's events + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events.yaml new file mode 100644 index 0000000000..fd119be3ea --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events.yaml @@ -0,0 +1,20 @@ +get: + description: Returns a list of the current user’s events + operationId: findEvents + parameters: + responses: + "200": + content: + application/json: + schema: + $ref: "../components/schemas/EventList.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../components/schemas/Error.yaml" + description: unauthorized + summary: Retrieve current user's events + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events/id.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events/id.yaml new file mode 100644 index 0000000000..ab793220d1 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/events/id.yaml @@ -0,0 +1,39 @@ +get: + description: Returns a single event if the user has access + operationId: findEventById + parameters: + - description: Event UUID + in: path + name: id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../components/schemas/Event.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../components/schemas/Error.yaml" + description: not found + summary: Retrieve an event + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/organizations/id/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/organizations/id/events.yaml new file mode 100644 index 0000000000..b778851f9f --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/organizations/id/events.yaml @@ -0,0 +1,39 @@ +get: + description: Returns a list of events for a single organization + operationId: findOrganizationEvents + parameters: + - description: Organization UUID + in: path + name: id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../../components/schemas/EventList.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: not found + summary: Retrieve organization's events + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/projects/id/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/projects/id/events.yaml new file mode 100644 index 0000000000..93828fb412 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/projects/id/events.yaml @@ -0,0 +1,39 @@ +get: + description: Returns a list of events for a single project + operationId: findProjectEvents + parameters: + - description: Project UUID + in: path + name: id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../../components/schemas/EventList.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: not found + summary: Retrieve project's events + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/routes/id/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/routes/id/events.yaml new file mode 100644 index 0000000000..8cb492b933 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/routes/id/events.yaml @@ -0,0 +1,39 @@ +get: + description: Returns a list of the VRF route events + operationId: findVrfRouteEvents + parameters: + - description: VRF Route UUID + in: path + name: id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../../components/schemas/Event.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: not found + summary: Retrieve VRF route events + tags: + - Events diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/virtual-circuits/id/events.yaml b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/virtual-circuits/id/events.yaml new file mode 100644 index 0000000000..c3b46da5c8 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/oas3.fetched/paths/virtual-circuits/id/events.yaml @@ -0,0 +1,39 @@ +get: + description: Returns a list of the virtual circuit events + operationId: findVirtualCircuitEvents + parameters: + - description: Virtual Circuit UUID + in: path + name: id + required: true + schema: + format: uuid + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "../../../components/schemas/Event.yaml" + description: ok + "401": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: unauthorized + "403": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: forbidden + "404": + content: + application/json: + schema: + $ref: "../../../components/schemas/Error.yaml" + description: not found + summary: Retrieve virtual circuit events + tags: + - Events From ceb028c0b9d0eeaca8a81a03ad953cf4f8b1b338 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Thu, 8 Aug 2024 14:56:20 -0500 Subject: [PATCH 2/6] Normalize refs to prevent schema duplication --- .../parser/processors/ExternalRefProcessor.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 4320f43208..9ded7824eb 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 @@ -34,6 +34,7 @@ import io.swagger.v3.parser.ResolverCache; import io.swagger.v3.parser.models.RefFormat; import io.swagger.v3.parser.models.RefType; +import io.swagger.v3.parser.util.RefUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; @@ -92,6 +93,20 @@ public String processRefToExternalSchema(String $ref, RefFormat refFormat) { return renamedRef; } + RefFormat format = computeRefFormat($ref); + if (format.equals(RefFormat.RELATIVE)) { + String normalizedRef = "./" + Paths.get($ref).normalize().toString(); + if (!normalizedRef.equals($ref)) { + System.out.println("Normalized " + $ref + " to " + normalizedRef); + renamedRef = cache.getRenamedRef($ref); + if (renamedRef != null) { + return renamedRef; + } else { + $ref = normalizedRef; + } + } + } + final Schema schema = cache.loadRef($ref, refFormat, Schema.class); if(schema == null) { From 753cb10ddfca42276094dc7e1af2a25a8be09afa Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Wed, 8 Jan 2025 10:35:16 -0600 Subject: [PATCH 3/6] fix test that was checking for duplicate model --- .../java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5e1029e9e1..c60e819e05 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 @@ -3359,7 +3359,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") ); From 9a05a4bacddaa2ed05f0e4b5192bf82fcb2db7e9 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Thu, 9 Jan 2025 14:20:37 -0600 Subject: [PATCH 4/6] use existing RefUtils.mungedRef function --- .../io/swagger/v3/parser/processors/ExternalRefProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9ded7824eb..6d9bd1970f 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 @@ -95,7 +95,7 @@ public String processRefToExternalSchema(String $ref, RefFormat refFormat) { RefFormat format = computeRefFormat($ref); if (format.equals(RefFormat.RELATIVE)) { - String normalizedRef = "./" + Paths.get($ref).normalize().toString(); + String normalizedRef = RefUtils.mungedRef(Paths.get($ref).normalize().toString()); if (!normalizedRef.equals($ref)) { System.out.println("Normalized " + $ref + " to " + normalizedRef); renamedRef = cache.getRenamedRef($ref); From 9fbb60f5130154dca18509d99b2e7af7271a78e3 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Thu, 13 Aug 2026 13:45:46 +0200 Subject: [PATCH 5/6] Move normalization to ResolverCache. Add tests --- .../io/swagger/v3/parser/ResolverCache.java | 29 +++-- .../processors/ExternalRefProcessor.java | 18 +-- .../v3/parser/test/OpenAPIV3ParserTest.java | 16 ++- .../v3/parser/test/ResolverCacheTest.java | 106 ++++++++++++++++++ 4 files changed, 139 insertions(+), 30 deletions(-) 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..dbc96fa43e 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,6 +28,8 @@ import java.io.File; import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URLDecoder; import java.nio.file.Path; import java.util.ArrayList; @@ -131,9 +133,11 @@ public T loadRef(String ref, RefFormat refFormat, Class expectedType) { final String file = refParts[0]; final String definitionPath = refParts.length == 2 ? refParts[1] : null; + final String canonicalRef = canonicalize(ref); + final String canonicalFile = canonicalize(file); //we might have already resolved this ref, so check the resolutionCache - Object previouslyResolvedEntity = resolutionCache.get(ref); + Object previouslyResolvedEntity = resolutionCache.get(canonicalRef); if (previouslyResolvedEntity != null) { if(expectedType.equals(Header.class)){ @@ -147,7 +151,7 @@ public T loadRef(String ref, RefFormat refFormat, Class expectedType) { //we have not resolved this particular ref //but we may have already loaded the file or url in question - String contents = externalFileCache.get(file); + String contents = externalFileCache.get(canonicalFile); if (contents == null) { if(parseOptions.isSafelyResolveURL()){ @@ -164,7 +168,7 @@ else if (rootPath != null) { contents = RefUtils.readExternalClasspathRef(file, refFormat, auths, rootPath, permittedUrlsChecker); } - externalFileCache.put(file, contents); + externalFileCache.put(canonicalFile, contents); } SwaggerParseResult deserializationUtilResult = new SwaggerParseResult(); JsonNode tree = DeserializationUtils.deserializeIntoTree(contents, file, parseOptions, deserializationUtilResult); @@ -176,7 +180,7 @@ else if (rootPath != null) { } else { result = DeserializationUtils.deserialize(contents, file, expectedType, openapi31); } - resolutionCache.put(ref, result); + resolutionCache.put(canonicalRef, result); if (deserializationUtilResult.getMessages() != null) { if (this.resolveValidationMessages != null) { this.resolveValidationMessages.addAll(deserializationUtilResult.getMessages()); @@ -215,7 +219,7 @@ else if (rootPath != null) { } } updateLocalRefs(file, result); - resolutionCache.put(ref, result); + resolutionCache.put(canonicalRef, result); if (deserializationUtilResult.getMessages() != null) { if (this.resolveValidationMessages != null) { this.resolveValidationMessages.addAll(deserializationUtilResult.getMessages()); @@ -402,11 +406,22 @@ public void addReferencedKey(String modelKey) { } public String getRenamedRef(String originalRef) { - return renameCache.get(originalRef); + return renameCache.get(canonicalize(originalRef)); } public void putRenamedRef(String originalRef, String newRef) { - renameCache.put(originalRef, newRef); + renameCache.put(canonicalize(originalRef), newRef); + } + + private static String canonicalize(String ref) { + if (ref == null || ref.isEmpty()) { + return ref; + } + try { + return new URI(ref).normalize().toString(); + } catch (URISyntaxException ignored) { + return ref; + } } public Map getResolutionCache() { 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 6d9bd1970f..ca13e00a0d 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 @@ -34,8 +34,6 @@ import io.swagger.v3.parser.ResolverCache; import io.swagger.v3.parser.models.RefFormat; import io.swagger.v3.parser.models.RefType; -import io.swagger.v3.parser.util.RefUtils; - import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.LoggerFactory; @@ -66,7 +64,7 @@ private String finalNameRec(Map schemas, String possiblyConflict // use the new model existingModel = null; } else if (!newSchema.equals(existingModel)) { - if(cache.getResolutionCache().get(newSchema.get$ref())!= null){ + if (cache.getRenamedRef(newSchema.get$ref()) != null) { return tryName; } LOGGER.debug("A model for " + existingModel + " already exists"); @@ -93,20 +91,6 @@ public String processRefToExternalSchema(String $ref, RefFormat refFormat) { return renamedRef; } - RefFormat format = computeRefFormat($ref); - if (format.equals(RefFormat.RELATIVE)) { - String normalizedRef = RefUtils.mungedRef(Paths.get($ref).normalize().toString()); - if (!normalizedRef.equals($ref)) { - System.out.println("Normalized " + $ref + " to " + normalizedRef); - renamedRef = cache.getRenamedRef($ref); - if (renamedRef != null) { - return renamedRef; - } else { - $ref = normalizedRef; - } - } - } - final Schema schema = cache.loadRef($ref, refFormat, Schema.class); if(schema == null) { 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 c60e819e05..91381ae210 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 @@ -600,6 +600,9 @@ public void testIssue1518() { OpenAPI openAPI = result.getOpenAPI(); assertEquals(((Schema) openAPI.getComponents().getSchemas().get("Analemmata").getProperties().get("tashotSipe")).get$ref(), "#/components/schemas/TashotSipe"); assertNull(openAPI.getComponents().getSchemas().get("analemmata")); + assertNull(openAPI.getComponents().getSchemas().get("TashotSipe_1")); + assertNotNull(openAPI.getComponents().getSchemas().get("Stunts")); + assertNull(openAPI.getComponents().getSchemas().get("Stunts_1")); } @Test @@ -2015,18 +2018,17 @@ public void testRelativePath2() { } @Test - public void testExternalRefsNormalization() throws Exception { + public void testIssue2105EquivalentExternalRefsUseSingleComponent() { ParseOptions options = new ParseOptions(); options.setResolve(true); SwaggerParseResult result = new OpenAPIV3Parser() .readLocation("src/test/resources/oas3.fetched/openapi3.yaml", null, options); OpenAPI openAPI = result.getOpenAPI(); - Schema originalModel = openAPI.getComponents().getSchemas().get("Event"); - Schema duplicateModel = openAPI.getComponents().getSchemas().get("Event_1"); - System.out.println("component schemas found: " + openAPI.getComponents().getSchemas().keySet()); - assertNull(duplicateModel); - assertNotNull(originalModel); + assertNotNull(openAPI.getComponents().getSchemas().get("Event")); + assertNotNull(openAPI.getComponents().getSchemas().get("EventList")); + assertNull(openAPI.getComponents().getSchemas().get("Event_1")); + assertNull(openAPI.getComponents().getSchemas().get("EventList_1")); } private OpenAPI doRelativeFileTest(String location) { @@ -3357,6 +3359,8 @@ public void testIssue1886() { OpenAPIV3Parser openApiParser = new OpenAPIV3Parser(); SwaggerParseResult parseResult = openApiParser.readLocation("issue-1886/openapi.yaml", null, options); OpenAPI openAPI = parseResult.getOpenAPI(); + assertNotNull(openAPI.getComponents().getSchemas().get("Enum1")); + assertNull(openAPI.getComponents().getSchemas().get("Enum1_1")); assertEqualsNoOrder( openAPI.getComponents().getSchemas().keySet(), Arrays.asList("ArrayPojo", "Enum1", "Enum2", "Enum3", "MapPojo", "SetPojo", "SimplePojo", 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..4ad83c2e89 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 @@ -29,6 +29,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; public class ResolverCacheTest { @@ -258,4 +259,109 @@ public void testRenameCache() { cache.putRenamedRef("foo", "bar"); assertEquals(cache.getRenamedRef("foo"), "bar"); } + + @Test + public void testEquivalentUriRefsShareCacheIdentity() { + final String refWithDotSegments = + "http://my.company.com/schemas/../schemas/file.yaml#/components/schemas/Foo"; + final String canonicalRef = + "http://my.company.com/schemas/file.yaml#/components/schemas/Foo"; + final String contents = "components:\n schemas:\n Foo:\n type: string\n"; + + new Expectations() {{ + RefUtils.readExternalUrlRef( + "http://my.company.com/schemas/../schemas/file.yaml", + RefFormat.URL, + auths, + "http://my.company.com/root.yaml", + (PermittedUrlsChecker) any); + times = 1; + result = contents; + }}; + + ResolverCache cache = new ResolverCache(openAPI, auths, "http://my.company.com/root.yaml"); + Schema first = cache.loadRef(refWithDotSegments, RefFormat.URL, Schema.class); + Schema second = cache.loadRef(canonicalRef, RefFormat.URL, Schema.class); + + assertSame(first, second); + assertEquals(cache.getExternalFileCache().size(), 1); + assertEquals(cache.getResolutionCache().size(), 1); + cache.putRenamedRef(refWithDotSegments, "Foo"); + assertEquals(cache.getRenamedRef(canonicalRef), "Foo"); + } + + @Test + public void testEquivalentApiResponseRefsShareCacheIdentity() { + final String refWithDotSegments = + "http://my.company.com/responses/../responses/common.yaml#/components/responses/Error"; + final String canonicalRef = + "http://my.company.com/responses/common.yaml#/components/responses/Error"; + final String contents = + "components:\n responses:\n Error:\n description: Error response\n"; + + new Expectations() {{ + RefUtils.readExternalUrlRef( + "http://my.company.com/responses/../responses/common.yaml", + RefFormat.URL, + auths, + "http://my.company.com/root.yaml", + (PermittedUrlsChecker) any); + times = 1; + result = contents; + }}; + + ResolverCache cache = new ResolverCache(openAPI, auths, "http://my.company.com/root.yaml"); + ApiResponse first = cache.loadRef(refWithDotSegments, RefFormat.URL, ApiResponse.class); + ApiResponse second = cache.loadRef(canonicalRef, RefFormat.URL, ApiResponse.class); + + assertSame(first, second); + assertEquals(first.getDescription(), "Error response"); + assertEquals(cache.getExternalFileCache().size(), 1); + assertEquals(cache.getResolutionCache().size(), 1); + } + + @Test + public void testIssue2016EquivalentRelativeRefsShareRenameCacheIdentity() { + ResolverCache cache = new ResolverCache(openAPI, auths, null); + String refWithRedundantDotSegment = "./../A.yaml#/components/schemas/A"; + String equivalentRef = "../A.yaml#/components/schemas/A"; + + cache.putRenamedRef(refWithRedundantDotSegment, "A"); + + assertEquals(cache.getRenamedRef(equivalentRef), "A"); + assertEquals(cache.getRenameCache().size(), 1); + } + + @Test + public void testCanonicalRenameCacheKeysPreserveUriParts() { + ResolverCache cache = new ResolverCache(openAPI, auths, null); + + cache.putRenamedRef( + "file:///tmp/schemas/../Foo.yaml#/components/schemas/Foo", "FileFoo"); + cache.putRenamedRef( + "/tmp/schemas/../Foo.yaml#/components/schemas/Foo", "AbsoluteFoo"); + cache.putRenamedRef( + "https://example.com/a/../Foo.yaml?version=1#/components/schemas/Foo", "HttpFoo"); + cache.putRenamedRef("#/components/schemas/Foo", "InternalFoo"); + cache.putRenamedRef("C:\\schemas\\Foo.yaml", "WindowsPath"); + cache.putRenamedRef( + "my schemas/../Foo.yaml#/components/schemas/Foo", "UnencodedSpace"); + + assertEquals( + cache.getRenameCache().get("file:/tmp/Foo.yaml#/components/schemas/Foo"), + "FileFoo"); + assertEquals( + cache.getRenameCache().get("/tmp/Foo.yaml#/components/schemas/Foo"), + "AbsoluteFoo"); + assertEquals( + cache.getRenameCache().get( + "https://example.com/Foo.yaml?version=1#/components/schemas/Foo"), + "HttpFoo"); + assertEquals(cache.getRenameCache().get("#/components/schemas/Foo"), "InternalFoo"); + assertEquals(cache.getRenameCache().get("C:\\schemas\\Foo.yaml"), "WindowsPath"); + assertEquals( + cache.getRenameCache().get( + "my schemas/../Foo.yaml#/components/schemas/Foo"), + "UnencodedSpace"); + } } From 0312386103940678670697b8a09d86aa9ac20042 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Mon, 17 Aug 2026 08:38:25 +0200 Subject: [PATCH 6/6] normalize keys --- .../io/swagger/v3/parser/ResolverCache.java | 25 +++++++---- .../v3/parser/test/ResolverCacheTest.java | 41 +++++++++++++++---- 2 files changed, 51 insertions(+), 15 deletions(-) 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 dbc96fa43e..c8ed14ea29 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 @@ -70,6 +70,8 @@ public class ResolverCache { private final String rootPath; private Map resolutionCache = new HashMap<>(); private Map externalFileCache = new HashMap<>(); + private Map canonicalResolutionCache = new HashMap<>(); + private Map canonicalExternalFileCache = new HashMap<>(); private List referencedModelKeys = new ArrayList<>(); private Set resolveValidationMessages; private final ParseOptions parseOptions; @@ -81,6 +83,7 @@ public class ResolverCache { * references */ private Map renameCache = new HashMap<>(); + private Map canonicalRenameCache = new HashMap<>(); public ResolverCache(OpenAPI openApi, List auths, String parentFileLocation) { this(openApi, auths, parentFileLocation, new HashSet<>()); @@ -136,10 +139,12 @@ public T loadRef(String ref, RefFormat refFormat, Class expectedType) { final String canonicalRef = canonicalize(ref); final String canonicalFile = canonicalize(file); - //we might have already resolved this ref, so check the resolutionCache - Object previouslyResolvedEntity = resolutionCache.get(canonicalRef); + //we might have already resolved an equivalent ref, so check the canonical cache + Object previouslyResolvedEntity = canonicalResolutionCache.get(canonicalRef); if (previouslyResolvedEntity != null) { + resolutionCache.putIfAbsent(ref, previouslyResolvedEntity); + externalFileCache.putIfAbsent(file, canonicalExternalFileCache.get(canonicalFile)); if(expectedType.equals(Header.class)){ if (expectedType.getClass().equals(previouslyResolvedEntity.getClass())) { return expectedType.cast(previouslyResolvedEntity); @@ -151,7 +156,7 @@ public T loadRef(String ref, RefFormat refFormat, Class expectedType) { //we have not resolved this particular ref //but we may have already loaded the file or url in question - String contents = externalFileCache.get(canonicalFile); + String contents = canonicalExternalFileCache.get(canonicalFile); if (contents == null) { if(parseOptions.isSafelyResolveURL()){ @@ -168,8 +173,9 @@ else if (rootPath != null) { contents = RefUtils.readExternalClasspathRef(file, refFormat, auths, rootPath, permittedUrlsChecker); } - externalFileCache.put(canonicalFile, contents); + canonicalExternalFileCache.put(canonicalFile, contents); } + externalFileCache.putIfAbsent(file, contents); SwaggerParseResult deserializationUtilResult = new SwaggerParseResult(); JsonNode tree = DeserializationUtils.deserializeIntoTree(contents, file, parseOptions, deserializationUtilResult); @@ -180,7 +186,8 @@ else if (rootPath != null) { } else { result = DeserializationUtils.deserialize(contents, file, expectedType, openapi31); } - resolutionCache.put(canonicalRef, result); + resolutionCache.put(ref, result); + canonicalResolutionCache.put(canonicalRef, result); if (deserializationUtilResult.getMessages() != null) { if (this.resolveValidationMessages != null) { this.resolveValidationMessages.addAll(deserializationUtilResult.getMessages()); @@ -219,7 +226,8 @@ else if (rootPath != null) { } } updateLocalRefs(file, result); - resolutionCache.put(canonicalRef, result); + resolutionCache.put(ref, result); + canonicalResolutionCache.put(canonicalRef, result); if (deserializationUtilResult.getMessages() != null) { if (this.resolveValidationMessages != null) { this.resolveValidationMessages.addAll(deserializationUtilResult.getMessages()); @@ -406,11 +414,12 @@ public void addReferencedKey(String modelKey) { } public String getRenamedRef(String originalRef) { - return renameCache.get(canonicalize(originalRef)); + return canonicalRenameCache.get(canonicalize(originalRef)); } public void putRenamedRef(String originalRef, String newRef) { - renameCache.put(canonicalize(originalRef), newRef); + renameCache.put(originalRef, newRef); + canonicalRenameCache.put(canonicalize(originalRef), newRef); } private static String canonicalize(String ref) { 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 4ad83c2e89..45297ab0b2 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 @@ -284,10 +284,21 @@ public void testEquivalentUriRefsShareCacheIdentity() { Schema second = cache.loadRef(canonicalRef, RefFormat.URL, Schema.class); assertSame(first, second); - assertEquals(cache.getExternalFileCache().size(), 1); - assertEquals(cache.getResolutionCache().size(), 1); + assertEquals(cache.getExternalFileCache().size(), 2); + assertEquals( + cache.getExternalFileCache().get( + "http://my.company.com/schemas/../schemas/file.yaml"), + contents); + assertEquals( + cache.getExternalFileCache().get("http://my.company.com/schemas/file.yaml"), + contents); + assertEquals(cache.getResolutionCache().size(), 2); + assertSame(cache.getResolutionCache().get(refWithDotSegments), first); + assertSame(cache.getResolutionCache().get(canonicalRef), first); cache.putRenamedRef(refWithDotSegments, "Foo"); assertEquals(cache.getRenamedRef(canonicalRef), "Foo"); + assertEquals(cache.getRenameCache().get(refWithDotSegments), "Foo"); + assertNull(cache.getRenameCache().get(canonicalRef)); } @Test @@ -316,8 +327,10 @@ public void testEquivalentApiResponseRefsShareCacheIdentity() { assertSame(first, second); assertEquals(first.getDescription(), "Error response"); - assertEquals(cache.getExternalFileCache().size(), 1); - assertEquals(cache.getResolutionCache().size(), 1); + assertEquals(cache.getExternalFileCache().size(), 2); + assertEquals(cache.getResolutionCache().size(), 2); + assertSame(cache.getResolutionCache().get(refWithDotSegments), first); + assertSame(cache.getResolutionCache().get(canonicalRef), first); } @Test @@ -330,6 +343,8 @@ public void testIssue2016EquivalentRelativeRefsShareRenameCacheIdentity() { assertEquals(cache.getRenamedRef(equivalentRef), "A"); assertEquals(cache.getRenameCache().size(), 1); + assertEquals(cache.getRenameCache().get(refWithRedundantDotSegment), "A"); + assertNull(cache.getRenameCache().get(equivalentRef)); } @Test @@ -348,14 +363,16 @@ public void testCanonicalRenameCacheKeysPreserveUriParts() { "my schemas/../Foo.yaml#/components/schemas/Foo", "UnencodedSpace"); assertEquals( - cache.getRenameCache().get("file:/tmp/Foo.yaml#/components/schemas/Foo"), + cache.getRenameCache().get( + "file:///tmp/schemas/../Foo.yaml#/components/schemas/Foo"), "FileFoo"); assertEquals( - cache.getRenameCache().get("/tmp/Foo.yaml#/components/schemas/Foo"), + cache.getRenameCache().get( + "/tmp/schemas/../Foo.yaml#/components/schemas/Foo"), "AbsoluteFoo"); assertEquals( cache.getRenameCache().get( - "https://example.com/Foo.yaml?version=1#/components/schemas/Foo"), + "https://example.com/a/../Foo.yaml?version=1#/components/schemas/Foo"), "HttpFoo"); assertEquals(cache.getRenameCache().get("#/components/schemas/Foo"), "InternalFoo"); assertEquals(cache.getRenameCache().get("C:\\schemas\\Foo.yaml"), "WindowsPath"); @@ -363,5 +380,15 @@ public void testCanonicalRenameCacheKeysPreserveUriParts() { cache.getRenameCache().get( "my schemas/../Foo.yaml#/components/schemas/Foo"), "UnencodedSpace"); + assertEquals( + cache.getRenamedRef("file:/tmp/Foo.yaml#/components/schemas/Foo"), + "FileFoo"); + assertEquals( + cache.getRenamedRef("/tmp/Foo.yaml#/components/schemas/Foo"), + "AbsoluteFoo"); + assertEquals( + cache.getRenamedRef( + "https://example.com/Foo.yaml?version=1#/components/schemas/Foo"), + "HttpFoo"); } }