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(),