From 2fded86e8229b760dcbb37ed7d4c311d1218bd7e Mon Sep 17 00:00:00 2001 From: Sebastian Toepfer <61313468+sebastian-toepfer@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:26:20 +0200 Subject: [PATCH] add support for properties keyword --- core/pom.xml | 3 + .../applicator/ApplicatorVocabulary.java | 5 +- .../applicator/PropertiesKeywordType.java | 100 ++++++++++++++ .../applicator/PropertiesKeywordTypeTest.java | 127 ++++++++++++++++++ 4 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java create mode 100644 core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordTypeTest.java diff --git a/core/pom.xml b/core/pom.xml index a5b34cdc..65e3fb00 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -118,6 +118,9 @@ **/tests/draft2020-12/minItems.json **/tests/draft2020-12/maxItems.json **/tests/draft2020-12/patternProperties.json + 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 5668c99c..2f4f1848 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 @@ -37,9 +37,10 @@ public ApplicatorVocabulary() { this.vocab = new DefaultVocabulary( URI.create("https://json-schema.org/draft/2020-12/vocab/applicator"), + new PropertiesKeywordType(), new PatternPropertiesKeywordType(), - new PrefixItemsKeywordType(), - new ItemsKeywordType() + new ItemsKeywordType(), + new PrefixItemsKeywordType() ); } diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java new file mode 100644 index 00000000..f1906bd8 --- /dev/null +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordType.java @@ -0,0 +1,100 @@ +/* + * 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.applicator; + +import static jakarta.json.stream.JsonCollectors.toJsonArray; + +import io.github.sebastiantoepfer.jsonschema.InstanceType; +import io.github.sebastiantoepfer.jsonschema.JsonSchema; +import io.github.sebastiantoepfer.jsonschema.JsonSchemas; +import io.github.sebastiantoepfer.jsonschema.keyword.Annotation; +import io.github.sebastiantoepfer.jsonschema.keyword.Applicator; +import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; +import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class PropertiesKeywordType implements KeywordType { + + @Override + public String name() { + return "properties"; + } + + @Override + public Keyword createKeyword(final JsonSchema schema, final JsonValue value) { + return new PropertiesKeyword(value.asJsonObject()); + } + + private class PropertiesKeyword implements Applicator, Annotation { + + private final JsonObject schemas; + + public PropertiesKeyword(final JsonObject schemas) { + this.schemas = schemas; + } + + @Override + public boolean hasName(final String name) { + return Objects.equals(name(), name); + } + + @Override + public Collection categories() { + return List.of(KeywordCategory.ANNOTATION, KeywordCategory.APPLICATOR); + } + + @Override + public boolean applyTo(final JsonValue instance) { + return !InstanceType.OBJECT.isInstance(instance) || propertiesMatches(instance.asJsonObject()); + } + + private boolean propertiesMatches(final JsonObject instance) { + return instance.entrySet().stream().allMatch(this::propertyMatches); + } + + private boolean propertyMatches(final Map.Entry property) { + return ( + !schemas.containsKey(property.getKey()) || + JsonSchemas.load(schemas.get(property.getKey())).validator().validate(property.getValue()).isEmpty() + ); + } + + @Override + public JsonValue valueFor(final JsonValue value) { + return value + .asJsonObject() + .keySet() + .stream() + .filter(schemas::containsKey) + .map(Json::createValue) + .collect(toJsonArray()); + } + } +} diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordTypeTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordTypeTest.java new file mode 100644 index 00000000..1991a32d --- /dev/null +++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PropertiesKeywordTypeTest.java @@ -0,0 +1,127 @@ +/* + * 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.applicator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +import io.github.sebastiantoepfer.jsonschema.core.DefaultJsonSchemaFactory; +import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; +import jakarta.json.Json; +import jakarta.json.JsonValue; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +class PropertiesKeywordTypeTest { + + @Test + void should_be_know_his_name() { + final Keyword keyword = new PropertiesKeywordType() + .createKeyword(new DefaultJsonSchemaFactory().create(JsonValue.TRUE), JsonValue.EMPTY_JSON_OBJECT); + + assertThat(keyword.hasName("properties"), is(true)); + assertThat(keyword.hasName("test"), is(false)); + } + + @Test + void should_be_an_applicator_and_annotation() { + assertThat( + new PropertiesKeywordType() + .createKeyword(new DefaultJsonSchemaFactory().create(JsonValue.TRUE), JsonValue.EMPTY_JSON_OBJECT) + .categories(), + Matchers.containsInAnyOrder(Keyword.KeywordCategory.APPLICATOR, Keyword.KeywordCategory.ANNOTATION) + ); + } + + @Test + void should_be_valid_for_non_objects() { + assertThat( + new PropertiesKeywordType() + .createKeyword(new DefaultJsonSchemaFactory().create(JsonValue.TRUE), JsonValue.EMPTY_JSON_OBJECT) + .asApplicator() + .applyTo(JsonValue.EMPTY_JSON_ARRAY), + is(true) + ); + } + + @Test + void should_be_valid_if_properties_applies_to_his_schema() { + assertThat( + new PropertiesKeywordType() + .createKeyword( + new DefaultJsonSchemaFactory().create(JsonValue.TRUE), + Json.createObjectBuilder().add("test", JsonValue.TRUE).build() + ) + .asApplicator() + .applyTo(Json.createObjectBuilder().add("test", 1).build()), + is(true) + ); + } + + @Test + void should_be_invalid_if_properties_not_applies_to_his_schema() { + assertThat( + new PropertiesKeywordType() + .createKeyword( + new DefaultJsonSchemaFactory().create(JsonValue.TRUE), + Json.createObjectBuilder().add("test", JsonValue.FALSE).build() + ) + .asApplicator() + .applyTo(Json.createObjectBuilder().add("test", 1).build()), + is(false) + ); + } + + @Test + void should_be_valid_for_empty_objects() { + assertThat( + new PropertiesKeywordType() + .createKeyword( + new DefaultJsonSchemaFactory().create(JsonValue.TRUE), + Json.createObjectBuilder().add("test", JsonValue.FALSE).build() + ) + .asApplicator() + .applyTo(JsonValue.EMPTY_JSON_OBJECT), + is(true) + ); + } + + @Test + void should_return_all_matched_propertynames() { + assertThat( + new PropertiesKeywordType() + .createKeyword( + new DefaultJsonSchemaFactory().create(JsonValue.TRUE), + Json.createObjectBuilder().add("test", true).add("foo", true).build() + ) + .asAnnotation() + .valueFor(Json.createObjectBuilder().add("foo", 1).build()) + .asJsonArray() + .stream() + .toList(), + contains(Json.createValue("foo")) + ); + } +}