Skip to content

Commit 7dd94c5

Browse files
committed
fix: Fix nested whole document external refs not rewritten to internal component refs (#1889)
1 parent f605c3c commit 7dd94c5

16 files changed

Lines changed: 291 additions & 0 deletions

File tree

modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/ExternalRefProcessor.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.Map;
1111
import java.util.Objects;
1212
import java.util.Optional;
13+
import java.util.function.BiFunction;
14+
import java.util.function.Consumer;
1315
import java.util.function.Function;
1416
import java.util.stream.Collectors;
1517
import java.util.stream.Stream;
@@ -892,6 +894,8 @@ private void processRefExample(Example example, String externalFile) {
892894
if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) {
893895
$ref = join(externalFile, example.get$ref());
894896
example.set$ref($ref);
897+
resolveWholeDocumentRef($ref, this::processRefToExternalExample, example::set$ref,
898+
RefType.COMPONENTS.getInternalPrefix() + "examples/");
895899
}else {
896900
processRefToExternalExample($ref, format);
897901
}
@@ -955,6 +959,8 @@ private void processRefSchema(Schema subRef, String externalFile) {
955959
if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) {
956960
$ref = constructRef(subRef, externalFile);
957961
subRef.set$ref($ref);
962+
resolveWholeDocumentRef($ref, this::processRefToExternalSchema, subRef::set$ref,
963+
RefType.SCHEMAS.getInternalPrefix());
958964
}else {
959965
processRefToExternalSchema($ref, format);
960966
}
@@ -980,6 +986,8 @@ private void processRefHeader(Header subRef, String externalFile) {
980986
if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) {
981987
$ref = join(externalFile, subRef.get$ref());
982988
subRef.set$ref($ref);
989+
resolveWholeDocumentRef($ref, this::processRefToExternalHeader, subRef::set$ref,
990+
RefType.COMPONENTS.getInternalPrefix() + "headers/");
983991
}else {
984992
processRefToExternalHeader($ref, format);
985993
}
@@ -999,12 +1007,32 @@ private void processRefLink(Link subRef, String externalFile) {
9991007
if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) {
10001008
$ref = join(externalFile, subRef.get$ref());
10011009
subRef.set$ref($ref);
1010+
resolveWholeDocumentRef($ref, this::processRefToExternalLink, subRef::set$ref,
1011+
RefType.COMPONENTS.getInternalPrefix() + "links/");
10021012
}else {
10031013
processRefToExternalLink($ref, format);
10041014
}
10051015
}
10061016

10071017

