Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -131,9 +133,11 @@ public <T> T loadRef(String ref, RefFormat refFormat, Class<T> 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)){
Expand All @@ -147,7 +151,7 @@ public <T> T loadRef(String ref, RefFormat refFormat, Class<T> 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()){
Expand All @@ -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);
Expand All @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<String, Object> getResolutionCache() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import io.swagger.v3.parser.ResolverCache;
import io.swagger.v3.parser.models.RefFormat;
import io.swagger.v3.parser.models.RefType;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -65,7 +64,7 @@ private String finalNameRec(Map<String, Schema> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2014,6 +2017,20 @@ 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 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();
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) {
OpenAPIV3Parser parser = new OpenAPIV3Parser();
ParseOptions options = new ParseOptions();
Expand Down Expand Up @@ -3342,9 +3359,11 @@ 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", "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")
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
properties:
events:
items:
$ref: './Event.yaml'
type: array
meta:
$ref: './Meta.yaml'
type: object
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
properties:
href:
type: string
required:
- href
type: object
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading