Skip to content

Commit

Permalink
DGS-9753 FIx local refs for JSON Schema 2020-12
Browse files Browse the repository at this point in the history
  • Loading branch information
rayokota committed Jan 13, 2024
1 parent 706895c commit e07cbfa
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 56 deletions.
Expand Up @@ -53,6 +53,7 @@
import com.github.erosb.jsonsKema.ValidationFailure;
import com.github.erosb.jsonsKema.Validator;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import io.confluent.kafka.schemaregistry.ParsedSchema;
import io.confluent.kafka.schemaregistry.client.rest.entities.Metadata;
import io.confluent.kafka.schemaregistry.client.rest.entities.RuleSet;
Expand All @@ -70,7 +71,6 @@
import io.confluent.kafka.schemaregistry.utils.BoundedConcurrentHashMap;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
Expand All @@ -79,6 +79,8 @@
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -348,22 +350,24 @@ public Schema rawSchema() {
}

private void loadLatestDraft() throws URISyntaxException {
URI baseUri = new URI(DEFAULT_BASE_URI);
URI idUri = null;
if (jsonNode.has("$id")) {
String id = jsonNode.get("$id").asText();
if (id != null) {
idUri = ReferenceResolver.resolve((URI) null, id);
idUri = ReferenceResolver.resolve(baseUri, id);
}
} else {
idUri = new URI(DEFAULT_BASE_URI);
idUri = baseUri;
}
Map<URI, String> references = new HashMap<>();
for (Map.Entry<String, String> dep : resolvedReferences.entrySet()) {
URI child = ReferenceResolver.resolve(idUri, dep.getKey());
references.put(child, dep.getValue());
}
SchemaLoaderConfig config = new SchemaLoaderConfig(
new ReferenceSchemaClient(references), DEFAULT_BASE_URI);
new MemoizingSchemaClient(
new ReferenceSchemaClient(references, new DefaultSchemaClient())), DEFAULT_BASE_URI);

JsonValue schemaJson = objectMapper.convertValue(jsonNode, JsonObject.class);
skemaObj = new com.github.erosb.jsonsKema.SchemaLoader(schemaJson, config).load();
Expand Down Expand Up @@ -531,17 +535,17 @@ public static JsonNode validate(com.github.erosb.jsonsKema.Schema schema, JsonNo
Validator validator = Validator.forSchema(schema);
JsonValue primitiveValue = null;
if (value instanceof BinaryNode) {
primitiveValue = new JsonString(((BinaryNode) value).asText(), UnknownSource.INSTANCE);
primitiveValue = new JsonString(value.asText(), UnknownSource.INSTANCE);
} else if (value instanceof BooleanNode) {
primitiveValue = new JsonBoolean(((BooleanNode) value).asBoolean(), UnknownSource.INSTANCE);
primitiveValue = new JsonBoolean(value.asBoolean(), UnknownSource.INSTANCE);
} else if (value instanceof NullNode) {
primitiveValue = new JsonNull(UnknownSource.INSTANCE);
} else if (value instanceof NumericNode) {
primitiveValue = new JsonNumber(((NumericNode) value).numberValue(), UnknownSource.INSTANCE);
primitiveValue = new JsonNumber(value.numberValue(), UnknownSource.INSTANCE);
} else if (value instanceof TextNode) {
primitiveValue = new JsonString(((TextNode) value).asText(), UnknownSource.INSTANCE);
primitiveValue = new JsonString(value.asText(), UnknownSource.INSTANCE);
}
ValidationFailure failure = null;
ValidationFailure failure;
if (primitiveValue != null) {
failure = validator.validate(primitiveValue);
} else {
Expand Down Expand Up @@ -1051,31 +1055,87 @@ private void modifySchemaTags(JsonNode node,
}
}

public static class ReferenceSchemaClient implements SchemaClient {
public static abstract class JsonSchemaClient implements SchemaClient {

@Override
public IJsonValue getParsed(URI uri) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(get(uri), StandardCharsets.UTF_8))) {
String string = reader.lines().collect(Collectors.joining());
return new JsonParser(string, uri).parse();
} catch (Exception ex) {
throw new SchemaLoadingException("failed to parse json content returned from $uri", ex);
}
}
}

