From 94786aa994da0ff844223e5af9f78012d60670d2 Mon Sep 17 00:00:00 2001
From: Sebastian Toepfer <61313468+sebastian-toepfer@users.noreply.github.com>
Date: Wed, 29 May 2024 22:35:04 +0200
Subject: [PATCH] add support for minContains keyword
---
core/pom.xml | 3 +-
.../keyword/type/AffectedByKeywordType.java | 90 ++++++++
.../core/keyword/type/AffectsKeywordType.java | 90 ++++++++
.../keyword/type/KeywordRelationship.java | 85 ++++++++
.../applicator/ApplicatorVocabulary.java | 10 +-
.../vocab/applicator/ContainsKeyword.java | 20 +-
.../vocab/validation/MaxContainsKeyword.java | 86 ++++++++
.../validation/MaxContainsKeywordType.java | 116 ----------
.../vocab/validation/MinContainsKeyword.java | 86 ++++++++
.../validation/ValidationVocabulary.java | 21 ++
.../vocab/applicator/ContainsKeywordTest.java | 55 ++++-
...eTest.java => MaxContainsKeywordTest.java} | 21 +-
.../validation/MinContainsKeywordTest.java | 199 ++++++++++++++++++
13 files changed, 746 insertions(+), 136 deletions(-)
create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedByKeywordType.java
create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectsKeywordType.java
create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/KeywordRelationship.java
create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeyword.java
delete mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordType.java
create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeyword.java
rename core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/{MaxContainsKeywordTypeTest.java => MaxContainsKeywordTest.java} (89%)
create mode 100644 core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeywordTest.java
diff --git a/core/pom.xml b/core/pom.xml
index 469017fd..2f2778e0 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -131,13 +131,12 @@
-
**/tests/draft2020-12/maxItems.json
**/tests/draft2020-12/maxLength.json
**/tests/draft2020-12/maximum.json
**/tests/draft2020-12/maxProperties.json
+ **/tests/draft2020-12/minContains.json
**/tests/draft2020-12/minItems.json
**/tests/draft2020-12/minLength.json
**/tests/draft2020-12/minimum.json
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedByKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedByKeywordType.java
new file mode 100644
index 00000000..ab528879
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedByKeywordType.java
@@ -0,0 +1,90 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.keyword.type;
+
+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.keyword.Keyword;
+import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+public class AffectedByKeywordType implements KeywordType {
+
+ private final String name;
+ private final List affectedBy;
+ private final BiFunction, JsonSchema, Keyword> keywordCreator;
+
+ public AffectedByKeywordType(
+ final String name,
+ final List affectedBy,
+ final BiFunction, JsonSchema, Keyword> keywordCreator
+ ) {
+ this.name = Objects.requireNonNull(name);
+ this.affectedBy = List.copyOf(affectedBy);
+ this.keywordCreator = Objects.requireNonNull(keywordCreator);
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public Keyword createKeyword(final JsonSchema schema) {
+ return new AffectedByKeyword(schema, name, affectedBy, keywordCreator);
+ }
+
+ static final class AffectedByKeyword extends KeywordRelationship {
+
+ private final JsonSchema schema;
+ private final List affectedBy;
+ private final BiFunction, JsonSchema, Keyword> keywordCreator;
+
+ public AffectedByKeyword(
+ final JsonSchema schema,
+ final String name,
+ final List affectedBy,
+ final BiFunction, JsonSchema, Keyword> keywordCreator
+ ) {
+ super(name);
+ this.schema = Objects.requireNonNull(schema);
+ this.affectedBy = List.copyOf(affectedBy);
+ this.keywordCreator = Objects.requireNonNull(keywordCreator);
+ }
+
+ @Override
+ protected Keyword delegate() {
+ return affectedBy
+ .stream()
+ .map(schema::keywordByName)
+ .flatMap(Optional::stream)
+ .collect(collectingAndThen(toList(), k -> keywordCreator.apply(k, schema)));
+ }
+ }
+}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectsKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectsKeywordType.java
new file mode 100644
index 00000000..33a27e87
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectsKeywordType.java
@@ -0,0 +1,90 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.keyword.type;
+
+import io.github.sebastiantoepfer.jsonschema.JsonSchema;
+import io.github.sebastiantoepfer.jsonschema.keyword.Annotation;
+import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
+import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType;
+import io.github.sebastiantoepfer.jsonschema.keyword.StaticAnnotation;
+import jakarta.json.JsonValue;
+import java.util.Objects;
+import java.util.function.BiFunction;
+
+public class AffectsKeywordType implements KeywordType {
+
+ private final String name;
+ private final String affects;
+ private final BiFunction keywordCreator;
+
+ public AffectsKeywordType(
+ final String name,
+ final String affects,
+ final BiFunction keywordCreator
+ ) {
+ this.name = name;
+ this.affects = affects;
+ this.keywordCreator = keywordCreator;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public Keyword createKeyword(final JsonSchema schema) {
+ return new AffectsKeyword(schema, name, affects, keywordCreator);
+ }
+
+ static final class AffectsKeyword extends KeywordRelationship {
+
+ private final JsonSchema schema;
+ private final String affects;
+ private final BiFunction keywordCreator;
+
+ public AffectsKeyword(
+ final JsonSchema schema,
+ final String name,
+ final String affects,
+ final BiFunction keywordCreator
+ ) {
+ super(name);
+ this.schema = Objects.requireNonNull(schema);
+ this.affects = affects;
+ this.keywordCreator = Objects.requireNonNull(keywordCreator);
+ }
+
+ @Override
+ protected Keyword delegate() {
+ return keywordCreator.apply(
+ schema
+ .keywordByName(affects)
+ .map(Keyword::asAnnotation)
+ .orElseGet(() -> new StaticAnnotation(affects, JsonValue.NULL)),
+ schema
+ );
+ }
+ }
+}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/KeywordRelationship.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/KeywordRelationship.java
new file mode 100644
index 00000000..de395e83
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/KeywordRelationship.java
@@ -0,0 +1,85 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.keyword.type;
+
+import io.github.sebastiantoepfer.ddd.common.Media;
+import io.github.sebastiantoepfer.jsonschema.keyword.Annotation;
+import io.github.sebastiantoepfer.jsonschema.keyword.Applicator;
+import io.github.sebastiantoepfer.jsonschema.keyword.Assertion;
+import io.github.sebastiantoepfer.jsonschema.keyword.Identifier;
+import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
+import io.github.sebastiantoepfer.jsonschema.keyword.ReservedLocation;
+import java.util.Collection;
+import java.util.Objects;
+
+abstract class KeywordRelationship implements Keyword {
+
+ private final String name;
+
+ protected KeywordRelationship(final String name) {
+ this.name = Objects.requireNonNull(name);
+ }
+
+ @Override
+ public final Collection categories() {
+ return delegate().categories();
+ }
+
+ @Override
+ public final boolean hasName(final String name) {
+ return Objects.equals(this.name, name);
+ }
+
+ @Override
+ public final > T printOn(final T media) {
+ return delegate().printOn(media);
+ }
+
+ @Override
+ public final Identifier asIdentifier() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final Assertion asAssertion() {
+ return delegate().asAssertion();
+ }
+
+ @Override
+ public final Annotation asAnnotation() {
+ return delegate().asAnnotation();
+ }
+
+ @Override
+ public final Applicator asApplicator() {
+ return delegate().asApplicator();
+ }
+
+ @Override
+ public final ReservedLocation asReservedLocation() {
+ throw new UnsupportedOperationException();
+ }
+
+ protected abstract Keyword delegate();
+}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ApplicatorVocabulary.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ApplicatorVocabulary.java
index 0c5b7e6c..ba3a0076 100644
--- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ApplicatorVocabulary.java
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ApplicatorVocabulary.java
@@ -24,12 +24,14 @@
package io.github.sebastiantoepfer.jsonschema.core.vocab.applicator;
import io.github.sebastiantoepfer.jsonschema.Vocabulary;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectedByKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.ArraySubSchemaKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.NamedJsonSchemaKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.SubSchemaKeywordType;
import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType;
import io.github.sebastiantoepfer.jsonschema.vocabulary.spi.DefaultVocabulary;
import java.net.URI;
+import java.util.List;
import java.util.Optional;
/**
@@ -51,7 +53,13 @@ public ApplicatorVocabulary() {
new NamedJsonSchemaKeywordType(PatternPropertiesKeyword.NAME, PatternPropertiesKeyword::new),
new SubSchemaKeywordType(ItemsKeyword.NAME, ItemsKeyword::new),
new ArraySubSchemaKeywordType(PrefixItemsKeyword.NAME, PrefixItemsKeyword::new),
- new SubSchemaKeywordType(ContainsKeyword.NAME, ContainsKeyword::new)
+ //normally affeced by minContains and maxContains, but only min has a direct effect!
+ new AffectedByKeywordType(
+ ContainsKeyword.NAME,
+ List.of("minContains"),
+ (a, schema) ->
+ new SubSchemaKeywordType(ContainsKeyword.NAME, s -> new ContainsKeyword(a, s)).createKeyword(schema)
+ )
);
}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeyword.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeyword.java
index dcb14df0..49636d27 100644
--- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeyword.java
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeyword.java
@@ -27,14 +27,16 @@
import io.github.sebastiantoepfer.ddd.common.Media;
import io.github.sebastiantoepfer.jsonschema.InstanceType;
-import io.github.sebastiantoepfer.jsonschema.JsonSubSchema;
+import io.github.sebastiantoepfer.jsonschema.JsonSchema;
import io.github.sebastiantoepfer.jsonschema.keyword.Annotation;
import io.github.sebastiantoepfer.jsonschema.keyword.Applicator;
+import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
import jakarta.json.JsonArray;
import jakarta.json.JsonValue;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Stream;
/**
* contains : Schema
@@ -51,9 +53,11 @@
final class ContainsKeyword implements Applicator, Annotation {
static final String NAME = "contains";
- private final JsonSubSchema contains;
+ private final JsonSchema contains;
+ private final List affectedBy;
- public ContainsKeyword(final JsonSubSchema contains) {
+ public ContainsKeyword(final List affectedBy, final JsonSchema contains) {
+ this.affectedBy = List.copyOf(affectedBy);
this.contains = Objects.requireNonNull(contains);
}
@@ -78,7 +82,7 @@ public boolean applyTo(final JsonValue instance) {
}
private boolean contains(final JsonArray array) {
- return array.stream().anyMatch(contains.validator()::isValid);
+ return !affectedBy.isEmpty() || matchingValues(array).findAny().isPresent();
}
@Override
@@ -94,12 +98,16 @@ public JsonValue valueFor(final JsonValue value) {
private JsonValue valueFor(final JsonArray values) {
final JsonValue result;
- final JsonArray matchingItems = values.stream().filter(contains.validator()::isValid).collect(toJsonArray());
- if (matchingItems.size() == values.size()) {
+ final JsonArray matchingItems = matchingValues(values).collect(toJsonArray());
+ if (matchingItems.size() == values.size() && !values.isEmpty()) {
result = JsonValue.TRUE;
} else {
result = matchingItems;
}
return result;
}
+
+ Stream matchingValues(final JsonArray values) {
+ return values.stream().filter(contains.validator()::isValid);
+ }
}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeyword.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeyword.java
new file mode 100644
index 00000000..e9e51768
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeyword.java
@@ -0,0 +1,86 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.vocab.validation;
+
+import io.github.sebastiantoepfer.ddd.common.Media;
+import io.github.sebastiantoepfer.jsonschema.InstanceType;
+import io.github.sebastiantoepfer.jsonschema.keyword.Annotation;
+import io.github.sebastiantoepfer.jsonschema.keyword.Assertion;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonValue;
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * minProperties : Integer
+ * An object instance is valid if its number of properties is less than, or equal to, the value of this keyword.
+ * keyword.
+ *
+ *
+ *
+ * source: https://www.learnjsonschema.com/2020-12/validation/maxcontains/
+ * spec: https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6.4.4
+ */
+final class MaxContainsKeyword implements Assertion {
+
+ static final String NAME = "maxContains";
+ private final Annotation affects;
+ private final BigInteger maxContains;
+
+ public MaxContainsKeyword(final Annotation affects, final BigInteger maxContains) {
+ this.affects = Objects.requireNonNull(affects);
+ this.maxContains = Objects.requireNonNull(maxContains);
+ }
+
+ @Override
+ public boolean hasName(final String name) {
+ return Objects.equals(NAME, name);
+ }
+
+ @Override
+ public > T printOn(final T media) {
+ return media.withValue(NAME, maxContains);
+ }
+
+ @Override
+ public boolean isValidFor(final JsonValue instance) {
+ return (
+ !InstanceType.ARRAY.isInstance(instance) || isValidFor(affects.valueFor(instance), instance.asJsonArray())
+ );
+ }
+
+ private boolean isValidFor(final JsonValue containing, final JsonArray values) {
+ final boolean result;
+ if (JsonValue.NULL.equals(containing)) {
+ result = true;
+ } else if (JsonValue.TRUE.equals(containing)) {
+ result = values.size() <= maxContains.intValue();
+ } else {
+ result = containing.asJsonArray().size() <= maxContains.intValue();
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordType.java
deleted file mode 100644
index 9b61deed..00000000
--- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordType.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright 2024 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.vocab.validation;
-
-import io.github.sebastiantoepfer.ddd.common.Media;
-import io.github.sebastiantoepfer.jsonschema.InstanceType;
-import io.github.sebastiantoepfer.jsonschema.JsonSchema;
-import io.github.sebastiantoepfer.jsonschema.core.keyword.type.IntegerKeywordType;
-import io.github.sebastiantoepfer.jsonschema.keyword.Assertion;
-import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
-import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonValue;
-import jakarta.json.spi.JsonProvider;
-import java.math.BigInteger;
-import java.util.Objects;
-
-/**
- * minProperties : Integer
- * An object instance is valid if its number of properties is less than, or equal to, the value of this keyword.
- * keyword.
- *
- *
- *
- * source: https://www.learnjsonschema.com/2020-12/validation/maxcontains/
- * spec: https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6.4.4
- */
-final class MaxContainsKeywordType implements KeywordType {
-
- private final JsonProvider jsonContext;
-
- public MaxContainsKeywordType(final JsonProvider jsonContext) {
- this.jsonContext = jsonContext;
- }
-
- @Override
- public String name() {
- return "maxContains";
- }
-
- @Override
- public Keyword createKeyword(final JsonSchema schema) {
- return new IntegerKeywordType(
- jsonContext,
- name(),
- value -> new MaxContainsKeyword(schema, value)
- ).createKeyword(schema);
- }
-
- private final class MaxContainsKeyword implements Assertion {
-
- private final JsonSchema owner;
- private final BigInteger maxContains;
-
- public MaxContainsKeyword(final JsonSchema owner, final BigInteger maxContains) {
- this.owner = Objects.requireNonNull(owner);
- this.maxContains = Objects.requireNonNull(maxContains);
- }
-
- @Override
- public boolean hasName(final String name) {
- return Objects.equals(name(), name);
- }
-
- @Override
- public > T printOn(final T media) {
- return media.withValue(name(), maxContains);
- }
-
- @Override
- public boolean isValidFor(final JsonValue instance) {
- return (
- !InstanceType.ARRAY.isInstance(instance) ||
- owner
- .keywordByName("contains")
- .map(Keyword::asAnnotation)
- .map(annotation -> annotation.valueFor(instance))
- .map(contains -> isValidFor(contains, instance.asJsonArray()))
- .orElse(true)
- );
- }
-
- private boolean isValidFor(final JsonValue containing, final JsonArray values) {
- final boolean result;
- if (JsonValue.TRUE.equals(containing)) {
- result = values.size() <= maxContains.intValue();
- } else {
- result = containing.asJsonArray().size() <= maxContains.intValue();
- }
- return result;
- }
- }
-}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeyword.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeyword.java
new file mode 100644
index 00000000..4b6ef50b
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeyword.java
@@ -0,0 +1,86 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.vocab.validation;
+
+import io.github.sebastiantoepfer.ddd.common.Media;
+import io.github.sebastiantoepfer.jsonschema.InstanceType;
+import io.github.sebastiantoepfer.jsonschema.keyword.Annotation;
+import io.github.sebastiantoepfer.jsonschema.keyword.Assertion;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonValue;
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * minContains : Integer
+ * The number of times that the contains keyword (if set) successfully validates against the instance must be
+ * greater than or equal to the given integer.
+ *
+ *
+ *
+ * source: https://www.learnjsonschema.com/2020-12/validation/mincontains/
+ * spec: https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6.4.5
+ */
+final class MinContainsKeyword implements Assertion {
+
+ static final String NAME = "minContains";
+ private final Annotation affects;
+ private final BigInteger minContains;
+
+ public MinContainsKeyword(final Annotation affects, final BigInteger minContains) {
+ this.affects = Objects.requireNonNull(affects);
+ this.minContains = Objects.requireNonNull(minContains);
+ }
+
+ @Override
+ public boolean hasName(final String name) {
+ return Objects.equals(NAME, name);
+ }
+
+ @Override
+ public > T printOn(final T media) {
+ return media.withValue(NAME, minContains);
+ }
+
+ @Override
+ public boolean isValidFor(final JsonValue instance) {
+ return (
+ !InstanceType.ARRAY.isInstance(instance) || isValidFor(affects.valueFor(instance), instance.asJsonArray())
+ );
+ }
+
+ private boolean isValidFor(final JsonValue containing, final JsonArray values) {
+ final boolean result;
+ if (JsonValue.NULL.equals(containing)) {
+ result = true;
+ } else if (JsonValue.TRUE.equals(containing)) {
+ result = values.size() >= minContains.intValue();
+ } else {
+ result = containing.asJsonArray().size() >= minContains.intValue();
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ValidationVocabulary.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ValidationVocabulary.java
index d8914192..f7d3ea36 100644
--- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ValidationVocabulary.java
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ValidationVocabulary.java
@@ -24,6 +24,7 @@
package io.github.sebastiantoepfer.jsonschema.core.vocab.validation;
import io.github.sebastiantoepfer.jsonschema.Vocabulary;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectsKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AnyKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.ArrayKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.BooleanKeywordType;
@@ -62,6 +63,26 @@ public ValidationVocabulary(final JsonProvider jsonContext) {
new StringArrayKeywordType(jsonContext, RequiredKeyword.NAME, RequiredKeyword::new),
new IntegerKeywordType(jsonContext, MaxItemsKeyword.NAME, MaxItemsKeyword::new),
new IntegerKeywordType(jsonContext, MinItemsKeyword.NAME, MinItemsKeyword::new),
+ new AffectsKeywordType(
+ MaxContainsKeyword.NAME,
+ "contains",
+ (affects, schema) ->
+ new IntegerKeywordType(
+ JsonProvider.provider(),
+ MaxContainsKeyword.NAME,
+ value -> new MaxContainsKeyword(affects, value)
+ ).createKeyword(schema)
+ ),
+ new AffectsKeywordType(
+ MinContainsKeyword.NAME,
+ "contains",
+ (a, s) ->
+ new IntegerKeywordType(
+ JsonProvider.provider(),
+ MinContainsKeyword.NAME,
+ value -> new MinContainsKeyword(a, value)
+ ).createKeyword(s)
+ ),
new BooleanKeywordType(jsonContext, UniqueItemsKeyword.NAME, UniqueItemsKeyword::new)
);
}
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeywordTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeywordTest.java
index f307b6c8..e6ddbad0 100644
--- a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeywordTest.java
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/ContainsKeywordTest.java
@@ -31,11 +31,13 @@
import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia;
import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectedByKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.SubSchemaKeywordType;
import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
+import java.util.List;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
@@ -54,8 +56,9 @@ void should_be_printable() {
@Test
void should_know_his_name() {
- final Keyword enumKeyword = createKeywordFrom(
- Json.createObjectBuilder().add("contains", Json.createObjectBuilder().add("type", "number")).build()
+ final Keyword enumKeyword = new ContainsKeyword(
+ List.of(),
+ new DefaultJsonSchemaFactory().create(JsonValue.TRUE)
);
assertThat(enumKeyword.hasName("contains"), is(true));
@@ -84,6 +87,33 @@ void should_apply_for_non_array() {
);
}
+ @Test
+ void should_apply_to_empty_array_if_min_andor_max_provided() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "number"))
+ .add("minContains", 0)
+ .build()
+ )
+ .asApplicator()
+ .applyTo(JsonValue.EMPTY_JSON_ARRAY),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_not_apply_to_empty_array_if_non_min_andor_max_is_provided() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder().add("contains", Json.createObjectBuilder().add("type", "number")).build()
+ )
+ .asApplicator()
+ .applyTo(JsonValue.EMPTY_JSON_ARRAY),
+ is(false)
+ );
+ }
+
@Test
void should_apply_if_one_item_applies() {
assertThat(
@@ -155,6 +185,19 @@ void should_return_empty_array_if_no_item_applies() {
);
}
+ @Test
+ void should_return_empty_array_for_empty_array() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder().add("contains", Json.createObjectBuilder().add("type", "number")).build()
+ )
+ .asAnnotation()
+ .valueFor(JsonValue.EMPTY_JSON_ARRAY)
+ .asJsonArray(),
+ is(empty())
+ );
+ }
+
@Test
void should_return_matching_items() {
assertThat(
@@ -189,8 +232,10 @@ void should_return_true_if_all_item_applies() {
}
private static Keyword createKeywordFrom(final JsonObject json) {
- return new SubSchemaKeywordType("contains", ContainsKeyword::new).createKeyword(
- new DefaultJsonSchemaFactory().create(json)
- );
+ return new AffectedByKeywordType(
+ "contains",
+ List.of("minContains", "maxContains"),
+ (a, schema) -> new SubSchemaKeywordType("contains", s -> new ContainsKeyword(a, s)).createKeyword(schema)
+ ).createKeyword(new DefaultJsonSchemaFactory().create(json));
}
}
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTypeTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTest.java
similarity index 89%
rename from core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTypeTest.java
rename to core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTest.java
index 01882a5c..2b7a3861 100644
--- a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTypeTest.java
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTest.java
@@ -29,7 +29,10 @@
import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia;
import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectsKeywordType;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.IntegerKeywordType;
import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
+import io.github.sebastiantoepfer.jsonschema.keyword.StaticAnnotation;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
@@ -38,12 +41,11 @@
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
-class MaxContainsKeywordTypeTest {
+class MaxContainsKeywordTest {
@Test
void should_know_his_name() {
- final Keyword enumKeyword = createKeywordFrom(Json.createObjectBuilder().add("maxContains", 2).build());
-
+ final Keyword enumKeyword = new MaxContainsKeyword(new StaticAnnotation("", JsonValue.NULL), BigInteger.ONE);
assertThat(enumKeyword.hasName("maxContains"), is(true));
assertThat(enumKeyword.hasName("test"), is(false));
}
@@ -182,8 +184,15 @@ void should_be_invalid_if_contains_applies_to_all_and_more_items_in_array() {
}
private static Keyword createKeywordFrom(final JsonObject json) {
- return new MaxContainsKeywordType(JsonProvider.provider()).createKeyword(
- new DefaultJsonSchemaFactory().create(json)
- );
+ return new AffectsKeywordType(
+ "maxContains",
+ "contains",
+ (a, s) ->
+ new IntegerKeywordType(
+ JsonProvider.provider(),
+ "maxContains",
+ value -> new MaxContainsKeyword(a, value)
+ ).createKeyword(s)
+ ).createKeyword(new DefaultJsonSchemaFactory().create(json));
}
}
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeywordTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeywordTest.java
new file mode 100644
index 00000000..682bcab6
--- /dev/null
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MinContainsKeywordTest.java
@@ -0,0 +1,199 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.vocab.validation;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.is;
+
+import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia;
+import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectsKeywordType;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.IntegerKeywordType;
+import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
+import io.github.sebastiantoepfer.jsonschema.keyword.StaticAnnotation;
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonValue;
+import jakarta.json.spi.JsonProvider;
+import java.math.BigInteger;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+
+class MinContainsKeywordTest {
+
+ @Test
+ void should_know_his_name() {
+ final Keyword enumKeyword = new MinContainsKeyword(new StaticAnnotation("", JsonValue.NULL), BigInteger.ONE);
+
+ assertThat(enumKeyword.hasName("minContains"), is(true));
+ assertThat(enumKeyword.hasName("test"), is(false));
+ }
+
+ @Test
+ void should_be_printable() {
+ assertThat(
+ createKeywordFrom(Json.createObjectBuilder().add("minContains", 2).build()).printOn(new HashMapMedia()),
+ (Matcher) hasEntry(is("minContains"), is(BigInteger.valueOf(2)))
+ );
+ }
+
+ @Test
+ void should_be_valid_for_non_arrays() {
+ assertThat(
+ createKeywordFrom(Json.createObjectBuilder().add("minContains", 2).build())
+ .asAssertion()
+ .isValidFor(JsonValue.EMPTY_JSON_OBJECT),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_be_valid_if_no_contains_is_present() {
+ assertThat(
+ createKeywordFrom(Json.createObjectBuilder().add("minContains", 2).build())
+ .asAssertion()
+ .isValidFor(Json.createArrayBuilder().add("foo").build()),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_be_valid_if_contains_applies_to_exact_count() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .add("minContains", 2)
+ .build()
+ )
+ .asAssertion()
+ .isValidFor(Json.createArrayBuilder().add("foo").add("bar").add(1).build()),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_be_valid_if_contains_applies_to_more_items() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .add("minContains", 2)
+ .build()
+ )
+ .asAssertion()
+ .isValidFor(Json.createArrayBuilder().add("foo").add(2).add(3).add("bar").add("baz").build()),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_be_valid_for_empty_arrays() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("const", 1))
+ .add("minContains", 0)
+ .build()
+ )
+ .asAssertion()
+ .isValidFor(JsonValue.EMPTY_JSON_ARRAY),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_be_invalid_if_contains_applies_to_less_items() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .add("minContains", 2)
+ .build()
+ )
+ .asAssertion()
+ .isValidFor(Json.createArrayBuilder().add("foo").add(1).build()),
+ is(false)
+ );
+ }
+
+ @Test
+ void should_be_valid_if_contains_applies_to_all_and_more_items_in_array() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .add("minContains", 2)
+ .build()
+ )
+ .asAssertion()
+ .isValidFor(Json.createArrayBuilder().add("foo").add("bar").add("baz").build()),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_be_valid_if_contains_applies_to_all_and_exact_items_count_in_array() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .add("minContains", 2)
+ .build()
+ )
+ .asAssertion()
+ .isValidFor(Json.createArrayBuilder().add("foo").add("bar").build()),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_be_invalid_if_contains_applies_to_all_and_less_items_in_array() {
+ assertThat(
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .add("minContains", 2)
+ .build()
+ )
+ .asAssertion()
+ .isValidFor(Json.createArrayBuilder().add("foo").build()),
+ is(false)
+ );
+ }
+
+ private static Keyword createKeywordFrom(final JsonObject json) {
+ return new AffectsKeywordType(
+ "minContains",
+ "contains",
+ (a, s) ->
+ new IntegerKeywordType(
+ JsonProvider.provider(),
+ "minContains",
+ value -> new MinContainsKeyword(a, value)
+ ).createKeyword(s)
+ ).createKeyword(new DefaultJsonSchemaFactory().create(json));
+ }
+}