1018+
private static boolean isWholeDocumentRef(String ref) {
1019+
return !ref.contains("#");
1020+
}
1021+
1022+
private void resolveWholeDocumentRef(String ref, BiFunction<String, RefFormat, String> processor,
1023+
Consumer<String> setRef, String internalPrefix) {
1024+
if (!isWholeDocumentRef(ref)) {
1025+
return;
1026+
}
1027+
1028+
RefFormat format = computeRefFormat(ref);
1029+
processor.apply(ref, format);
1030+
String renamedRef = cache.getRenamedRef(ref);
1031+
if (renamedRef != null) {
1032+
setRef.accept(internalPrefix + renamedRef);
1033+
}
1034+
}
1035+
10081036
// visible for testing
10091037
public static String join(String source, String fragment) {
10101038
try {
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package io.swagger.v3.parser.test;
2+
3+
import io.swagger.v3.oas.models.OpenAPI;
4+
import io.swagger.v3.oas.models.media.ArraySchema;
5+
import io.swagger.v3.oas.models.media.Schema;
6+
import io.swagger.v3.oas.models.responses.ApiResponse;
7+
import io.swagger.v3.parser.OpenAPIV3Parser;
8+
import io.swagger.v3.parser.core.models.ParseOptions;
9+
import io.swagger.v3.parser.core.models.SwaggerParseResult;
10+
import org.testng.annotations.Test;
11+
12+
import static org.testng.Assert.assertFalse;
13+
import static org.testng.Assert.assertNotNull;
14+
import static org.testng.Assert.assertNull;
15+
import static org.testng.Assert.assertTrue;
16+
17+
public class Issue1889Test {
18+
19+
private static final String SCHEMA_PREFIX = "#/components/schemas/";
20+
private static final String HEADER_PREFIX = "#/components/headers/";
21+
22+
@Test
23+
public void resolvesNestedRelativeArrayItemReference() {
24+
OpenAPI openAPI = parse("issue-1889/root.yaml");
25+
26+
Schema items = getProductItems(openAPI);
27+
String resolvedName = assertInternalReference(items);
28+
29+
assertNotNull(openAPI.getComponents().getSchemas().get(resolvedName));
30+
}
31+
32+
@Test
33+
public void resolvesNestedRelativeArrayItemReferenceWithoutOverwritingExistingSchema() {
34+
OpenAPI openAPI = parse("issue-1889/root-with-product.yaml");
35+
36+
Schema rootProduct = openAPI.getComponents().getSchemas().get("Product");
37+
assertNotNull(rootProduct);
38+
assertNotNull(rootProduct.getProperties().get("rootOnly"));
39+
40+
Schema items = getProductItems(openAPI);
41+
String resolvedName = assertInternalReference(items);
42+
Schema resolvedProduct = openAPI.getComponents().getSchemas().get(resolvedName);
43+
44+
assertNotNull(resolvedProduct);
45+
assertNotNull(resolvedProduct.getProperties().get("id"));
46+
assertNull(resolvedProduct.getProperties().get("rootOnly"));
47+
}
48+
49+
@Test
50+
public void resolvesNestedRelativeArrayItemReferenceWithFragment() {
51+
OpenAPI openAPI = parse("issue-1889/root-fragment-ref.yaml");
52+
53+
Schema response = openAPI.getComponents().getSchemas().get("GetProductResponse");
54+
assertNotNull(response);
55+
Schema products = (Schema) response.getProperties().get("products");
56+
assertTrue(products instanceof ArraySchema);
57+
Schema items = ((ArraySchema) products).getItems();
58+
59+
assertNotNull(items);
60+
assertNotNull(items.get$ref());
61+
assertTrue(items.get$ref().startsWith(SCHEMA_PREFIX),
62+
"items.$ref should be internal ref, was: " + items.get$ref());
63+
assertFalse(items.get$ref().contains("schemas.yaml"),
64+
"items.$ref should not contain file path, was: " + items.get$ref());
65+
66+
String resolvedName = items.get$ref().substring(SCHEMA_PREFIX.length());
67+
assertNotNull(openAPI.getComponents().getSchemas().get(resolvedName));
68+
}
69+
70+
@Test
71+
public void resolvesThreeLevelChainOfNestedRelativeFileReferences() {
72+
OpenAPI openAPI = parse("issue-1889/root-chain.yaml");
73+
74+
Schema response = openAPI.getComponents().getSchemas().get("GetProductResponse");
75+
assertNotNull(response);
76+
Schema products = (Schema) response.getProperties().get("products");
77+
assertTrue(products instanceof ArraySchema);
78+
Schema productItems = ((ArraySchema) products).getItems();
79+
80+
assertNotNull(productItems);
81+
assertNotNull(productItems.get$ref());
82+
assertTrue(productItems.get$ref().startsWith(SCHEMA_PREFIX),
83+
"productItems.$ref should be internal ref, was: " + productItems.get$ref());
84+
85+
String productName = productItems.get$ref().substring(SCHEMA_PREFIX.length());
86+
Schema productSchema = openAPI.getComponents().getSchemas().get(productName);
87+
assertNotNull(productSchema, "ProductChain schema should be in components");
88+
89+
Schema nestedItems = (Schema) productSchema.getProperties().get("items");
90+
assertNotNull(nestedItems);
91+
assertTrue(nestedItems instanceof ArraySchema);
92+
Schema itemRef = ((ArraySchema) nestedItems).getItems();
93+
94+
assertNotNull(itemRef);
95+
assertNotNull(itemRef.get$ref());
96+
assertTrue(itemRef.get$ref().startsWith(SCHEMA_PREFIX),
97+
"itemRef.$ref should be internal ref, was: " + itemRef.get$ref());
98+
assertFalse(itemRef.get$ref().contains("Item.yaml"),
99+
"itemRef.$ref should not contain file path, was: " + itemRef.get$ref());
100+
101+
String itemName = itemRef.get$ref().substring(SCHEMA_PREFIX.length());
102+
assertNotNull(openAPI.getComponents().getSchemas().get(itemName), "Item schema should be in components");
103+
}
104+
105+
@Test
106+
public void resolvesNestedRelativeHeaderReferenceFromExternalResponse() {
107+
OpenAPI openAPI = parse("issue-1889/root-header.yaml");
108+
ApiResponse response = getProductsResponse(openAPI);
109+
assertNotNull(response.getHeaders(), "response should have headers");
110+
111+
io.swagger.v3.oas.models.headers.Header rateLimitHeader = response.getHeaders().get("X-Rate-Limit");
112+
assertNotNull(rateLimitHeader);
113+
assertNotNull(rateLimitHeader.get$ref());
114+
assertTrue(rateLimitHeader.get$ref().startsWith(HEADER_PREFIX),
115+
"X-Rate-Limit $ref should be internal header ref, was: " + rateLimitHeader.get$ref());
116+
assertFalse(rateLimitHeader.get$ref().contains("RateLimitHeader.yaml"),
117+
"X-Rate-Limit $ref should not contain file path, was: " + rateLimitHeader.get$ref());
118+
119+
String headerName = rateLimitHeader.get$ref().substring(HEADER_PREFIX.length());
120+
assertNotNull(openAPI.getComponents().getHeaders().get(headerName),
121+
"RateLimitHeader should be in components/headers");
122+
}
123+
124+
private OpenAPI parse(String location) {
125+
ParseOptions options = new ParseOptions();
126+
options.setResolve(true);
127+
128+
SwaggerParseResult result = new OpenAPIV3Parser().readLocation(location, null, options);
129+
assertNotNull(result);
130+
assertNotNull(result.getOpenAPI());
131+
return result.getOpenAPI();
132+
}
133+
134+
private Schema getProductItems(OpenAPI openAPI) {
135+
Schema response = openAPI.getComponents().getSchemas().get("GetProductResponse");
136+
assertNotNull(response);
137+
Schema products = (Schema) response.getProperties().get("products");
138+
assertTrue(products instanceof ArraySchema);
139+
return ((ArraySchema) products).getItems();
140+
}
141+
142+
private ApiResponse getProductsResponse(OpenAPI openAPI) {
143+
ApiResponse pathResponse = openAPI.getPaths().get("/products").getGet().getResponses().get("200");
144+
assertNotNull(pathResponse);
145+
assertNotNull(pathResponse.get$ref());
146+
String responsePrefix = "#/components/responses/";
147+
assertTrue(pathResponse.get$ref().startsWith(responsePrefix),
148+
"path response $ref should be internal, was: " + pathResponse.get$ref());
149+
150+
String responseName = pathResponse.get$ref().substring(responsePrefix.length());
151+
ApiResponse response = openAPI.getComponents().getResponses().get(responseName);
152+
assertNotNull(response, "response should be in components/responses");
153+
return response;
154+
}
155+
156+
private String assertInternalReference(Schema items) {
157+
assertNotNull(items);
158+
assertNotNull(items.get$ref());
159+
assertTrue(items.get$ref().startsWith(SCHEMA_PREFIX));
160+
assertFalse(items.get$ref().contains("Product.yaml"));
161+
return items.get$ref().substring(SCHEMA_PREFIX.length());
162+
}
163+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
description: Rate limit header
2+
schema:
3+
type: integer
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: object
2+
properties:
3+
products:
4+
type: array
5+
items:
6+
$ref: Product.yaml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: object
2+
properties:
3+
products:
4+
type: array
5+
items:
6+
$ref: ProductChain.yaml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: object
2+
properties:
3+
products:
4+
type: array
5+
items:
6+
$ref: './schemas.yaml#/Product'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type: object
2+
properties:
3+
id:
4+
type: string
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type: object
2+
properties:
3+
id:
4+
type: string
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: object
2+
properties:
3+
items:
4+
type: array
5+
items:
6+
$ref: Item.yaml
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Product:
2+
type: object
3+
properties:
4+
id:
5+
type: string

0 commit comments

Comments
 (0)