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 7d549985a1..89158f7503 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 @@ -10,6 +10,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -892,6 +894,8 @@ private void processRefExample(Example example, String externalFile) { if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { $ref = join(externalFile, example.get$ref()); example.set$ref($ref); + resolveWholeDocumentRef($ref, this::processRefToExternalExample, example::set$ref, + RefType.COMPONENTS.getInternalPrefix() + "examples/"); }else { processRefToExternalExample($ref, format); } @@ -955,6 +959,8 @@ private void processRefSchema(Schema subRef, String externalFile) { if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { $ref = constructRef(subRef, externalFile); subRef.set$ref($ref); + resolveWholeDocumentRef($ref, this::processRefToExternalSchema, subRef::set$ref, + RefType.SCHEMAS.getInternalPrefix()); }else { processRefToExternalSchema($ref, format); } @@ -980,6 +986,8 @@ private void processRefHeader(Header subRef, String externalFile) { if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { $ref = join(externalFile, subRef.get$ref()); subRef.set$ref($ref); + resolveWholeDocumentRef($ref, this::processRefToExternalHeader, subRef::set$ref, + RefType.COMPONENTS.getInternalPrefix() + "headers/"); }else { processRefToExternalHeader($ref, format); } @@ -999,12 +1007,32 @@ private void processRefLink(Link subRef, String externalFile) { if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { $ref = join(externalFile, subRef.get$ref()); subRef.set$ref($ref); + resolveWholeDocumentRef($ref, this::processRefToExternalLink, subRef::set$ref, + RefType.COMPONENTS.getInternalPrefix() + "links/"); }else { processRefToExternalLink($ref, format); } } + private static boolean isWholeDocumentRef(String ref) { + return !ref.contains("#"); + } + + private void resolveWholeDocumentRef(String ref, BiFunction processor, + Consumer setRef, String internalPrefix) { + if (!isWholeDocumentRef(ref)) { + return; + } + + RefFormat format = computeRefFormat(ref); + processor.apply(ref, format); + String renamedRef = cache.getRenamedRef(ref); + if (renamedRef != null) { + setRef.accept(internalPrefix + renamedRef); + } + } + // visible for testing public static String join(String source, String fragment) { try { diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue1889Test.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue1889Test.java new file mode 100644 index 0000000000..960cdc58a2 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/Issue1889Test.java @@ -0,0 +1,163 @@ +package io.swagger.v3.parser.test; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.responses.ApiResponse; +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 static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class Issue1889Test { + + private static final String SCHEMA_PREFIX = "#/components/schemas/"; + private static final String HEADER_PREFIX = "#/components/headers/"; + + @Test + public void resolvesNestedRelativeArrayItemReference() { + OpenAPI openAPI = parse("issue-1889/root.yaml"); + + Schema items = getProductItems(openAPI); + String resolvedName = assertInternalReference(items); + + assertNotNull(openAPI.getComponents().getSchemas().get(resolvedName)); + } + + @Test + public void resolvesNestedRelativeArrayItemReferenceWithoutOverwritingExistingSchema() { + OpenAPI openAPI = parse("issue-1889/root-with-product.yaml"); + + Schema rootProduct = openAPI.getComponents().getSchemas().get("Product"); + assertNotNull(rootProduct); + assertNotNull(rootProduct.getProperties().get("rootOnly")); + + Schema items = getProductItems(openAPI); + String resolvedName = assertInternalReference(items); + Schema resolvedProduct = openAPI.getComponents().getSchemas().get(resolvedName); + + assertNotNull(resolvedProduct); + assertNotNull(resolvedProduct.getProperties().get("id")); + assertNull(resolvedProduct.getProperties().get("rootOnly")); + } + + @Test + public void resolvesNestedRelativeArrayItemReferenceWithFragment() { + OpenAPI openAPI = parse("issue-1889/root-fragment-ref.yaml"); + + Schema response = openAPI.getComponents().getSchemas().get("GetProductResponse"); + assertNotNull(response); + Schema products = (Schema) response.getProperties().get("products"); + assertTrue(products instanceof ArraySchema); + Schema items = ((ArraySchema) products).getItems(); + + assertNotNull(items); + assertNotNull(items.get$ref()); + assertTrue(items.get$ref().startsWith(SCHEMA_PREFIX), + "items.$ref should be internal ref, was: " + items.get$ref()); + assertFalse(items.get$ref().contains("schemas.yaml"), + "items.$ref should not contain file path, was: " + items.get$ref()); + + String resolvedName = items.get$ref().substring(SCHEMA_PREFIX.length()); + assertNotNull(openAPI.getComponents().getSchemas().get(resolvedName)); + } + + @Test + public void resolvesThreeLevelChainOfNestedRelativeFileReferences() { + OpenAPI openAPI = parse("issue-1889/root-chain.yaml"); + + Schema response = openAPI.getComponents().getSchemas().get("GetProductResponse"); + assertNotNull(response); + Schema products = (Schema) response.getProperties().get("products"); + assertTrue(products instanceof ArraySchema); + Schema productItems = ((ArraySchema) products).getItems(); + + assertNotNull(productItems); + assertNotNull(productItems.get$ref()); + assertTrue(productItems.get$ref().startsWith(SCHEMA_PREFIX), + "productItems.$ref should be internal ref, was: " + productItems.get$ref()); + + String productName = productItems.get$ref().substring(SCHEMA_PREFIX.length()); + Schema productSchema = openAPI.getComponents().getSchemas().get(productName); + assertNotNull(productSchema, "ProductChain schema should be in components"); + + Schema nestedItems = (Schema) productSchema.getProperties().get("items"); + assertNotNull(nestedItems); + assertTrue(nestedItems instanceof ArraySchema); + Schema itemRef = ((ArraySchema) nestedItems).getItems(); + + assertNotNull(itemRef); + assertNotNull(itemRef.get$ref()); + assertTrue(itemRef.get$ref().startsWith(SCHEMA_PREFIX), + "itemRef.$ref should be internal ref, was: " + itemRef.get$ref()); + assertFalse(itemRef.get$ref().contains("Item.yaml"), + "itemRef.$ref should not contain file path, was: " + itemRef.get$ref()); + + String itemName = itemRef.get$ref().substring(SCHEMA_PREFIX.length()); + assertNotNull(openAPI.getComponents().getSchemas().get(itemName), "Item schema should be in components"); + } + + @Test + public void resolvesNestedRelativeHeaderReferenceFromExternalResponse() { + OpenAPI openAPI = parse("issue-1889/root-header.yaml"); + ApiResponse response = getProductsResponse(openAPI); + assertNotNull(response.getHeaders(), "response should have headers"); + + io.swagger.v3.oas.models.headers.Header rateLimitHeader = response.getHeaders().get("X-Rate-Limit"); + assertNotNull(rateLimitHeader); + assertNotNull(rateLimitHeader.get$ref()); + assertTrue(rateLimitHeader.get$ref().startsWith(HEADER_PREFIX), + "X-Rate-Limit $ref should be internal header ref, was: " + rateLimitHeader.get$ref()); + assertFalse(rateLimitHeader.get$ref().contains("RateLimitHeader.yaml"), + "X-Rate-Limit $ref should not contain file path, was: " + rateLimitHeader.get$ref()); + + String headerName = rateLimitHeader.get$ref().substring(HEADER_PREFIX.length()); + assertNotNull(openAPI.getComponents().getHeaders().get(headerName), + "RateLimitHeader should be in components/headers"); + } + + private OpenAPI parse(String location) { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + + SwaggerParseResult result = new OpenAPIV3Parser().readLocation(location, null, options); + assertNotNull(result); + assertNotNull(result.getOpenAPI()); + return result.getOpenAPI(); + } + + private Schema getProductItems(OpenAPI openAPI) { + Schema response = openAPI.getComponents().getSchemas().get("GetProductResponse"); + assertNotNull(response); + Schema products = (Schema) response.getProperties().get("products"); + assertTrue(products instanceof ArraySchema); + return ((ArraySchema) products).getItems(); + } + + private ApiResponse getProductsResponse(OpenAPI openAPI) { + ApiResponse pathResponse = openAPI.getPaths().get("/products").getGet().getResponses().get("200"); + assertNotNull(pathResponse); + assertNotNull(pathResponse.get$ref()); + String responsePrefix = "#/components/responses/"; + assertTrue(pathResponse.get$ref().startsWith(responsePrefix), + "path response $ref should be internal, was: " + pathResponse.get$ref()); + + String responseName = pathResponse.get$ref().substring(responsePrefix.length()); + ApiResponse response = openAPI.getComponents().getResponses().get(responseName); + assertNotNull(response, "response should be in components/responses"); + return response; + } + + private String assertInternalReference(Schema items) { + assertNotNull(items); + assertNotNull(items.get$ref()); + assertTrue(items.get$ref().startsWith(SCHEMA_PREFIX)); + assertFalse(items.get$ref().contains("Product.yaml")); + return items.get$ref().substring(SCHEMA_PREFIX.length()); + } +} diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/headers/RateLimitHeader.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/headers/RateLimitHeader.yaml new file mode 100644 index 0000000000..b80f2d3d83 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/headers/RateLimitHeader.yaml @@ -0,0 +1,3 @@ +description: Rate limit header +schema: + type: integer \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponse.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponse.yaml new file mode 100644 index 0000000000..747c396902 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponse.yaml @@ -0,0 +1,6 @@ +type: object +properties: + products: + type: array + items: + $ref: Product.yaml diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponseChain.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponseChain.yaml new file mode 100644 index 0000000000..66bd9fc6e4 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponseChain.yaml @@ -0,0 +1,6 @@ +type: object +properties: + products: + type: array + items: + $ref: ProductChain.yaml \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponseFragmentRef.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponseFragmentRef.yaml new file mode 100644 index 0000000000..7540827060 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/GetProductResponseFragmentRef.yaml @@ -0,0 +1,6 @@ +type: object +properties: + products: + type: array + items: + $ref: './schemas.yaml#/Product' \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/Item.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/Item.yaml new file mode 100644 index 0000000000..9ec46dc836 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/Item.yaml @@ -0,0 +1,4 @@ +type: object +properties: + id: + type: string \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/Product.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/Product.yaml new file mode 100644 index 0000000000..11e2b4b86a --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/Product.yaml @@ -0,0 +1,4 @@ +type: object +properties: + id: + type: string diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/ProductChain.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/ProductChain.yaml new file mode 100644 index 0000000000..63521d08ea --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/ProductChain.yaml @@ -0,0 +1,6 @@ +type: object +properties: + items: + type: array + items: + $ref: Item.yaml \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/schemas.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/schemas.yaml new file mode 100644 index 0000000000..e3df3c6a89 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/models/schemas.yaml @@ -0,0 +1,5 @@ +Product: + type: object + properties: + id: + type: string \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/common/responses/ProductsResponse.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/responses/ProductsResponse.yaml new file mode 100644 index 0000000000..31e4bf2ce6 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/common/responses/ProductsResponse.yaml @@ -0,0 +1,8 @@ +description: Products response +headers: + X-Rate-Limit: + $ref: '../headers/RateLimitHeader.yaml' +content: + application/json: + schema: + type: object \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/root-chain.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-chain.yaml new file mode 100644 index 0000000000..aa58716d5a --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-chain.yaml @@ -0,0 +1,9 @@ +openapi: 3.0.0 +info: + title: test + version: 1.0.0 + +components: + schemas: + GetProductResponse: + $ref: './common/models/GetProductResponseChain.yaml' \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/root-fragment-ref.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-fragment-ref.yaml new file mode 100644 index 0000000000..d59d64f9f3 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-fragment-ref.yaml @@ -0,0 +1,9 @@ +openapi: 3.0.0 +info: + title: test + version: 1.0.0 + +components: + schemas: + GetProductResponse: + $ref: './common/models/GetProductResponseFragmentRef.yaml' \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/root-header.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-header.yaml new file mode 100644 index 0000000000..3ca08e5fab --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-header.yaml @@ -0,0 +1,11 @@ +openapi: 3.0.0 +info: + title: test + version: 1.0.0 + +paths: + /products: + get: + responses: + '200': + $ref: './common/responses/ProductsResponse.yaml' \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/root-with-product.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-with-product.yaml new file mode 100644 index 0000000000..95d7dc369d --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/root-with-product.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.0 +info: + title: test + version: 1.0.0 + +components: + schemas: + Product: + type: object + properties: + rootOnly: + type: string + GetProductResponse: + $ref: './common/models/GetProductResponse.yaml' diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1889/root.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1889/root.yaml new file mode 100644 index 0000000000..576fb7dcaf --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1889/root.yaml @@ -0,0 +1,9 @@ +openapi: 3.0.0 +info: + title: test + version: 1.0.0 + +components: + schemas: + GetProductResponse: + $ref: './common/models/GetProductResponse.yaml'