diff --git a/core/pom.xml b/core/pom.xml
index e5af61a6..833426d5 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -81,14 +81,7 @@
com.google.errorprone
error_prone_annotations
test
-
-
-
- jakarta.json
- jakarta.json-api
- provided
-
-
+
org.eclipse.parsson
parsson
@@ -99,6 +92,17 @@
media-core
test
+
+ nl.jqno.equalsverifier
+ equalsverifier
+ test
+
+
+
+ jakarta.json
+ jakarta.json-api
+ provided
+
com.github.spotbugs
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectByType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectByType.java
new file mode 100644
index 00000000..de1ef5d6
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectByType.java
@@ -0,0 +1,43 @@
+/*
+ * 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.keyword.Keyword;
+
+public enum AffectByType {
+ EXTENDS {
+ @Override
+ Keyword affect(final Keyword affectedKeyword) {
+ return affectedKeyword;
+ }
+ },
+ REPLACE {
+ @Override
+ Keyword affect(final Keyword affectedKeyword) {
+ return new ReplacingKeyword(affectedKeyword);
+ }
+ };
+
+ abstract Keyword affect(final Keyword affectedKeyword);
+}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedBy.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedBy.java
new file mode 100644
index 00000000..0418be5c
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedBy.java
@@ -0,0 +1,89 @@
+/*
+ * 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.Keyword;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+public final class AffectedBy implements Comparable {
+
+ private final AffectByType type;
+ private final String name;
+
+ public AffectedBy(final AffectByType type, final String name) {
+ this.type = Objects.requireNonNull(type);
+ this.name = Objects.requireNonNull(name);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 83 * hash + Objects.hashCode(this.type);
+ hash = 83 * hash + Objects.hashCode(this.name);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ return compareTo((AffectedBy) obj) == 0;
+ }
+
+ @Override
+ public int compareTo(final AffectedBy other) {
+ final int result;
+ if (type.compareTo(other.type) == 0) {
+ result = name.compareTo(other.name);
+ } else {
+ result = type.compareTo(other.type);
+ }
+ return result;
+ }
+
+ Function findAffectedByKeywordIn(final JsonSchema schema) {
+ final UnaryOperator result;
+ if (schema.keywordByName(name).isPresent()) {
+ result = type::affect;
+ } else {
+ result = k -> k;
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "AffectedBy{" + "type=" + type + ", name=" + name + '}';
+ }
+}
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
index ab528879..4f16fd38 100644
--- 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
@@ -23,28 +23,30 @@
*/
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.Collection;
import java.util.List;
import java.util.Objects;
-import java.util.Optional;
-import java.util.function.BiFunction;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.function.Function;
-public class AffectedByKeywordType implements KeywordType {
+public final class AffectedByKeywordType implements KeywordType {
private final String name;
- private final List affectedBy;
- private final BiFunction, JsonSchema, Keyword> keywordCreator;
+ private final Collection affectedBy;
+ private final Function keywordCreator;
public AffectedByKeywordType(
final String name,
- final List affectedBy,
- final BiFunction, JsonSchema, Keyword> keywordCreator
+ final Collection affectedBy,
+ final Function keywordCreator
) {
+ if (affectedBy.isEmpty()) {
+ throw new IllegalArgumentException("affectedBy can not be empty!");
+ }
this.name = Objects.requireNonNull(name);
this.affectedBy = List.copyOf(affectedBy);
this.keywordCreator = Objects.requireNonNull(keywordCreator);
@@ -57,24 +59,24 @@ public String name() {
@Override
public Keyword createKeyword(final JsonSchema schema) {
- return new AffectedByKeyword(schema, name, affectedBy, keywordCreator);
+ return new AffectedKeyword(schema, name, affectedBy, keywordCreator);
}
- static final class AffectedByKeyword extends KeywordRelationship {
+ static final class AffectedKeyword extends KeywordRelationship {
private final JsonSchema schema;
- private final List affectedBy;
- private final BiFunction, JsonSchema, Keyword> keywordCreator;
+ private final SortedSet affectedBy;
+ private final Function keywordCreator;
- public AffectedByKeyword(
+ public AffectedKeyword(
final JsonSchema schema,
final String name,
- final List affectedBy,
- final BiFunction, JsonSchema, Keyword> keywordCreator
+ final Collection affectedBy,
+ final Function keywordCreator
) {
super(name);
this.schema = Objects.requireNonNull(schema);
- this.affectedBy = List.copyOf(affectedBy);
+ this.affectedBy = new TreeSet<>(affectedBy);
this.keywordCreator = Objects.requireNonNull(keywordCreator);
}
@@ -82,9 +84,10 @@ public AffectedByKeyword(
protected Keyword delegate() {
return affectedBy
.stream()
- .map(schema::keywordByName)
- .flatMap(Optional::stream)
- .collect(collectingAndThen(toList(), k -> keywordCreator.apply(k, schema)));
+ .map(a -> a.findAffectedByKeywordIn(schema))
+ .reduce(Function::andThen)
+ .orElseThrow()
+ .apply(keywordCreator.apply(schema));
}
}
}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/Affects.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/Affects.java
new file mode 100644
index 00000000..62c181d3
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/Affects.java
@@ -0,0 +1,94 @@
+/*
+ * 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.StaticAnnotation;
+import jakarta.json.JsonValue;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+public final class Affects {
+
+ private final String name;
+ private final AbsenceStrategy strategy;
+
+ public Affects(final String name, final JsonValue answerInAbsence) {
+ this(name, new ProvideDefaultValue(answerInAbsence));
+ }
+
+ public Affects(final String name, final AbsenceStrategy strategy) {
+ this.name = Objects.requireNonNull(name);
+ this.strategy = Objects.requireNonNull(strategy);
+ }
+
+ @Override
+ public String toString() {
+ return "Affects{" + "name=" + name + ", strategy=" + strategy.getClass() + '}';
+ }
+
+ Map.Entry> findAffectsKeywordIn(final JsonSchema schema) {
+ final Map.Entry> result;
+ final Optional annotation = schema
+ .keywordByName(name)
+ .filter(k -> k.hasCategory(Keyword.KeywordCategory.ANNOTATION))
+ .map(Keyword::asAnnotation);
+ if (annotation.isPresent()) {
+ result = Map.entry(annotation.get(), k -> k);
+ } else {
+ result = strategy.create(name);
+ }
+ return result;
+ }
+
+ public interface AbsenceStrategy {
+ Map.Entry> create(String name);
+ }
+
+ public static final class ReplaceKeyword implements AbsenceStrategy {
+
+ @Override
+ public Map.Entry> create(final String name) {
+ return Map.entry(new StaticAnnotation(name, JsonValue.NULL), ReplacingKeyword::new);
+ }
+ }
+
+ public static final class ProvideDefaultValue implements AbsenceStrategy {
+
+ private final JsonValue answerInAbsence;
+
+ public ProvideDefaultValue(final JsonValue answerInAbsence) {
+ this.answerInAbsence = Objects.requireNonNullElse(answerInAbsence, JsonValue.NULL);
+ }
+
+ @Override
+ public Map.Entry> create(final String name) {
+ return Map.entry(new StaticAnnotation(name, answerInAbsence), k -> k);
+ }
+ }
+}
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
index 33a27e87..5cf8620d 100644
--- 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
@@ -27,24 +27,27 @@
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.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
+import java.util.function.Function;
public class AffectsKeywordType implements KeywordType {
private final String name;
- private final String affects;
- private final BiFunction keywordCreator;
+ private final Collection affects;
+ private final BiFunction, JsonSchema, Keyword> keywordCreator;
public AffectsKeywordType(
final String name,
- final String affects,
- final BiFunction keywordCreator
+ final Collection affects,
+ final BiFunction, JsonSchema, Keyword> keywordCreator
) {
- this.name = name;
- this.affects = affects;
+ this.name = Objects.requireNonNull(name);
+ this.affects = List.copyOf(affects);
this.keywordCreator = keywordCreator;
}
@@ -55,36 +58,46 @@ public String name() {
@Override
public Keyword createKeyword(final JsonSchema schema) {
- return new AffectsKeyword(schema, name, affects, keywordCreator);
+ return new AffectsKeyword(schema, name, List.copyOf(affects), keywordCreator);
}
static final class AffectsKeyword extends KeywordRelationship {
private final JsonSchema schema;
- private final String affects;
- private final BiFunction keywordCreator;
+ private final Collection affects;
+ private final BiFunction, JsonSchema, Keyword> keywordCreator;
public AffectsKeyword(
final JsonSchema schema,
final String name,
- final String affects,
- final BiFunction keywordCreator
+ final List affects,
+ final BiFunction, JsonSchema, Keyword> keywordCreator
) {
super(name);
this.schema = Objects.requireNonNull(schema);
- this.affects = affects;
+ this.affects = List.copyOf(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
- );
+ //ugly ... map.entry is not the right structure ...
+ final Map.Entry, Function> p = affects
+ .stream()
+ .map(a -> a.findAffectsKeywordIn(schema))
+ .reduce(
+ Map.entry(List.of(), k -> k),
+ (
+ Map.Entry, Function> t,
+ Map.Entry> u
+ ) -> {
+ final ArrayList newAnnotations = new ArrayList<>(t.getKey());
+ newAnnotations.add(u.getKey());
+ return Map.entry(newAnnotations, t.getValue().andThen(u.getValue()));
+ },
+ (l, r) -> null
+ );
+ return p.getValue().apply(keywordCreator.apply(p.getKey(), schema));
}
}
}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/ReplacingKeyword.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/ReplacingKeyword.java
new file mode 100644
index 00000000..39d5a619
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/ReplacingKeyword.java
@@ -0,0 +1,114 @@
+/*
+ * 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.function.Predicate.not;
+import static java.util.stream.Collectors.toSet;
+
+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.EnumSet;
+import java.util.List;
+import java.util.Objects;
+
+final class ReplacingKeyword implements Keyword {
+
+ private final Keyword keywordToReplace;
+ private final Collection categoriesToReplace;
+
+ public ReplacingKeyword(final Keyword keywordToReplace) {
+ this(keywordToReplace, EnumSet.of(KeywordCategory.APPLICATOR, KeywordCategory.ASSERTION));
+ }
+
+ public ReplacingKeyword(final Keyword keywordToReplace, final Collection categoriesToReplace) {
+ this.keywordToReplace = Objects.requireNonNull(keywordToReplace);
+ this.categoriesToReplace = List.copyOf(categoriesToReplace);
+ }
+
+ @Override
+ public Identifier asIdentifier() {
+ if (categoriesToReplace.contains(KeywordCategory.IDENTIFIER)) {
+ throw new UnsupportedOperationException();
+ } else {
+ return keywordToReplace.asIdentifier();
+ }
+ }
+
+ @Override
+ public Assertion asAssertion() {
+ if (categoriesToReplace.contains(KeywordCategory.ASSERTION)) {
+ throw new UnsupportedOperationException();
+ } else {
+ return keywordToReplace.asAssertion();
+ }
+ }
+
+ @Override
+ public Annotation asAnnotation() {
+ if (categoriesToReplace.contains(KeywordCategory.ANNOTATION)) {
+ throw new UnsupportedOperationException();
+ } else {
+ return keywordToReplace.asAnnotation();
+ }
+ }
+
+ @Override
+ public Applicator asApplicator() {
+ if (categoriesToReplace.contains(KeywordCategory.APPLICATOR)) {
+ throw new UnsupportedOperationException();
+ } else {
+ return keywordToReplace.asApplicator();
+ }
+ }
+
+ @Override
+ public ReservedLocation asReservedLocation() {
+ if (categoriesToReplace.contains(KeywordCategory.RESERVED_LOCATION)) {
+ throw new UnsupportedOperationException();
+ } else {
+ return keywordToReplace.asReservedLocation();
+ }
+ }
+
+ @Override
+ public Collection categories() {
+ return keywordToReplace.categories().stream().filter(not(categoriesToReplace::contains)).collect(toSet());
+ }
+
+ @Override
+ public boolean hasName(final String string) {
+ return keywordToReplace.hasName(string);
+ }
+
+ @Override
+ public > T printOn(final T media) {
+ return keywordToReplace.printOn(media);
+ }
+}
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 1dc844f2..d3bd39fc 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,6 +24,8 @@
package io.github.sebastiantoepfer.jsonschema.core.vocab.applicator;
import io.github.sebastiantoepfer.jsonschema.Vocabulary;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectByType;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectedBy;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AffectedByKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.NamedJsonSchemaKeywordType;
import io.github.sebastiantoepfer.jsonschema.core.keyword.type.SchemaArrayKeywordType;
@@ -56,12 +58,14 @@ public ApplicatorVocabulary() {
new NamedJsonSchemaKeywordType(PatternPropertiesKeyword.NAME, PatternPropertiesKeyword::new),
new SubSchemaKeywordType(ItemsKeyword.NAME, ItemsKeyword::new),
new SchemaArrayKeywordType(PrefixItemsKeyword.NAME, PrefixItemsKeyword::new),
- //normally affeced by minContains and maxContains, but only min has a direct effect!
+ new SchemaArrayKeywordType(PrefixItemsKeyword.NAME, PrefixItemsKeyword::new),
new AffectedByKeywordType(
ContainsKeyword.NAME,
- List.of("minContains"),
- (a, schema) ->
- new SubSchemaKeywordType(ContainsKeyword.NAME, s -> new ContainsKeyword(a, s)).createKeyword(schema)
+ List.of(
+ new AffectedBy(AffectByType.REPLACE, "minContains"),
+ new AffectedBy(AffectByType.EXTENDS, "maxContains")
+ ),
+ new SubSchemaKeywordType(ContainsKeyword.NAME, ContainsKeyword::new)::createKeyword
)
);
}
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 49636d27..c22e044b 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
@@ -30,7 +30,6 @@
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;
@@ -54,10 +53,8 @@ final class ContainsKeyword implements Applicator, Annotation {
static final String NAME = "contains";
private final JsonSchema contains;
- private final List affectedBy;
- public ContainsKeyword(final List affectedBy, final JsonSchema contains) {
- this.affectedBy = List.copyOf(affectedBy);
+ public ContainsKeyword(final JsonSchema contains) {
this.contains = Objects.requireNonNull(contains);
}
@@ -82,7 +79,7 @@ public boolean applyTo(final JsonValue instance) {
}
private boolean contains(final JsonArray array) {
- return !affectedBy.isEmpty() || matchingValues(array).findAny().isPresent();
+ return matchingValues(array).findAny().isPresent();
}
@Override
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
index e9e51768..94d9fada 100644
--- 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
@@ -30,6 +30,8 @@
import jakarta.json.JsonArray;
import jakarta.json.JsonValue;
import java.math.BigInteger;
+import java.util.Collection;
+import java.util.List;
import java.util.Objects;
/**
@@ -47,11 +49,11 @@
final class MaxContainsKeyword implements Assertion {
static final String NAME = "maxContains";
- private final Annotation affects;
+ private final Collection affects;
private final BigInteger maxContains;
- public MaxContainsKeyword(final Annotation affects, final BigInteger maxContains) {
- this.affects = Objects.requireNonNull(affects);
+ public MaxContainsKeyword(final Collection affects, final BigInteger maxContains) {
+ this.affects = List.copyOf(affects);
this.maxContains = Objects.requireNonNull(maxContains);
}
@@ -67,20 +69,21 @@ public > T printOn(final T media) {
@Override
public boolean isValidFor(final JsonValue instance) {
- return (
- !InstanceType.ARRAY.isInstance(instance) || isValidFor(affects.valueFor(instance), instance.asJsonArray())
+ return (!InstanceType.ARRAY.isInstance(instance) || isValidFor(instance.asJsonArray()));
+ }
+
+ private boolean isValidFor(final JsonArray instance) {
+ return isValidFor(
+ affects
+ .stream()
+ .map(a -> a.valueFor(instance))
+ .map(v -> new NumberOfMatches(instance, v))
+ .mapToInt(NumberOfMatches::count)
+ .sum()
);
}
- 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;
+ private boolean isValidFor(final int containing) {
+ return containing <= maxContains.intValue();
}
}
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
index 4b6ef50b..e8277014 100644
--- 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
@@ -30,6 +30,8 @@
import jakarta.json.JsonArray;
import jakarta.json.JsonValue;
import java.math.BigInteger;
+import java.util.Collection;
+import java.util.List;
import java.util.Objects;
/**
@@ -47,11 +49,11 @@
final class MinContainsKeyword implements Assertion {
static final String NAME = "minContains";
- private final Annotation affects;
+ private final Collection affects;
private final BigInteger minContains;
- public MinContainsKeyword(final Annotation affects, final BigInteger minContains) {
- this.affects = Objects.requireNonNull(affects);
+ public MinContainsKeyword(final Collection affects, final BigInteger minContains) {
+ this.affects = List.copyOf(affects);
this.minContains = Objects.requireNonNull(minContains);
}
@@ -67,20 +69,21 @@ public > T printOn(final T media) {
@Override
public boolean isValidFor(final JsonValue instance) {
- return (
- !InstanceType.ARRAY.isInstance(instance) || isValidFor(affects.valueFor(instance), instance.asJsonArray())
+ return (!InstanceType.ARRAY.isInstance(instance) || isValidFor(instance.asJsonArray()));
+ }
+
+ private boolean isValidFor(final JsonArray instance) {
+ return isValidFor(
+ affects
+ .stream()
+ .map(a -> a.valueFor(instance))
+ .map(v -> new NumberOfMatches(instance, v))
+ .mapToInt(NumberOfMatches::count)
+ .sum()
);
}
- 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;
+ private boolean isValidFor(final int containing) {
+ return containing >= minContains.intValue();
}
}
diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/NumberOfMatches.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/NumberOfMatches.java
new file mode 100644
index 00000000..bb8f07da
--- /dev/null
+++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/NumberOfMatches.java
@@ -0,0 +1,43 @@
+/*
+ * 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 jakarta.json.JsonArray;
+import jakarta.json.JsonValue;
+import java.util.Objects;
+
+final class NumberOfMatches {
+
+ private final JsonArray array;
+ private final JsonValue value;
+
+ public NumberOfMatches(final JsonArray array, final JsonValue value) {
+ this.array = Objects.requireNonNull(array);
+ this.value = Objects.requireNonNull(value);
+ }
+
+ public int count() {
+ return value == JsonValue.TRUE ? array.size() : value.asJsonArray().size();
+ }
+}
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 abc376e7..86e69b80 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.Affects;
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;
@@ -37,6 +38,7 @@
import io.github.sebastiantoepfer.jsonschema.vocabulary.spi.DefaultVocabulary;
import jakarta.json.spi.JsonProvider;
import java.net.URI;
+import java.util.List;
import java.util.Optional;
public final class ValidationVocabulary implements Vocabulary {
@@ -65,7 +67,7 @@ public ValidationVocabulary(final JsonProvider jsonContext) {
new IntegerKeywordType(jsonContext, MinItemsKeyword.NAME, MinItemsKeyword::new),
new AffectsKeywordType(
MaxContainsKeyword.NAME,
- "contains",
+ List.of(new Affects("contains", new Affects.ReplaceKeyword())),
(affects, schema) ->
new IntegerKeywordType(
JsonProvider.provider(),
@@ -75,13 +77,13 @@ public ValidationVocabulary(final JsonProvider jsonContext) {
),
new AffectsKeywordType(
MinContainsKeyword.NAME,
- "contains",
- (a, s) ->
+ List.of(new Affects("contains", new Affects.ReplaceKeyword())),
+ (affects, schema) ->
new IntegerKeywordType(
JsonProvider.provider(),
MinContainsKeyword.NAME,
- value -> new MinContainsKeyword(a, value)
- ).createKeyword(s)
+ value -> new MinContainsKeyword(affects, value)
+ ).createKeyword(schema)
),
new BooleanKeywordType(jsonContext, UniqueItemsKeyword.NAME, UniqueItemsKeyword::new)
);
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedByTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedByTest.java
new file mode 100644
index 00000000..bce5b3ab
--- /dev/null
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectedByTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+
+import java.util.List;
+import java.util.TreeSet;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.jupiter.api.Test;
+
+class AffectedByTest {
+
+ @Test
+ void should_fullfil_equals_contract() {
+ EqualsVerifier.forClass(AffectedBy.class).withNonnullFields("type", "name").verify();
+ }
+
+ @Test
+ void should_be_sorted_correctly() {
+ assertThat(
+ new TreeSet<>(
+ List.of(
+ new AffectedBy(AffectByType.REPLACE, "d"),
+ new AffectedBy(AffectByType.EXTENDS, "c"),
+ new AffectedBy(AffectByType.EXTENDS, "b"),
+ new AffectedBy(AffectByType.REPLACE, "a")
+ )
+ ),
+ contains(
+ new AffectedBy(AffectByType.EXTENDS, "b"),
+ new AffectedBy(AffectByType.EXTENDS, "c"),
+ new AffectedBy(AffectByType.REPLACE, "a"),
+ new AffectedBy(AffectByType.REPLACE, "d")
+ )
+ );
+ }
+}
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectsTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectsTest.java
new file mode 100644
index 00000000..5e13cce3
--- /dev/null
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AffectsTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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 org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import io.github.sebastiantoepfer.jsonschema.JsonSchemas;
+import jakarta.json.Json;
+import jakarta.json.JsonValue;
+import org.junit.jupiter.api.Test;
+
+class AffectsTest {
+
+ @Test
+ void should_handle_non_annotation_as_absence() {
+ assertThat(
+ new Affects("type", new Affects.ProvideDefaultValue(JsonValue.EMPTY_JSON_OBJECT))
+ .findAffectsKeywordIn(JsonSchemas.load(Json.createObjectBuilder().add("type", "number").build()))
+ .getKey()
+ .valueFor(JsonValue.FALSE),
+ is(JsonValue.EMPTY_JSON_OBJECT)
+ );
+ }
+
+ @Test
+ void should_return_original_annotation() {
+ assertThat(
+ new Affects("properties", new Affects.ProvideDefaultValue(JsonValue.EMPTY_JSON_OBJECT))
+ .findAffectsKeywordIn(
+ JsonSchemas.load(
+ Json.createObjectBuilder()
+ .add(
+ "properties",
+ Json.createObjectBuilder().add("test", Json.createObjectBuilder().add("type", "number"))
+ )
+ .build()
+ )
+ )
+ .getKey()
+ .valueFor(Json.createObjectBuilder().add("test", 1L).build()),
+ is(Json.createArrayBuilder().add("test").build())
+ );
+ }
+
+ @Test
+ void should_create_original_annotation() {
+ assertThat(
+ new Affects("properties", new Affects.ProvideDefaultValue(JsonValue.EMPTY_JSON_OBJECT))
+ .findAffectsKeywordIn(
+ JsonSchemas.load(
+ Json.createObjectBuilder()
+ .add(
+ "properties",
+ Json.createObjectBuilder().add("test", Json.createObjectBuilder().add("type", "number"))
+ )
+ .build()
+ )
+ )
+ .getValue()
+ .apply(new MockKeyword("test"))
+ .hasName("test"),
+ is(true)
+ );
+ }
+
+ @Test
+ void should_create_original_annotation_also_for_missing() {
+ assertThat(
+ new Affects("properties", new Affects.ProvideDefaultValue(JsonValue.EMPTY_JSON_OBJECT))
+ .findAffectsKeywordIn(JsonSchemas.load(JsonValue.TRUE))
+ .getValue()
+ .apply(new MockKeyword("test"))
+ .hasName("test"),
+ is(true)
+ );
+ }
+}
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/MockKeyword.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/MockKeyword.java
new file mode 100644
index 00000000..35ba659b
--- /dev/null
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/MockKeyword.java
@@ -0,0 +1,80 @@
+/*
+ * 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.ReservedLocation;
+import jakarta.json.JsonValue;
+import java.net.URI;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Objects;
+
+final class MockKeyword implements Identifier, Assertion, Annotation, Applicator, ReservedLocation {
+
+ private final String name;
+
+ public MockKeyword(final String name) {
+ this.name = name;
+ }
+
+ @Override
+ public URI asUri() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public boolean isValidFor(final JsonValue instance) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public JsonValue valueFor(final JsonValue value) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public boolean applyTo(final JsonValue instance) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public Collection categories() {
+ return EnumSet.allOf(KeywordCategory.class);
+ }
+
+ @Override
+ public boolean hasName(final String name) {
+ return Objects.equals(this.name, name);
+ }
+
+ @Override
+ public > T printOn(final T media) {
+ return media.withValue(name, name);
+ }
+}
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/ReplacingKeywordTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/ReplacingKeywordTest.java
new file mode 100644
index 00000000..0f7c1a87
--- /dev/null
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/ReplacingKeywordTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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 org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.both;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia;
+import io.github.sebastiantoepfer.jsonschema.keyword.Keyword;
+import io.github.sebastiantoepfer.jsonschema.keyword.Keyword.KeywordCategory;
+import java.util.EnumSet;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+class ReplacingKeywordTest {
+
+ @Test
+ void should_not_be_createable_as_identifier_if_it_should_replace_it() {
+ final Keyword keyword = new ReplacingKeyword(
+ new MockKeyword("test"),
+ EnumSet.of(Keyword.KeywordCategory.IDENTIFIER)
+ );
+ assertThrows(UnsupportedOperationException.class, () -> keyword.asIdentifier());
+ }
+
+ @Test
+ void should_return_identifier_if_it_should_not_replaced() {
+ final Keyword keyword = new MockKeyword("test");
+ assertThat(
+ new ReplacingKeyword(
+ keyword,
+ EnumSet.complementOf(EnumSet.of(Keyword.KeywordCategory.IDENTIFIER))
+ ).asIdentifier(),
+ is(sameInstance(keyword))
+ );
+ }
+
+ @Test
+ void should_not_be_createable_as_assertion_if_it_should_replace_it() {
+ final Keyword keyword = new ReplacingKeyword(
+ new MockKeyword("test"),
+ EnumSet.of(Keyword.KeywordCategory.ASSERTION)
+ );
+ assertThrows(UnsupportedOperationException.class, () -> keyword.asAssertion());
+ }
+
+ @Test
+ void should_return_assertion_if_it_should_not_replaced() {
+ final Keyword keyword = new MockKeyword("test");
+ assertThat(
+ new ReplacingKeyword(
+ keyword,
+ EnumSet.complementOf(EnumSet.of(Keyword.KeywordCategory.ASSERTION))
+ ).asAssertion(),
+ is(sameInstance(keyword))
+ );
+ }
+
+ @Test
+ void should_not_be_createable_as_annotation_if_it_should_replace_it() {
+ final Keyword keyword = new ReplacingKeyword(
+ new MockKeyword("test"),
+ EnumSet.of(Keyword.KeywordCategory.ANNOTATION)
+ );
+ assertThrows(UnsupportedOperationException.class, () -> keyword.asAnnotation());
+ }
+
+ @Test
+ void should_return_annotation_if_it_should_not_replaced() {
+ final Keyword keyword = new MockKeyword("test");
+ assertThat(
+ new ReplacingKeyword(
+ keyword,
+ EnumSet.complementOf(EnumSet.of(Keyword.KeywordCategory.ANNOTATION))
+ ).asAnnotation(),
+ is(sameInstance(keyword))
+ );
+ }
+
+ @Test
+ void should_not_be_createable_as_applicator_if_it_should_replace_it() {
+ final Keyword keyword = new ReplacingKeyword(
+ new MockKeyword("test"),
+ EnumSet.of(Keyword.KeywordCategory.APPLICATOR)
+ );
+ assertThrows(UnsupportedOperationException.class, () -> keyword.asApplicator());
+ }
+
+ @Test
+ void should_return_applicator_if_it_should_not_replaced() {
+ final Keyword keyword = new MockKeyword("test");
+ assertThat(
+ new ReplacingKeyword(
+ keyword,
+ EnumSet.complementOf(EnumSet.of(Keyword.KeywordCategory.APPLICATOR))
+ ).asApplicator(),
+ is(sameInstance(keyword))
+ );
+ }
+
+ @Test
+ void should_not_be_createable_as_reserved_location_if_it_should_replace_it() {
+ final Keyword keyword = new ReplacingKeyword(
+ new MockKeyword("test"),
+ EnumSet.of(Keyword.KeywordCategory.RESERVED_LOCATION)
+ );
+ assertThrows(UnsupportedOperationException.class, () -> keyword.asReservedLocation());
+ }
+
+ @Test
+ void should_return_reserved_location_if_it_should_not_replaced() {
+ final Keyword keyword = new MockKeyword("test");
+ assertThat(
+ new ReplacingKeyword(
+ keyword,
+ EnumSet.complementOf(EnumSet.of(Keyword.KeywordCategory.RESERVED_LOCATION))
+ ).asReservedLocation(),
+ is(sameInstance(keyword))
+ );
+ }
+
+ @Test
+ void should_name_of_decoded_keyword() {
+ final Keyword keyword = new ReplacingKeyword(new MockKeyword("test"));
+
+ assertThat(keyword.hasName("test"), is(true));
+ assertThat(keyword.hasName("bunny"), is(false));
+ }
+
+ @Test
+ void should_print_as_decored_keyword() {
+ assertThat(
+ new ReplacingKeyword(new MockKeyword("test")).printOn(new HashMapMedia()),
+ Matchers.hasEntry("test", "test")
+ );
+ }
+
+ @ParameterizedTest
+ @EnumSource(KeywordCategory.class)
+ void should_not_return_replaced_keyword_category(final Keyword.KeywordCategory category) {
+ assertThat(
+ new ReplacingKeyword(new MockKeyword("test"), EnumSet.of(category)).categories(),
+ both(not(hasItem(category))).and((Matcher) is(not(empty())))
+ );
+ }
+}
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 e6ddbad0..02c8c5f8 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
@@ -30,14 +30,13 @@
import static org.hamcrest.Matchers.is;
import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia;
+import io.github.sebastiantoepfer.jsonschema.JsonSchemas;
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;
@@ -56,10 +55,7 @@ void should_be_printable() {
@Test
void should_know_his_name() {
- final Keyword enumKeyword = new ContainsKeyword(
- List.of(),
- new DefaultJsonSchemaFactory().create(JsonValue.TRUE)
- );
+ final Keyword enumKeyword = new ContainsKeyword(JsonSchemas.load(JsonValue.TRUE));
assertThat(enumKeyword.hasName("contains"), is(true));
assertThat(enumKeyword.hasName("test"), is(false));
@@ -87,21 +83,6 @@ 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(
@@ -232,10 +213,8 @@ void should_return_true_if_all_item_applies() {
}
private static Keyword createKeywordFrom(final JsonObject 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));
+ return new SubSchemaKeywordType("contains", ContainsKeyword::new).createKeyword(
+ new DefaultJsonSchemaFactory().create(json)
+ );
}
}
diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTest.java
index 2b7a3861..bff8d9b2 100644
--- a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTest.java
+++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/MaxContainsKeywordTest.java
@@ -29,6 +29,7 @@
import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia;
import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.Affects;
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;
@@ -38,6 +39,7 @@
import jakarta.json.JsonValue;
import jakarta.json.spi.JsonProvider;
import java.math.BigInteger;
+import java.util.List;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
@@ -45,7 +47,10 @@ class MaxContainsKeywordTest {
@Test
void should_know_his_name() {
- final Keyword enumKeyword = new MaxContainsKeyword(new StaticAnnotation("", JsonValue.NULL), BigInteger.ONE);
+ final Keyword enumKeyword = new MaxContainsKeyword(
+ List.of(new StaticAnnotation("", JsonValue.NULL)),
+ BigInteger.ONE
+ );
assertThat(enumKeyword.hasName("maxContains"), is(true));
assertThat(enumKeyword.hasName("test"), is(false));
}
@@ -61,23 +66,18 @@ void should_be_printable() {
@Test
void should_be_valid_for_non_arrays() {
assertThat(
- createKeywordFrom(Json.createObjectBuilder().add("maxContains", 2).build())
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .add("maxContains", 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("maxContains", 2).build())
- .asAssertion()
- .isValidFor(Json.createArrayBuilder().add("foo").build()),
- is(true)
- );
- }
-
@Test
void should_be_valid_if_contains_applies_to_exact_count() {
assertThat(
@@ -186,7 +186,7 @@ void should_be_invalid_if_contains_applies_to_all_and_more_items_in_array() {
private static Keyword createKeywordFrom(final JsonObject json) {
return new AffectsKeywordType(
"maxContains",
- "contains",
+ List.of(new Affects("contains", new Affects.ReplaceKeyword())),
(a, s) ->
new IntegerKeywordType(
JsonProvider.provider(),
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
index 682bcab6..645feeb1 100644
--- 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
@@ -29,6 +29,7 @@
import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia;
import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory;
+import io.github.sebastiantoepfer.jsonschema.core.keyword.type.Affects;
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;
@@ -38,6 +39,7 @@
import jakarta.json.JsonValue;
import jakarta.json.spi.JsonProvider;
import java.math.BigInteger;
+import java.util.List;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
@@ -45,7 +47,10 @@ class MinContainsKeywordTest {
@Test
void should_know_his_name() {
- final Keyword enumKeyword = new MinContainsKeyword(new StaticAnnotation("", JsonValue.NULL), BigInteger.ONE);
+ final Keyword enumKeyword = new MinContainsKeyword(
+ List.of(new StaticAnnotation("", JsonValue.NULL)),
+ BigInteger.ONE
+ );
assertThat(enumKeyword.hasName("minContains"), is(true));
assertThat(enumKeyword.hasName("test"), is(false));
@@ -62,23 +67,18 @@ void should_be_printable() {
@Test
void should_be_valid_for_non_arrays() {
assertThat(
- createKeywordFrom(Json.createObjectBuilder().add("minContains", 2).build())
+ createKeywordFrom(
+ Json.createObjectBuilder()
+ .add("contains", Json.createObjectBuilder().add("type", "string"))
+ .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(
@@ -187,7 +187,7 @@ void should_be_invalid_if_contains_applies_to_all_and_less_items_in_array() {
private static Keyword createKeywordFrom(final JsonObject json) {
return new AffectsKeywordType(
"minContains",
- "contains",
+ List.of(new Affects("contains", new Affects.ReplaceKeyword())),
(a, s) ->
new IntegerKeywordType(
JsonProvider.provider(),