Skip to content

Commit

Permalink
Merge pull request #79 from eclipse-vertx/issues/recursive-resolve
Browse files Browse the repository at this point in the history
Ensure that when we resolve a schema, and there are recursive refs, w…
  • Loading branch information
pmlopes authored Dec 1, 2022
2 parents 87ae9ef + b967c25 commit cce9a61
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 26 deletions.
70 changes: 44 additions & 26 deletions src/main/java/io/vertx/json/schema/impl/Ref.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> POINTER_KEYWORD = Arrays.asList(
"$ref",
"$id",
Expand All @@ -37,6 +39,13 @@ public final class Ref {
}

public static JsonObject resolve(Map<String, JsonSchema> refs, URL baseUri, JsonSchema schema) {
return resolve(refs, baseUri, schema, RESOLVE_LIMIT);
}
private static JsonObject resolve(Map<String, JsonSchema> 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<String, List<Ref>> pointers = new HashMap<>();

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

final JsonObject dynamicAnchors = new JsonObject();

boolean updated = false;

pointers
.computeIfAbsent("$id", key -> Collections.emptyList())
.forEach(item -> {
Expand Down Expand Up @@ -90,31 +101,30 @@ public static JsonObject resolve(Map<String, JsonSchema> 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;
Expand All @@ -130,9 +140,17 @@ public static JsonObject resolve(Map<String, JsonSchema> 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<String, List<Ref>> pointers) {
Expand Down
14 changes: 14 additions & 0 deletions src/test/java/io/vertx/json/schema/ResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -122,4 +124,16 @@ 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();

assertThat(ref.matcher(json.encode()).find()).isFalse();
}
}
211 changes: 211 additions & 0 deletions src/test/resources/resolve/petstore.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}

0 comments on commit cce9a61

Please sign in to comment.