diff --git a/core/pom.xml b/core/pom.xml index 7a4ae06b..e5e55021 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -160,6 +160,13 @@ **/tests/draft2019-09/exclusiveMaximum.json **/tests/draft2020-12/exclusiveMaximum.json **/tests/draft-next/exclusiveMaximum.json + + **/tests/draft4/multipleOf.json + **/tests/draft6/multipleOf.json + **/tests/draft7/multipleOf.json + **/tests/draft2019-09/multipleOf.json + **/tests/draft2020-12/multipleOf.json + **/tests/draft-next/multipleOf.json diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/BasicVocabulary.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/BasicVocabulary.java index 57cb0cad..0616c530 100644 --- a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/BasicVocabulary.java +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/BasicVocabulary.java @@ -45,7 +45,8 @@ public BasicVocabulary() { new MinimumKeywordType(), new ExclusiveMinimumKeywordType(), new MaximumKeywordType(), - new ExclusiveMaximumKeywordType() + new ExclusiveMaximumKeywordType(), + new MultipleOfKeywordType() ); } diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/MultipleOfKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/MultipleOfKeywordType.java new file mode 100644 index 00000000..7fd33a92 --- /dev/null +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/MultipleOfKeywordType.java @@ -0,0 +1,84 @@ +/* + * The MIT License + * + * Copyright 2023 sebastian. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.github.sebastiantoepfer.jsonschema.core.vocab.basic; + +import io.github.sebastiantoepfer.jsonschema.ConstraintViolation; +import io.github.sebastiantoepfer.jsonschema.InstanceType; +import io.github.sebastiantoepfer.jsonschema.core.vocab.ConstraintAssertion; +import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; +import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType; +import jakarta.json.JsonNumber; +import jakarta.json.JsonValue; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +final class MultipleOfKeywordType implements KeywordType { + + @Override + public String name() { + return "multipleOf"; + } + + @Override + public Keyword createKeyword(final JsonValue value) { + if (InstanceType.NUMBER.isInstance(value)) { + return new MultipleOfKeyword((JsonNumber) value); + } else { + throw new IllegalArgumentException("value must be a postiv number"); + } + } + + private class MultipleOfKeyword implements ConstraintAssertion { + + private final BigDecimal multipleOf; + + public MultipleOfKeyword(final JsonNumber multipleOf) { + this.multipleOf = multipleOf.bigDecimalValue(); + } + + @Override + public boolean hasName(final String name) { + return Objects.equals(name(), name); + } + + @Override + public Collection violationsBy(final JsonValue value) { + final Collection result; + if ( + !InstanceType.NUMBER.isInstance(value) || + BigDecimal.ZERO.equals( + ((JsonNumber) value).bigDecimalValue().remainder(multipleOf).stripTrailingZeros() + ) + ) { + result = Collections.emptyList(); + } else { + result = List.of(new ConstraintViolation()); + } + return result; + } + } +} diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/MultipleOfKeywordTypeTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/MultipleOfKeywordTypeTest.java new file mode 100644 index 00000000..302c9b28 --- /dev/null +++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/basic/MultipleOfKeywordTypeTest.java @@ -0,0 +1,95 @@ +/* + * The MIT License + * + * Copyright 2023 sebastian. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.github.sebastiantoepfer.jsonschema.core.vocab.basic; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; +import jakarta.json.Json; +import jakarta.json.JsonValue; +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class MultipleOfKeywordTypeTest { + + @Test + void should_know_his_name() { + final Keyword multipleOf = new MultipleOfKeywordType().createKeyword(Json.createValue(10)); + + assertThat(multipleOf.hasName("multipleOf"), is(true)); + assertThat(multipleOf.hasName("test"), is(false)); + } + + @Test + void should_not_be_creatable_with_non_integer_value() { + final MultipleOfKeywordType keywordType = new MultipleOfKeywordType(); + assertThrows(IllegalArgumentException.class, () -> keywordType.createKeyword(JsonValue.FALSE)); + } + + @Test + void should_be_valid_for_non_number_values() { + assertThat( + new MultipleOfKeywordType() + .createKeyword(Json.createValue(10)) + .asAssertion() + .isValidFor(JsonValue.EMPTY_JSON_OBJECT), + is(true) + ); + } + + @Test + void should_be_valid_for_a_multipleOf() { + assertThat( + new MultipleOfKeywordType() + .createKeyword(Json.createValue(1.5)) + .asAssertion() + .isValidFor(Json.createValue(4.5)), + is(true) + ); + } + + @Test + void should_be_invalid_for_non_multipleOf() { + assertThat( + new MultipleOfKeywordType() + .createKeyword(Json.createValue(2)) + .asAssertion() + .isValidFor(Json.createValue(7)), + is(false) + ); + } + + @Test + void should_be_valid_for_any_int_if_multipleOf_is_1en8() { + assertThat( + new MultipleOfKeywordType() + .createKeyword(Json.createValue(new BigDecimal("1e-8"))) + .asAssertion() + .isValidFor(Json.createValue(12391239123L)), + is(true) + ); + } +}