public static class MemoizingSchemaClient extends JsonSchemaClient {

private Map<URI, String> references;
private final SchemaClient delegate;
private final Map<URI, byte[]> cache = new HashMap<>();

public ReferenceSchemaClient(Map<URI, String> references) {
public MemoizingSchemaClient(SchemaClient delegate) {
this.delegate = delegate;
}

@Override
public InputStream get(URI uri) {
byte[] bytes = cache.computeIfAbsent(uri, k -> {
try {
return ByteStreams.toByteArray(delegate.get(uri));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
return new ByteArrayInputStream(bytes);
}
}

public static class ReferenceSchemaClient extends JsonSchemaClient {

private final SchemaClient fallbackClient;

private final Map<URI, String> references;

public ReferenceSchemaClient(Map<URI, String> references, SchemaClient fallbackClient) {
this.fallbackClient = fallbackClient;
this.references = references;
}

@Override
public InputStream get(URI uri) {
String reference = references.get(uri);
if (reference == null) {
throw new UncheckedIOException(new FileNotFoundException(uri.toString()));
return fallbackClient.get(uri);
}
return new ByteArrayInputStream(reference.getBytes(StandardCharsets.UTF_8));
}
}

public static class DefaultSchemaClient extends JsonSchemaClient {

@Override
public IJsonValue getParsed(URI uri) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(get(uri), StandardCharsets.UTF_8))) {
String string = reader.lines().collect(Collectors.joining());
return new JsonParser(string, uri).parse();
} catch (Exception ex) {
throw new SchemaLoadingException("failed to parse json content returned from $uri", ex);
public InputStream get(URI uri) {
try {
URL u = toURL(uri);
URLConnection conn = u.openConnection();
String location = conn.getHeaderField("Location");
if (location != null) {
return get(new URI(location));
}
return (InputStream) conn.getContent();
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

private URL toURL(URI uri) throws IOException {
try {
return uri.toURL();
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("URI '$uri' can't be converted to URL: ${e.message}", e);
}
}
}
Expand Down
Expand Up @@ -921,50 +921,100 @@ public void testAddTagToCompositeField() {
@Test
public void testRestrictedFields() {
String schema = "{\n"
+ " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n"
+ " \"$id\": \"task.schema.json\",\n"
+ " \"title\": \"Task\",\n"
+ " \"description\": \"A task\",\n"
+ " \"type\": \"object\",\n"
+ " \"properties\": {\n"
+ " \"$id\": {\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"$$title\": {\n"
+ " \"description\": \"Task title\",\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"status\": {\n"
+ " \"type\": \"string\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n"
+ " \"$id\": \"task.schema.json\",\n"
+ " \"title\": \"Task\",\n"
+ " \"description\": \"A task\",\n"
+ " \"type\": \"object\",\n"
+ " \"properties\": {\n"
+ " \"$id\": {\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"$$title\": {\n"
+ " \"description\": \"Task title\",\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"status\": {\n"
+ " \"type\": \"string\"\n"
+ " }\n"
+ " }\n"
+ "}";
JsonSchema jsonSchema = new JsonSchema(schema);
jsonSchema.validate(false);
assertThrows(ValidationException.class, () -> jsonSchema.validate(true));
String stringSchema = "{\n"
+ " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n"
+ " \"$id\": \"task.schema.json\",\n"
+ " \"title\": \"Task\",\n"
+ " \"description\": \"A task\",\n"
+ " \"type\": \"object\",\n"
+ " \"properties\": {\n"
+ " \"$id\": {\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"title\": {\n"
+ " \"description\": \"Task title\",\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"status\": {\n"
+ " \"type\": \"string\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n"
+ " \"$id\": \"task.schema.json\",\n"
+ " \"title\": \"Task\",\n"
+ " \"description\": \"A task\",\n"
+ " \"type\": \"object\",\n"
+ " \"properties\": {\n"
+ " \"$id\": {\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"title\": {\n"
+ " \"description\": \"Task title\",\n"
+ " \"type\": \"string\"\n"
+ " }, \n"
+ " \"status\": {\n"
+ " \"type\": \"string\"\n"
+ " }\n"
+ " }\n"
+ "}";
JsonSchema validSchema = new JsonSchema(stringSchema);
validSchema.validate(true);
}

@Test
public void testDraft2020_12WithReference() {
String parent = "{\n"
+ " \"$id\": \"breadpayments.webhooks.checkout-application_updated.jsonschema.json\",\n"
+ " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n"
+ " \"$scope\": \"r:application\",\n"
+ " \"title\": \"ApplicationUpdatedEvent\",\n"
+ " \"description\": \"Application updated event representing a state change in application data.\",\n"
+ " \"type\": \"object\",\n"
+ " \"properties\": {\n"
+ " \"identity\": {\n"
+ " \"$ref\": \"https://getbread.github.io/docs/oas/v2/models.openapi3.json#/components/schemas/Identity\"\n"
+ " },\n"
+ " \"application\": {\n"
+ " \"$ref\": \"./checkout.common.webhooks.jsonschema.json#/components/schemas/Application\"\n"
+ " }\n"
+ " },\n"
+ " \"required\": [\n"
+ " \"identity\",\n"
+ " \"application\"\n"
+ " ]\n"
+ "}";
String child = "{\n"
+ " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n"
+ " \"components\": {\n"
+ " \"schemas\": {\n"
+ " \"Application\": {\n"
+ " \"properties\": {\n"
+ " \"id\": {\n"
+ " \"description\": \"The unique identifier of the Application.\",\n"
+ " \"format\": \"uuid\",\n"
+ " \"readOnly\": true,\n"
+ " \"type\": \"string\"\n"
+ " },\n"
+ " \"shippingContact\": {\n"
+ " \"$ref\": \"https://getbread.github.io/docs/oas/v2/models.openapi3.json#/components/schemas/Contact\"\n"
+ " }\n"
+ " },\n"
+ " \"title\": \"Application\",\n"
+ " \"type\": \"object\"\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}";
SchemaReference ref = new SchemaReference("checkout.common.webhooks.jsonschema.json", "reference", 1);
JsonSchema jsonSchema = new JsonSchema(parent, Collections.singletonList(ref),
Collections.singletonMap("checkout.common.webhooks.jsonschema.json", child), null);
jsonSchema.validate(true);
}

private static Map<String, String> getJsonSchemaWithReferences() {
Map<String, String> schemas = new HashMap<>();
String reference = "{\"type\":\"object\",\"additionalProperties\":false,\"definitions\":"
Expand Down

0 comments on commit e07cbfa

Please sign in to comment.