Skip to content

Commit

Permalink
Merge pull request #78 from eclipse-vertx/preload
Browse files Browse the repository at this point in the history
feat: allow to preload json meta schemas into the repository
  • Loading branch information
pmlopes authored Nov 26, 2022
2 parents bcc2474 + 7ac1f9f commit 87ae9ef
Show file tree
Hide file tree
Showing 9 changed files with 4,814 additions and 28 deletions.
50 changes: 31 additions & 19 deletions src/main/java/io/vertx/json/schema/Draft.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,43 @@ public enum Draft {
/**
* Draft 4 - <a href="http://json-schema.org/draft-04/schema#">http://json-schema.org/draft-04/schema#</a>
*
* Usually used by Swagger 2.0
* Usually used by OpenAPI 3.0
*/
DRAFT4,
DRAFT4("http://json-schema.org/draft-04/schema#"),

/**
* Draft 7 - <a href="http://json-schema.org/draft-07/schema#">http://json-schema.org/draft-07/schema#</a>
*
* Usually used by OpenAPI 3.0
* Commonly used by many projects
*/
DRAFT7,
DRAFT7("http://json-schema.org/draft-07/schema#"),

/**
* Draft 2019-09 - <a href="https://json-schema.org/draft/2019-09/schema">https://json-schema.org/draft/2019-09/schema</a>
*
* Commonly used by many projects
*/
DRAFT201909,
DRAFT201909("https://json-schema.org/draft/2019-09/schema"),

/**
* Draft 2019-09 - <a href="https://json-schema.org/draft/2020-12/schema">https://json-schema.org/draft/2020-12/schema</a>
* Draft 2020-12 - <a href="https://json-schema.org/draft/2020-12/schema">https://json-schema.org/draft/2020-12/schema</a>
*
* Usually used by OpenAPI 3.1
*/
DRAFT202012;
DRAFT202012("https://json-schema.org/draft/2020-12/schema");

private final String identifier;

Draft(String identifier) {
this.identifier = identifier;
}

/**
* @return the identifier of the draft version.
*/
public String getIdentifier() {
return identifier;
}

/**
* Converts a draft number to a {@link Draft} enum value.
Expand All @@ -75,25 +88,24 @@ public static Draft from(String string) {
}

/**
* Converts a draft idenfifier to a {@link Draft} enum value.
* Converts a draft identifier to a {@link Draft} enum value.
* @param string The identifier (in URL format)
* @return a Draft enum value
*/
public static Draft fromIdentifier(String string) {
if (string == null) {
throw new IllegalArgumentException("Invalid draft identifier: null");
}
switch (string) {
case "http://json-schema.org/draft-04/schema#":
return DRAFT4;
case "http://json-schema.org/draft-07/schema#":
return DRAFT7;
case "https://json-schema.org/draft/2019-09/schema":
return DRAFT201909;
case "https://json-schema.org/draft/2020-12/schema":
return DRAFT202012;
default:
throw new IllegalArgumentException("Unsupported draft identifier: " + string);
if(DRAFT4.identifier.equals(string)) {
return DRAFT4;
} else if(DRAFT7.identifier.equals(string)) {
return DRAFT7;
} else if(DRAFT201909.identifier.equals(string)) {
return DRAFT201909;
} else if(DRAFT202012.identifier.equals(string)) {
return DRAFT202012;
} else {
throw new IllegalArgumentException("Unsupported draft identifier: " + string);
}
}
}
42 changes: 33 additions & 9 deletions src/main/java/io/vertx/json/schema/SchemaRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@

import io.vertx.codegen.annotations.Fluent;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.core.file.FileSystem;
import io.vertx.core.json.JsonObject;
import io.vertx.json.schema.impl.SchemaRepositoryImpl;

/**
* A repository is a holder of dereferenced schemas, it can be used to create validator instances for a specific schema.
*
* <p>
* This is to be used when multiple schema objects compose the global schema to be used for validation.
*
* @author Paulo Lopes
Expand All @@ -27,6 +28,7 @@ public interface SchemaRepository {

/**
* Create a repository with some initial configuration.
*
* @param options the initial configuration
* @return a repository
*/
Expand All @@ -38,24 +40,45 @@ static SchemaRepository create(JsonSchemaOptions options) {
* Dereferences a schema to the repository.
*
* @param schema a new schema to list
* @throws SchemaException when a schema is already present for the same id
* @return a repository
* @throws SchemaException when a schema is already present for the same id
*/
@Fluent
SchemaRepository dereference(JsonSchema schema) throws SchemaException;

/**
* Dereferences a schema to the repository.
*
* @param uri the source of the schema used for de-referencing, optionally relative to
* {@link JsonSchemaOptions#getBaseUri()}.
* @param uri the source of the schema used for de-referencing, optionally relative to
* {@link JsonSchemaOptions#getBaseUri()}.
* @param schema a new schema to list
* @throws SchemaException when a schema is already present for the same id
* @return a repository
* @throws SchemaException when a schema is already present for the same id
*/
@Fluent
SchemaRepository dereference(String uri, JsonSchema schema) throws SchemaException;

/**
* Preloads the repository with the meta schemas for the related @link {@link Draft} version. The related draft version
* is determined from the {@link JsonSchemaOptions}, in case that no draft is set in the options an
* {@link IllegalStateException} is thrown.
*
* @param fs The Vert.x file system to load the related schema meta files from classpath
* @return a repository
*/
@Fluent
SchemaRepository preloadMetaSchema(FileSystem fs);

/**
* Preloads the repository with the meta schemas for the related draft version.
*
* @param fs The Vert.x file system to load the related schema meta files from classpath
* @param draft The draft version of the meta files to load
* @return a repository
*/
@Fluent
SchemaRepository preloadMetaSchema(FileSystem fs, Draft draft);

/**
* A new validator instance using this repository options.
*
Expand All @@ -75,7 +98,7 @@ static SchemaRepository create(JsonSchemaOptions options) {
/**
* A new validator instance overriding this repository options.
*
* @param schema the start validation schema
* @param schema the start validation schema
* @param options the options to be using on the validator instance
* @return the validator
*/
Expand All @@ -84,15 +107,15 @@ static SchemaRepository create(JsonSchemaOptions options) {
/**
* A new validator instance overriding this repository options.
*
* @param ref the start validation reference in JSON pointer format
* @param ref the start validation reference in JSON pointer format
* @param options the options to be using on the validator instance
* @return the validator
*/
Validator validator(String ref, JsonSchemaOptions options);

/**
* Tries to resolve all internal and repository local references. External references are not resolved.
*
* <p>
* The result is an object where all references have been resolved. Resolution of references is shallow. This
* should normally not be a problem for this use case.
*
Expand All @@ -103,7 +126,7 @@ static SchemaRepository create(JsonSchemaOptions options) {

/**
* Tries to resolve all internal and repository local references. External references are not resolved.
*
* <p>
* The result is an object where all references have been resolved. Resolution of references is shallow. This
* should normally not be a problem for this use case.
*
Expand All @@ -115,6 +138,7 @@ static SchemaRepository create(JsonSchemaOptions options) {

/**
* Look up a schema using a JSON pointer notation
*
* @param pointer the JSON pointer
* @return the schema
*/
Expand Down
76 changes: 76 additions & 0 deletions src/main/java/io/vertx/json/schema/impl/SchemaRepositoryImpl.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.vertx.json.schema.impl;

import io.vertx.core.file.FileSystem;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.json.schema.*;
Expand Down Expand Up @@ -64,6 +65,35 @@ public class SchemaRepositoryImpl implements SchemaRepository {
"else"
);

static final List<String> DRAFT_4_META_FILES = Arrays.asList(
"http://json-schema.org/draft-04/schema"
);

static final List<String> DRAFT_7_META_FILES = Arrays.asList(
"http://json-schema.org/draft-07/schema"
);

static final List<String> DRAFT_201909_META_FILES = Arrays.asList(
"https://json-schema.org/draft/2019-09/schema",
"https://json-schema.org/draft/2019-09/meta/core",
"https://json-schema.org/draft/2019-09/meta/applicator",
"https://json-schema.org/draft/2019-09/meta/validation",
"https://json-schema.org/draft/2019-09/meta/meta-data",
"https://json-schema.org/draft/2019-09/meta/format",
"https://json-schema.org/draft/2019-09/meta/content"
);

static final List<String> DRAFT_202012_META_FILES = Arrays.asList(
"https://json-schema.org/draft/2020-12/schema",
"https://json-schema.org/draft/2020-12/meta/core",
"https://json-schema.org/draft/2020-12/meta/applicator",
"https://json-schema.org/draft/2020-12/meta/validation",
"https://json-schema.org/draft/2020-12/meta/meta-data",
"https://json-schema.org/draft/2020-12/meta/format-annotation",
"https://json-schema.org/draft/2020-12/meta/content",
"https://json-schema.org/draft/2020-12/meta/unevaluated"
);

private final Map<String, JsonSchema> lookup = new HashMap<>();

private final JsonSchemaOptions options;
Expand All @@ -88,6 +118,43 @@ public SchemaRepository dereference(String uri, JsonSchema schema) throws Schema
return this;
}

@Override
public SchemaRepository preloadMetaSchema(FileSystem fs) {
if (options.getDraft() == null) {
throw new IllegalStateException("No draft version is defined in the options of the repository");
}
return preloadMetaSchema(fs, options.getDraft());
}

@Override
public SchemaRepository preloadMetaSchema(FileSystem fs, Draft draft) {
List<String> metaSchemaIds;
switch (draft) {
case DRAFT4:
metaSchemaIds = DRAFT_4_META_FILES;
break;
case DRAFT7:
metaSchemaIds = DRAFT_7_META_FILES;
break;
case DRAFT201909:
metaSchemaIds = DRAFT_201909_META_FILES;
break;
case DRAFT202012:
metaSchemaIds = DRAFT_202012_META_FILES;
break;
default:
throw new IllegalStateException();
}

for (String id : metaSchemaIds) {
// read files from classpath
JsonSchema schema = JsonSchema.of(fs.readFileBlocking(id.substring(id.indexOf("://") + 3)).toJsonObject());
// try to extract the '$id' from the schema itself, fallback to old field 'id' and if not present to the given url
dereference(schema.get("$id", schema.get("id", id)), schema);
}
return this;
}

@Override
public Validator validator(JsonSchema schema) {
return new SchemaValidatorImpl(schema, options, Collections.unmodifiableMap(lookup));
Expand All @@ -97,6 +164,9 @@ public Validator validator(JsonSchema schema) {
public Validator validator(String ref) {
// resolve the pointer to an absolute path
final URL url = new URL(ref, baseUri);
if ("".equals(url.fragment())) {
url.anchor(""); // normalize hash https://url.spec.whatwg.org/#dom-url-hash
}
final String uri = url.href();
if (lookup.containsKey(uri)) {
return new SchemaValidatorImpl(uri, options, Collections.unmodifiableMap(lookup));
Expand Down Expand Up @@ -131,6 +201,9 @@ public Validator validator(String ref, JsonSchemaOptions options) {
// resolve the pointer to an absolute path
final URL url = new URL(ref, baseUri);
final String uri = url.href();
if ("".equals(url.fragment())) {
url.anchor(""); // normalize hash https://url.spec.whatwg.org/#dom-url-hash
}
if (lookup.containsKey(uri)) {
return new SchemaValidatorImpl(uri, config, Collections.unmodifiableMap(lookup));
}
Expand All @@ -151,6 +224,9 @@ public JsonObject resolve(JsonSchema schema) {
public JsonObject resolve(String ref) {
// resolve the pointer to an absolute path
final URL url = new URL(ref, baseUri);
if ("".equals(url.fragment())) {
url.anchor(""); // normalize hash https://url.spec.whatwg.org/#dom-url-hash
}
final String uri = url.href();
if (lookup.containsKey(uri)) {
return Ref.resolve(Collections.unmodifiableMap(lookup), baseUri, lookup.get(uri));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.vertx.json.schema;

import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.junit5.VertxExtension;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

import static io.vertx.json.schema.Draft.DRAFT201909;
import static io.vertx.json.schema.Draft.DRAFT202012;
import static io.vertx.json.schema.Draft.DRAFT4;
import static io.vertx.json.schema.Draft.DRAFT7;

@ExtendWith(VertxExtension.class)
class SchemaDefinitionValidationTest {

private static final Path RESOURCE_PATH = Paths.get("src", "test", "resources", "schema_definition_validation");

private static Stream<Arguments> testSchemaDefinitionValidation() {
return Stream.of(
Arguments.of(DRAFT4, RESOURCE_PATH.resolve("OpenAPI3_0.json")),
Arguments.of(DRAFT7, RESOURCE_PATH.resolve("angular_cli_workspace_schema.json")),
Arguments.of(DRAFT201909, RESOURCE_PATH.resolve("compose_spec.json")),
Arguments.of(DRAFT202012, RESOURCE_PATH.resolve("OpenAPI3_1.json"))
);
}

@ParameterizedTest(name = "{index} test preloadMetaSchema with draft {0}")
@MethodSource
void testSchemaDefinitionValidation(Draft draft, Path schemaPath, Vertx vertx) throws IOException {
JsonSchemaOptions opts = new JsonSchemaOptions().setBaseUri("https://example.org");
SchemaRepository repo = SchemaRepository.create(opts);
repo.preloadMetaSchema(vertx.fileSystem(), draft);

Buffer schemaBuffer = Buffer.buffer(Files.readAllBytes(schemaPath));
JsonSchema schemaToValidate = JsonSchema.of(schemaBuffer.toJsonObject());

OutputUnit res = repo.validator(draft.getIdentifier()).validate(schemaToValidate);
Assertions.assertTrue(res.getValid());
}
}
Loading

0 comments on commit 87ae9ef

Please sign in to comment.