From 2d3616451644b655a2104bea7df7a533da6910f0 Mon Sep 17 00:00:00 2001 From: Sebastian Toepfer <61313468+sebastian-toepfer@users.noreply.github.com> Date: Wed, 29 May 2024 22:35:04 +0200 Subject: [PATCH] add support for const keyword --- core/pom.xml | 1 + .../core/keyword/type/AnyKeywordType.java | 52 +++++++ .../core/vocab/validation/ConstKeyword.java | 84 ++++++++++ .../validation/ValidationVocabulary.java | 2 + .../vocab/validation/ConstKeywordTest.java | 146 ++++++++++++++++++ 5 files changed, 285 insertions(+) create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AnyKeywordType.java create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeyword.java create mode 100644 core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeywordTest.java diff --git a/core/pom.xml b/core/pom.xml index 189e953d..65163823 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -118,6 +118,7 @@ --> **/tests/draft2020-12/additionalProperties.json **/tests/draft2020-12/boolean_schema.json + **/tests/draft2020-12/const.json **/tests/draft2020-12/enum.json **/tests/draft2020-12/exclusiveMaximum.json **/tests/draft2020-12/exclusiveMinimum.json diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AnyKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AnyKeywordType.java new file mode 100644 index 00000000..1b7862c1 --- /dev/null +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/keyword/type/AnyKeywordType.java @@ -0,0 +1,52 @@ +/* + * 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 io.github.sebastiantoepfer.jsonschema.keyword.KeywordType; +import jakarta.json.JsonValue; +import java.util.Objects; +import java.util.function.Function; + +public class AnyKeywordType implements KeywordType { + + private final String name; + private final Function keywordCreator; + + public AnyKeywordType(final String name, final Function keywordCreator) { + this.name = Objects.requireNonNull(name); + this.keywordCreator = Objects.requireNonNull(keywordCreator); + } + + @Override + public String name() { + return name; + } + + @Override + public Keyword createKeyword(final JsonSchema schema) { + return keywordCreator.apply(schema.asJsonObject().getOrDefault(name, JsonValue.NULL)); + } +} diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeyword.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeyword.java new file mode 100644 index 00000000..ccb90adb --- /dev/null +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeyword.java @@ -0,0 +1,84 @@ +/* + * The MIT License + * + * Copyright 2024 sebastian. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.github.sebastiantoepfer.jsonschema.core.vocab.validation; + +import io.github.sebastiantoepfer.ddd.common.Media; +import io.github.sebastiantoepfer.ddd.media.json.JsonObjectPrintable; +import io.github.sebastiantoepfer.jsonschema.InstanceType; +import io.github.sebastiantoepfer.jsonschema.keyword.Assertion; +import jakarta.json.Json; +import jakarta.json.JsonNumber; +import jakarta.json.JsonValue; +import java.util.Objects; + +/** + * const : Any
+ * Validation succeeds if the instance is equal to this keyword’s value.
+ *
+ * + * + * source: https://www.learnjsonschema.com/2020-12/validation/const/ + * spec: https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6.1.3 + */ +class ConstKeyword implements Assertion { + + static final String NAME = "const"; + private final JsonValue allowedValue; + + public ConstKeyword(final JsonValue allowedValue) { + this.allowedValue = Objects.requireNonNull(allowedValue); + } + + @Override + public boolean isValidFor(final JsonValue instance) { + return ( + instance != null && allowedValue.getValueType() == instance.getValueType() && isEqualsToAllowed(instance) + ); + } + + @SuppressWarnings("BigDecimalEquals") + private boolean isEqualsToAllowed(final JsonValue instance) { + final boolean result; + if (InstanceType.NUMBER.isInstance(instance)) { + result = ((JsonNumber) instance).bigDecimalValue() + .stripTrailingZeros() + .equals(((JsonNumber) allowedValue).bigDecimalValue().stripTrailingZeros()); + } else { + result = Objects.equals(allowedValue, instance); + } + return result; + } + + @Override + public boolean hasName(final String name) { + return Objects.equals(NAME, name); + } + + @Override + public > T printOn(final T media) { + return new JsonObjectPrintable(Json.createObjectBuilder().add(NAME, allowedValue).build()).printOn(media); + } +} 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 aaaa0bf7..67ceef92 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.AnyKeywordType; import io.github.sebastiantoepfer.jsonschema.core.keyword.type.ArrayKeywordType; import io.github.sebastiantoepfer.jsonschema.core.keyword.type.BooleanKeywordType; import io.github.sebastiantoepfer.jsonschema.core.keyword.type.IntegerKeywordType; @@ -44,6 +45,7 @@ public ValidationVocabulary(final JsonProvider jsonContext) { this.vocab = new DefaultVocabulary( URI.create("https://json-schema.org/draft/2020-12/vocab/validation"), new TypeKeywordType(), + new AnyKeywordType(ConstKeyword.NAME, ConstKeyword::new), new ArrayKeywordType(EnumKeyword.NAME, EnumKeyword::new), new StringKeywordType(jsonContext, PatternKeyword.NAME, PatternKeyword::new), new IntegerKeywordType(jsonContext, MinLengthKeyword.NAME, MinLengthKeyword::new), diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeywordTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeywordTest.java new file mode 100644 index 00000000..ea31cde7 --- /dev/null +++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/validation/ConstKeywordTest.java @@ -0,0 +1,146 @@ +/* + * The MIT License + * + * Copyright 2024 sebastian. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.github.sebastiantoepfer.jsonschema.core.vocab.validation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; + +import io.github.sebastiantoepfer.ddd.media.core.HashMapMedia; +import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory; +import io.github.sebastiantoepfer.jsonschema.core.keyword.type.AnyKeywordType; +import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Test; + +class ConstKeywordTest { + + @Test + void should_know_his_name() { + final Keyword enumKeyword = createKeywordFrom( + Json.createObjectBuilder().add("const", JsonValue.EMPTY_JSON_ARRAY).build() + ); + + assertThat(enumKeyword.hasName("const"), is(true)); + assertThat(enumKeyword.hasName("test"), is(false)); + } + + @Test + void should_be_valid_for_same_string_value() { + assertThat( + createKeywordFrom(Json.createObjectBuilder().add("const", Json.createValue("hello")).build()) + .asAssertion() + .isValidFor(Json.createValue("hello")), + is(true) + ); + } + + @Test + void should_be_invalid_for_different_string_value() { + assertThat( + createKeywordFrom(Json.createObjectBuilder().add("const", Json.createValue("hello")).build()) + .asAssertion() + .isValidFor(Json.createValue("world")), + is(false) + ); + } + + @Test + void should_be_valid_for_same_number_value() { + assertThat( + createKeywordFrom(Json.createObjectBuilder().add("const", Json.createValue(3.14159)).build()) + .asAssertion() + .isValidFor(Json.createValue(3.14159)), + is(true) + ); + } + + @Test + void should_be_invalid_for_value_with_different_type() { + assertThat( + createKeywordFrom(Json.createObjectBuilder().add("const", Json.createValue(3.14159)).build()) + .asAssertion() + .isValidFor(Json.createValue("pi")), + is(false) + ); + } + + @Test + void should_be_valid_for_exact_object_structure() { + assertThat( + createKeywordFrom( + Json.createObjectBuilder() + .add("const", Json.createObjectBuilder().add("name", "John Doe").add("age", 31)) + .build() + ) + .asAssertion() + .isValidFor(Json.createObjectBuilder().add("name", "John Doe").add("age", 31).build()), + is(true) + ); + } + + @Test + void should_be_invalid_for_not_exact_object_structure() { + assertThat( + createKeywordFrom( + Json.createObjectBuilder() + .add("const", Json.createObjectBuilder().add("name", "John Doe").add("age", 31)) + .build() + ) + .asAssertion() + .isValidFor(Json.createObjectBuilder().add("name", "Robert").add("age", 31).build()), + is(false) + ); + } + + @Test + void should_be_valid_for_decimal_without_scale_if_number_is_valid() { + assertThat( + createKeywordFrom(Json.createObjectBuilder().add("const", 1).build()) + .asAssertion() + .isValidFor(Json.createValue(1.0)), + is(true) + ); + } + + @Test + void should_be_printable() { + assertThat( + createKeywordFrom( + Json.createObjectBuilder().add("const", Json.createArrayBuilder().add("TEST").add("VALID")).build() + ).printOn(new HashMapMedia()), + (Matcher) hasEntry(is("const"), containsInAnyOrder("TEST", "VALID")) + ); + } + + private static Keyword createKeywordFrom(final JsonObject json) { + return new AnyKeywordType("const", ConstKeyword::new).createKeyword( + new DefaultJsonSchemaFactory().create(json) + ); + } +}