Skip to content

Commit cce9a61

Browse files
authored
Merge pull request #79 from eclipse-vertx/issues/recursive-resolve
Ensure that when we resolve a schema, and there are recursive refs, w…
2 parents 87ae9ef + b967c25 commit cce9a61

File tree

3 files changed

+269
-26
lines changed

3 files changed

+269
-26
lines changed

src/main/java/io/vertx/json/schema/impl/Ref.java

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
public final class Ref {
1515

16+
private static final int RESOLVE_LIMIT = Integer.getInteger("io.vertx.json.schema.resolve.limit", 50);
17+
1618
public static final List<String> POINTER_KEYWORD = Arrays.asList(
1719
"$ref",
1820
"$id",
@@ -37,6 +39,13 @@ public final class Ref {
3739
}
3840

3941
public static JsonObject resolve(Map<String, JsonSchema> refs, URL baseUri, JsonSchema schema) {
42+
return resolve(refs, baseUri, schema, RESOLVE_LIMIT);
43+
}
44+
private static JsonObject resolve(Map<String, JsonSchema> refs, URL baseUri, JsonSchema schema, int limit) {
45+
if (limit == 0) {
46+
throw new RuntimeException("Too much recursion resolving schema");
47+
}
48+
4049
final JsonObject tree = ((JsonObject) schema).copy();
4150
final Map<String, List<Ref>> pointers = new HashMap<>();
4251

@@ -49,6 +58,8 @@ public static JsonObject resolve(Map<String, JsonSchema> refs, URL baseUri, Json
4958

5059
final JsonObject dynamicAnchors = new JsonObject();
5160

61+
boolean updated = false;
62+
5263
pointers
5364
.computeIfAbsent("$id", key -> Collections.emptyList())
5465
.forEach(item -> {
@@ -90,31 +101,30 @@ public static JsonObject resolve(Map<String, JsonSchema> refs, URL baseUri, Json
90101
dynamicAnchors.put("#" + ref, obj);
91102
});
92103

93-
pointers
94-
.computeIfAbsent("$ref", key -> Collections.emptyList())
95-
.forEach(item -> {
96-
final String ref = item.ref;
97-
final String prop = item.prop;
98-
final JsonObject obj = item.obj;
99-
final String id = item.id;
100-
101-
obj.remove(prop);
102-
103-
final String decodedRef = decodeURIComponent(ref);
104-
final String fullRef = decodedRef.charAt(0) != '#' ? decodedRef : id + decodedRef;
105-
// re-assign the obj
106-
obj.mergeIn(
107-
new JsonObject(
108-
resolveUri(refs, baseUri, schema, fullRef, anchors)
109-
// filter out pointer keywords
110-
.stream()
111-
.filter(kv -> !POINTER_KEYWORD.contains(kv.getKey()))
112-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
113-
});
104+
for (Ref item : pointers.computeIfAbsent("$ref", key -> Collections.emptyList())) {
105+
final String ref = item.ref;
106+
final String prop = item.prop;
107+
final JsonObject obj = item.obj;
108+
final String id = item.id;
109+
110+
obj.remove(prop);
111+
112+
final String decodedRef = decodeURIComponent(ref);
113+
final String fullRef = decodedRef.charAt(0) != '#' ? decodedRef : id + decodedRef;
114+
// re-assign the obj
115+
obj.mergeIn(
116+
new JsonObject(
117+
resolveUri(refs, baseUri, schema, fullRef, anchors)
118+
// filter out pointer keywords
119+
.stream()
120+
.filter(kv -> !POINTER_KEYWORD.contains(kv.getKey()))
121+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
122+
123+
// the underlying schema was updated
124+
updated = true;
125+
}
114126

115-
pointers
116-
.computeIfAbsent("$dynamicRef", key -> Collections.emptyList())
117-
.forEach(item -> {
127+
for (Ref item : pointers.computeIfAbsent("$dynamicRef", key -> Collections.emptyList())) {
118128
final String ref = item.ref;
119129
final String prop = item.prop;
120130
final JsonObject obj = item.obj;
@@ -130,9 +140,17 @@ public static JsonObject resolve(Map<String, JsonSchema> refs, URL baseUri, Json
130140
.stream()
131141
.filter(kv -> !POINTER_KEYWORD.contains(kv.getKey()))
132142
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
133-
});
134143

135-
return tree;
144+
// the underlying schema was updated
145+
updated = true;
146+
}
147+
148+
if (updated) {
149+
// the schema changed we need to re-run
150+
return resolve(refs, baseUri, JsonSchema.of(tree), limit - 1);
151+
} else {
152+
return tree;
153+
}
136154
}
137155

138156
private static void findRefsAndClean(Object obj, String path, String id, Map<String, List<Ref>> pointers) {

src/test/java/io/vertx/json/schema/ResolverTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.vertx.core.Vertx;
44
import io.vertx.core.buffer.Buffer;
5+
import io.vertx.core.json.Json;
56
import io.vertx.core.json.JsonObject;
67
import io.vertx.junit5.VertxExtension;
78
import org.junit.jupiter.api.Test;
@@ -11,6 +12,7 @@
1112
import java.nio.file.Files;
1213
import java.nio.file.Paths;
1314
import java.util.Arrays;
15+
import java.util.regex.Pattern;
1416

1517
import static org.assertj.core.api.Assertions.assertThat;
1618
import static org.assertj.core.api.Assertions.fail;
@@ -122,4 +124,16 @@ public void testResolveRefsWithinArray(Vertx vertx) {
122124
assertThat(json.getJsonArray("parameters").getValue(0))
123125
.isInstanceOf(JsonObject.class);
124126
}
127+
128+
@Test
129+
public void testResolveShouldHaveNoRefReferences(Vertx vertx) {
130+
131+
Buffer source = vertx.fileSystem().readFileBlocking("resolve/petstore.json");
132+
Pattern ref = Pattern.compile("\\$ref", Pattern.MULTILINE);
133+
assertThat(ref.matcher(source.toString()).find()).isTrue();
134+
135+
JsonObject json = JsonSchema.of(new JsonObject(source)).resolve();
136+
137+
assertThat(ref.matcher(json.encode()).find()).isFalse();
138+
}
125139
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"version": "1.0.0",
5+
"title": "Swagger Petstore",
6+
"license": {
7+
"identifier": "MIT",
8+
"name": "MIT License"
9+
}
10+
},
11+
"servers": [
12+
{
13+
"url": "https://petstore.swagger.io/v1"
14+
}
15+
],
16+
"security": [
17+
{
18+
"BasicAuth": []
19+
}
20+
],
21+
"paths": {
22+
"/pets": {
23+
"get": {
24+
"summary": "List all pets",
25+
"operationId": "listPets",
26+
"tags": [
27+
"pets"
28+
],
29+
"parameters": [
30+
{
31+
"name": "limit",
32+
"in": "query",
33+
"description": "How many items to return at one time (max 100)",
34+
"required": false,
35+
"schema": {
36+
"type": "integer",
37+
"maximum": 100,
38+
"format": "int32"
39+
}
40+
}
41+
],
42+
"responses": {
43+
"200": {
44+
"description": "A paged array of pets",
45+
"headers": {
46+
"x-next": {
47+
"description": "A link to the next page of responses",
48+
"schema": {
49+
"type": "string"
50+
}
51+
}
52+
},
53+
"content": {
54+
"application/json": {
55+
"schema": {
56+
"$ref": "#/components/schemas/Pets"
57+
}
58+
}
59+
}
60+
},
61+
"default": {
62+
"description": "unexpected error",
63+
"content": {
64+
"application/json": {
65+
"schema": {
66+
"$ref": "#/components/schemas/Error"
67+
}
68+
}
69+
}
70+
}
71+
}
72+
},
73+
"post": {
74+
"summary": "Create a pet",
75+
"operationId": "createPets",
76+
"tags": [
77+
"pets"
78+
],
79+
"responses": {
80+
"201": {
81+
"description": "Null response"
82+
},
83+
"400": {
84+
"description": "Bad Request",
85+
"content": {
86+
"application/json": {
87+
"schema": {
88+
"$ref": "#/components/schemas/Error"
89+
}
90+
}
91+
}
92+
},
93+
"default": {
94+
"description": "unexpected error",
95+
"content": {
96+
"application/json": {
97+
"schema": {
98+
"$ref": "#/components/schemas/Error"
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
105+
},
106+
"/pets/{petId}": {
107+
"get": {
108+
"summary": "Info for a specific pet",
109+
"operationId": "showPetById",
110+
"tags": [
111+
"pets"
112+
],
113+
"parameters": [
114+
{
115+
"name": "petId",
116+
"in": "path",
117+
"required": true,
118+
"description": "The id of the pet to retrieve",
119+
"schema": {
120+
"type": "string"
121+
}
122+
}
123+
],
124+
"responses": {
125+
"200": {
126+
"description": "Expected response to a valid request",
127+
"content": {
128+
"application/json": {
129+
"schema": {
130+
"$ref": "#/components/schemas/Pet"
131+
}
132+
}
133+
}
134+
},
135+
"400": {
136+
"description": "Bad Request",
137+
"content": {
138+
"application/json": {
139+
"schema": {
140+
"$ref": "#/components/schemas/Error"
141+
}
142+
}
143+
}
144+
},
145+
"default": {
146+
"description": "unexpected error",
147+
"content": {
148+
"application/json": {
149+
"schema": {
150+
"$ref": "#/components/schemas/Error"
151+
}
152+
}
153+
}
154+
}
155+
}
156+
}
157+
}
158+
},
159+
"components": {
160+
"schemas": {
161+
"Pet": {
162+
"type": "object",
163+
"required": [
164+
"id",
165+
"name"
166+
],
167+
"properties": {
168+
"id": {
169+
"type": "integer",
170+
"format": "int64"
171+
},
172+
"name": {
173+
"type": "string"
174+
},
175+
"tag": {
176+
"type": "string"
177+
}
178+
}
179+
},
180+
"Pets": {
181+
"type": "array",
182+
"maxItems": 100,
183+
"items": {
184+
"$ref": "#/components/schemas/Pet"
185+
}
186+
},
187+
"Error": {
188+
"type": "object",
189+
"required": [
190+
"code",
191+
"message"
192+
],
193+
"properties": {
194+
"code": {
195+
"type": "integer",
196+
"format": "int32"
197+
},
198+
"message": {
199+
"type": "string"
200+
}
201+
}
202+
}
203+
},
204+
"securitySchemes": {
205+
"BasicAuth": {
206+
"scheme": "basic",
207+
"type": "http"
208+
}
209+
}
210+
}
211+
}

0 commit comments

Comments
 (0)