From 2d9fd77b0830f05f21bb5c2125ae9f2785586853 Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Wed, 30 Nov 2022 14:22:23 +0100 Subject: [PATCH 1/2] Ensure that when we resolve a schema, and there are recursive refs, we recurse to a given limit Signed-off-by: Paulo Lopes --- .../java/io/vertx/json/schema/impl/Ref.java | 70 +++--- .../io/vertx/json/schema/ResolverTest.java | 15 ++ src/test/resources/resolve/petstore.json | 211 ++++++++++++++++++ 3 files changed, 270 insertions(+), 26 deletions(-) create mode 100644 src/test/resources/resolve/petstore.json diff --git a/src/main/java/io/vertx/json/schema/impl/Ref.java b/src/main/java/io/vertx/json/schema/impl/Ref.java index c2ab45d9..c4ca47f0 100644 --- a/src/main/java/io/vertx/json/schema/impl/Ref.java +++ b/src/main/java/io/vertx/json/schema/impl/Ref.java @@ -13,6 +13,8 @@ public final class Ref { + private static final int RESOLVE_LIMIT = Integer.getInteger("io.vertx.json.schema.resolve.limit", 50); + public static final List POINTER_KEYWORD = Arrays.asList( "$ref", "$id", @@ -37,6 +39,13 @@ public final class Ref { } public static JsonObject resolve(Map refs, URL baseUri, JsonSchema schema) { + return resolve(refs, baseUri, schema, RESOLVE_LIMIT); + } + private static JsonObject resolve(Map refs, URL baseUri, JsonSchema schema, int limit) { + if (limit == 0) { + throw new RuntimeException("Too much recursion resolving schema"); + } + final JsonObject tree = ((JsonObject) schema).copy(); final Map> pointers = new HashMap<>(); @@ -49,6 +58,8 @@ public static JsonObject resolve(Map refs, URL baseUri, Json final JsonObject dynamicAnchors = new JsonObject(); + boolean updated = false; + pointers .computeIfAbsent("$id", key -> Collections.emptyList()) .forEach(item -> { @@ -90,31 +101,30 @@ public static JsonObject resolve(Map refs, URL baseUri, Json dynamicAnchors.put("#" + ref, obj); }); - pointers - .computeIfAbsent("$ref", key -> Collections.emptyList()) - .forEach(item -> { - final String ref = item.ref; - final String prop = item.prop; - final JsonObject obj = item.obj; - final String id = item.id; - - obj.remove(prop); - - final String decodedRef = decodeURIComponent(ref); - final String fullRef = decodedRef.charAt(0) != '#' ? decodedRef : id + decodedRef; - // re-assign the obj - obj.mergeIn( - new JsonObject( - resolveUri(refs, baseUri, schema, fullRef, anchors) - // filter out pointer keywords - .stream() - .filter(kv -> !POINTER_KEYWORD.contains(kv.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); - }); + for (Ref item : pointers.computeIfAbsent("$ref", key -> Collections.emptyList())) { + final String ref = item.ref; + final String prop = item.prop; + final JsonObject obj = item.obj; + final String id = item.id; + + obj.remove(prop); + + final String decodedRef = decodeURIComponent(ref); + final String fullRef = decodedRef.charAt(0) != '#' ? decodedRef : id + decodedRef; + // re-assign the obj + obj.mergeIn( + new JsonObject( + resolveUri(refs, baseUri, schema, fullRef, anchors) + // filter out pointer keywords + .stream() + .filter(kv -> !POINTER_KEYWORD.contains(kv.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + + // the underlying schema was updated + updated = true; + } - pointers - .computeIfAbsent("$dynamicRef", key -> Collections.emptyList()) - .forEach(item -> { + for (Ref item : pointers.computeIfAbsent("$dynamicRef", key -> Collections.emptyList())) { final String ref = item.ref; final String prop = item.prop; final JsonObject obj = item.obj; @@ -130,9 +140,17 @@ public static JsonObject resolve(Map refs, URL baseUri, Json .stream() .filter(kv -> !POINTER_KEYWORD.contains(kv.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); - }); - return tree; + // the underlying schema was updated + updated = true; + } + + if (updated) { + // the schema changed we need to re-run + return resolve(refs, baseUri, JsonSchema.of(tree), limit - 1); + } else { + return tree; + } } private static void findRefsAndClean(Object obj, String path, String id, Map> pointers) { diff --git a/src/test/java/io/vertx/json/schema/ResolverTest.java b/src/test/java/io/vertx/json/schema/ResolverTest.java index 30ca2b85..1fb2287b 100644 --- a/src/test/java/io/vertx/json/schema/ResolverTest.java +++ b/src/test/java/io/vertx/json/schema/ResolverTest.java @@ -2,6 +2,7 @@ import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; import org.junit.jupiter.api.Test; @@ -11,6 +12,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; +import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -122,4 +124,17 @@ public void testResolveRefsWithinArray(Vertx vertx) { assertThat(json.getJsonArray("parameters").getValue(0)) .isInstanceOf(JsonObject.class); } + + @Test + public void testResolveShouldHaveNoRefReferences(Vertx vertx) { + + Buffer source = vertx.fileSystem().readFileBlocking("resolve/petstore.json"); + Pattern ref = Pattern.compile("\\$ref", Pattern.MULTILINE); + assertThat(ref.matcher(source.toString()).find()).isTrue(); + + JsonObject json = JsonSchema.of(new JsonObject(source)).resolve(); + + System.out.println(json.encodePrettily()); + assertThat(ref.matcher(json.encode()).find()).isFalse(); + } } diff --git a/src/test/resources/resolve/petstore.json b/src/test/resources/resolve/petstore.json new file mode 100644 index 00000000..6e407e24 --- /dev/null +++ b/src/test/resources/resolve/petstore.json @@ -0,0 +1,211 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "identifier": "MIT", + "name": "MIT License" + } + }, + "servers": [ + { + "url": "https://petstore.swagger.io/v1" + } + ], + "security": [ + { + "BasicAuth": [] + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "responses": { + "201": { + "description": "Null response" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Info for a specific pet", + "operationId": "showPetById", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to retrieve", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "Error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "BasicAuth": { + "scheme": "basic", + "type": "http" + } + } + } +} From b967c2588101808db09804fc50e7b0c693ad5219 Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Wed, 30 Nov 2022 14:37:05 +0100 Subject: [PATCH 2/2] Removing logging Signed-off-by: Paulo Lopes --- src/test/java/io/vertx/json/schema/ResolverTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/io/vertx/json/schema/ResolverTest.java b/src/test/java/io/vertx/json/schema/ResolverTest.java index 1fb2287b..05da185d 100644 --- a/src/test/java/io/vertx/json/schema/ResolverTest.java +++ b/src/test/java/io/vertx/json/schema/ResolverTest.java @@ -134,7 +134,6 @@ public void testResolveShouldHaveNoRefReferences(Vertx vertx) { JsonObject json = JsonSchema.of(new JsonObject(source)).resolve(); - System.out.println(json.encodePrettily()); assertThat(ref.matcher(json.encode()).find()).isFalse(); } }