From 4c7047044f44fd1da511eb120fa854a3bd95c06f Mon Sep 17 00:00:00 2001
From: Sebastian Toepfer <61313468+sebastian-toepfer@users.noreply.github.com>
Date: Sun, 3 Dec 2023 16:44:35 +0100
Subject: [PATCH] add limit support for ref -> works in properties
---
core/pom.xml | 9 ++-
.../core/DefaultJsonObjectSchema.java | 51 ++------------
.../core/DefaultJsonSchemaFactory.java | 11 ++-
.../jsonschema/core/DefaultJsonSubSchema.java | 38 ++++++++--
.../core/KeywordBasedValidator.java | 70 +++++++++++++++++++
.../jsonschema/core/KeywordExtractor.java | 57 +++++++++++++++
.../applicator/PropertiesKeywordType.java | 29 +++++---
.../core/vocab/core/RefKeywordType.java | 13 ++--
.../vocab/core/VocabularyKeywordType.java | 4 +-
core/src/test/java/module-info.java | 1 +
10 files changed, 215 insertions(+), 68 deletions(-)
create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordBasedValidator.java
create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/KeywordExtractor.java
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;
}