diff --git a/core/pom.xml b/core/pom.xml index e6d27979..6a5782d1 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -114,6 +114,10 @@ **/tests/draft2020-12/enum.json **/tests/draft2020-12/exclusiveMaximum.json **/tests/draft2020-12/exclusiveMinimum.json + **/tests/draft2020-12/format.json + @@ -128,7 +132,10 @@ **/tests/draft2020-12/patternProperties.json **/tests/draft2020-12/prefixItems.json **/tests/draft2020-12/properties.json - + **/tests/draft2020-12/required.json **/tests/draft2020-12/type.json **/tests/draft2020-12/uniqueItems.json diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonObjectSchema.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonObjectSchema.java index cf90c34e..87813013 100644 --- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonObjectSchema.java +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonObjectSchema.java @@ -26,25 +26,14 @@ import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toList; -import io.github.sebastiantoepfer.jsonschema.InstanceType; import io.github.sebastiantoepfer.jsonschema.JsonSubSchema; import io.github.sebastiantoepfer.jsonschema.Validator; -import io.github.sebastiantoepfer.jsonschema.core.codition.AllOfCondition; -import io.github.sebastiantoepfer.jsonschema.core.codition.ApplicatorBasedCondtion; -import io.github.sebastiantoepfer.jsonschema.core.codition.AssertionBasedCondition; -import io.github.sebastiantoepfer.jsonschema.core.codition.Condition; -import io.github.sebastiantoepfer.jsonschema.core.vocab.core.VocabularyKeywordType; import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; -import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType; -import io.github.sebastiantoepfer.jsonschema.vocabulary.spi.VocabularyDefinition; -import io.github.sebastiantoepfer.jsonschema.vocabulary.spi.VocabularyDefinitions; import jakarta.json.JsonObject; -import jakarta.json.JsonValue; -import java.util.Collection; import java.util.Optional; import java.util.stream.Stream; -final class DefaultJsonObjectSchema extends AbstractJsonValueSchema { +public final class DefaultJsonObjectSchema extends AbstractJsonValueSchema { public DefaultJsonObjectSchema(final JsonObject value) { super(value); @@ -52,12 +41,7 @@ public DefaultJsonObjectSchema(final JsonObject value) { @Override public Validator validator() { - return keywords() - .map(this::asContraint) - .flatMap(Optional::stream) - .collect( - collectingAndThen(toList(), constraints -> new DefaultValidator(new AllOfCondition<>(constraints))) - ); + return keywords().collect(collectingAndThen(toList(), KeywordBasedValidator::new)); } @Override @@ -66,42 +50,15 @@ public Optional keywordByName(final String name) { } private Stream keywords() { - final Keywords keywords = new Keywords(vocabulary()); + final Keywords keywords = new KeywordExtractor(this).createKeywords(); return asJsonObject().keySet().stream().map(propertyName -> keywords.createKeywordFor(this, propertyName)); } - private Collection vocabulary() { - final KeywordType keywordType = new VocabularyKeywordType(); - return Optional - .ofNullable(asJsonObject().get(keywordType.name())) - .map(keywordValue -> keywordType.createKeyword(this)) - .filter(VocabularyDefinitions.class::isInstance) - .map(VocabularyDefinitions.class::cast) - .stream() - .flatMap(VocabularyDefinitions::definitions) - .toList(); - } - @Override public Optional asSubSchema(final String name) { return Optional .ofNullable(asJsonObject().get(name)) - .filter(value -> - Stream.of(InstanceType.BOOLEAN, InstanceType.OBJECT).anyMatch(type -> type.isInstance(value)) - ) - .map(new DefaultJsonSchemaFactory()::create) + .flatMap(new DefaultJsonSchemaFactory()::tryToCreateSchemaFrom) .map(subSchema -> new DefaultJsonSubSchema(this, subSchema)); } - - private Optional> asContraint(final Keyword keyword) { - final Condition result; - if (keyword.hasCategory(Keyword.KeywordCategory.ASSERTION)) { - result = new AssertionBasedCondition(keyword.asAssertion()); - } else if (keyword.hasCategory(Keyword.KeywordCategory.APPLICATOR)) { - result = new ApplicatorBasedCondtion(keyword.asApplicator()); - } else { - result = null; - } - return Optional.ofNullable(result); - } } diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSchemaFactory.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSchemaFactory.java index 2435ac13..385a8b04 100644 --- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSchemaFactory.java +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSchemaFactory.java @@ -26,11 +26,16 @@ import io.github.sebastiantoepfer.jsonschema.JsonSchema; import io.github.sebastiantoepfer.jsonschema.spi.JsonSchemaFactory; import jakarta.json.JsonValue; +import java.util.Optional; public final class DefaultJsonSchemaFactory implements JsonSchemaFactory { @Override public JsonSchema create(final JsonValue schema) { + return tryToCreateSchemaFrom(schema).orElseThrow(IllegalArgumentException::new); + } + + public Optional tryToCreateSchemaFrom(final JsonValue schema) { final JsonSchema result; if (schema == JsonValue.TRUE) { result = new TrueJsonSchema(); @@ -38,9 +43,11 @@ public JsonSchema create(final JsonValue schema) { result = new FalseJsonSchema(); } else if (schema.equals(JsonValue.EMPTY_JSON_OBJECT)) { result = new EmptyJsonSchema(); - } else { + } else if (schema.getValueType() == JsonValue.ValueType.OBJECT) { result = new DefaultJsonObjectSchema(schema.asJsonObject()); + } else { + result = null; } - return result; + return Optional.ofNullable(result); } } diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSubSchema.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSubSchema.java index dd3d8787..c217eb9d 100644 --- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSubSchema.java +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/DefaultJsonSubSchema.java @@ -23,15 +23,20 @@ */ package io.github.sebastiantoepfer.jsonschema.core; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + import io.github.sebastiantoepfer.jsonschema.JsonSchema; import io.github.sebastiantoepfer.jsonschema.JsonSubSchema; import io.github.sebastiantoepfer.jsonschema.Validator; import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; import jakarta.json.JsonObject; +import jakarta.json.JsonValue; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; -final class DefaultJsonSubSchema implements JsonSubSchema { +public final class DefaultJsonSubSchema implements JsonSubSchema { private final JsonSchema owner; private final JsonSchema schema; @@ -48,17 +53,42 @@ public JsonSchema owner() { @Override public Validator validator() { - return schema.validator(); + final Validator result; + if (isJsonObject()) { + result = keywords().collect(collectingAndThen(toList(), KeywordBasedValidator::new)); + } else { + result = schema.validator(); + } + return result; } @Override public Optional keywordByName(final String name) { - return schema.keywordByName(name); + return keywords().filter(keyword -> keyword.hasName(name)).findAny(); + } + + private Stream keywords() { + final Stream result; + if (isJsonObject()) { + final Keywords keywords = new KeywordExtractor(schema).createKeywords(); + result = + asJsonObject().keySet().stream().map(propertyName -> keywords.createKeywordFor(this, propertyName)); + } else { + result = Stream.empty(); + } + return result; + } + + private boolean isJsonObject() { + return getValueType() == JsonValue.ValueType.OBJECT; } @Override public Optional asSubSchema(final String name) { - return schema.asSubSchema(name); + return Optional + .ofNullable(asJsonObject().get(name)) + .flatMap(new DefaultJsonSchemaFactory()::tryToCreateSchemaFrom) + .map(subSchema -> new DefaultJsonSubSchema(this, subSchema)); } @Override diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordBasedValidator.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordBasedValidator.java new file mode 100644 index 00000000..974d09b7 --- /dev/null +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordBasedValidator.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright 2023 sebastian. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.github.sebastiantoepfer.jsonschema.core; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + +import io.github.sebastiantoepfer.jsonschema.Validator; +import io.github.sebastiantoepfer.jsonschema.core.codition.AllOfCondition; +import io.github.sebastiantoepfer.jsonschema.core.codition.ApplicatorBasedCondtion; +import io.github.sebastiantoepfer.jsonschema.core.codition.AssertionBasedCondition; +import io.github.sebastiantoepfer.jsonschema.core.codition.Condition; +import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; +import jakarta.json.JsonValue; +import java.util.Collection; +import java.util.Optional; + +final class KeywordBasedValidator implements Validator { + + private final DefaultValidator validator; + + public KeywordBasedValidator(final Collection keywords) { + this.validator = + keywords + .stream() + .map(KeywordBasedValidator::asContraint) + .flatMap(Optional::stream) + .collect( + collectingAndThen(toList(), constraints -> new DefaultValidator(new AllOfCondition<>(constraints))) + ); + } + + @Override + public boolean isValid(final JsonValue data) { + return validator.isValid(data); + } + + private static Optional> asContraint(final Keyword keyword) { + final Condition result; + if (keyword.hasCategory(Keyword.KeywordCategory.ASSERTION)) { + result = new AssertionBasedCondition(keyword.asAssertion()); + } else if (keyword.hasCategory(Keyword.KeywordCategory.APPLICATOR)) { + result = new ApplicatorBasedCondtion(keyword.asApplicator()); + } else { + result = null; + } + return Optional.ofNullable(result); + } +} diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordExtractor.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordExtractor.java new file mode 100644 index 00000000..670ef102 --- /dev/null +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordExtractor.java @@ -0,0 +1,57 @@ +/* + * The MIT License + * + * Copyright 2023 sebastian. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.github.sebastiantoepfer.jsonschema.core; + +import io.github.sebastiantoepfer.jsonschema.JsonSchema; +import io.github.sebastiantoepfer.jsonschema.core.vocab.core.VocabularyKeywordType; +import jakarta.json.JsonValue; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +class KeywordExtractor { + + private final JsonSchema schema; + + public KeywordExtractor(final JsonSchema schema) { + this.schema = Objects.requireNonNull(schema); + } + + public Keywords createKeywords() { + final Keywords result; + final VocabularyKeywordType keywordType = new VocabularyKeywordType(); + if ( + schema.getValueType() == JsonValue.ValueType.OBJECT && schema.asJsonObject().containsKey(keywordType.name()) + ) { + result = + keywordType + .createKeyword(schema) + .definitions() + .collect(Collectors.collectingAndThen(Collectors.toList(), Keywords::new)); + } else { + result = new Keywords(List.of()); + } + return result; + } +} diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java index edd48814..0715ccb1 100644 --- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java @@ -27,7 +27,9 @@ import io.github.sebastiantoepfer.jsonschema.InstanceType; import io.github.sebastiantoepfer.jsonschema.JsonSchema; -import io.github.sebastiantoepfer.jsonschema.JsonSchemas; +import io.github.sebastiantoepfer.jsonschema.JsonSubSchema; +import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory; +import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSubSchema; import io.github.sebastiantoepfer.jsonschema.keyword.Annotation; import io.github.sebastiantoepfer.jsonschema.keyword.Applicator; import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; @@ -39,6 +41,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; final class PropertiesKeywordType implements KeywordType { @@ -49,14 +52,16 @@ public String name() { @Override public Keyword createKeyword(final JsonSchema schema) { - return new PropertiesKeyword(schema.asJsonObject().getJsonObject(name())); + return new PropertiesKeyword(schema, schema.asJsonObject().getJsonObject(name())); } private class PropertiesKeyword implements Applicator, Annotation { + private final JsonSchema schema; private final JsonObject schemas; - public PropertiesKeyword(final JsonObject schemas) { + public PropertiesKeyword(final JsonSchema schema, final JsonObject schemas) { + this.schema = schema; this.schemas = schemas; } @@ -80,10 +85,18 @@ private boolean propertiesMatches(final JsonObject instance) { } private boolean propertyMatches(final Map.Entry property) { - return ( - !schemas.containsKey(property.getKey()) || - JsonSchemas.load(schemas.get(property.getKey())).validator().isValid(property.getValue()) - ); + return Optional + .ofNullable(schemas.get(property.getKey())) + .flatMap(this::toSubSchema) + .map(JsonSchema::validator) + .map(validator -> validator.isValid(property.getValue())) + .orElse(true); + } + + private Optional toSubSchema(final JsonValue value) { + return new DefaultJsonSchemaFactory() + .tryToCreateSchemaFrom(value) + .map(subSchema -> new DefaultJsonSubSchema(schema, subSchema)); } @Override @@ -92,7 +105,7 @@ public JsonValue valueFor(final JsonValue instance) { .asJsonObject() .keySet() .stream() - .filter(schemas::containsKey) + .filter(schemas.asJsonObject()::containsKey) .map(Json::createValue) .collect(toJsonArray()); } diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/RefKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/RefKeywordType.java index a1bfd8d6..cff82fb0 100644 --- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/RefKeywordType.java +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/RefKeywordType.java @@ -33,6 +33,7 @@ import jakarta.json.JsonPointer; import jakarta.json.JsonReader; import jakarta.json.JsonString; +import jakarta.json.JsonStructure; import jakarta.json.JsonValue; import java.io.IOException; import java.net.URI; @@ -85,7 +86,7 @@ private JsonSchema retrieveJsonSchema() { if (isRemote()) { json = retrieveValueFromRemoteLocation(); } else { - json = retrievValueFromLocalSchema(); + json = retrieveValueFromLocalSchema(); } return JsonSchemas.load(json); } catch (IOException ex) { @@ -93,15 +94,19 @@ private JsonSchema retrieveJsonSchema() { } } - private JsonValue retrievValueFromLocalSchema() throws IOException { + private JsonValue retrieveValueFromLocalSchema() throws IOException { final JsonPointer pointer = createPointer(); - if (schema.getValueType() == JsonValue.ValueType.OBJECT && pointer.containsValue(schema.asJsonObject())) { - return pointer.getValue(schema.asJsonObject()); + if (pointer.containsValue(searchAnchor())) { + return pointer.getValue(searchAnchor()); } else { throw new IOException("can not find referenced value."); } } + private JsonStructure searchAnchor() { + return schema.rootSchema().asJsonObject(); + } + private JsonPointer createPointer() { final String fragment = uri.getFragment(); final JsonPointer pointer; diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/VocabularyKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/VocabularyKeywordType.java index e701e55e..6e648af9 100644 --- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/VocabularyKeywordType.java +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/core/VocabularyKeywordType.java @@ -48,9 +48,9 @@ public String name() { } @Override - public Keyword createKeyword(final JsonSchema schema) { + public VocabularyKeyword createKeyword(final JsonSchema schema) { final JsonValue value = schema.asJsonObject().get((name())); - final Keyword result; + final VocabularyKeyword result; if (InstanceType.OBJECT.isInstance(value)) { result = new VocabularyKeyword(value); } else { diff --git a/core/src/test/java/module-info.java b/core/src/test/java/module-info.java index 2950fd11..d1e86850 100644 --- a/core/src/test/java/module-info.java +++ b/core/src/test/java/module-info.java @@ -31,4 +31,5 @@ requires org.junit.jupiter.params; requires org.junit.jupiter.engine; requires org.hamcrest; + requires hamcrest.optional; }