From 9da94a5a946c3b63fe4a1c4de079c39dda162717 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 patternProperties keyword --- core/pom.xml | 1 + .../applicator/ApplicatorVocabulary.java | 1 + .../PatternPropertiesKeywordType.java | 97 ++++++++++++++ .../PatternPropertiesKeywordTypeTest.java | 120 ++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordType.java create mode 100644 core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordTypeTest.java diff --git a/core/pom.xml b/core/pom.xml index 6ac727db..a5b34cdc 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -117,6 +117,7 @@ --> **/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 856aad95..5668c99c 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,6 +37,7 @@ public ApplicatorVocabulary() { this.vocab = new DefaultVocabulary( URI.create("https://json-schema.org/draft/2020-12/vocab/applicator"), + new PatternPropertiesKeywordType(), new PrefixItemsKeywordType(), new ItemsKeywordType() ); diff --git a/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordType.java b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordType.java new file mode 100644 index 00000000..642707b3 --- /dev/null +++ b/core/src/main/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordType.java @@ -0,0 +1,97 @@ +/* + * 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 io.github.sebastiantoepfer.jsonschema.InstanceType; +import io.github.sebastiantoepfer.jsonschema.JsonSchema; +import io.github.sebastiantoepfer.jsonschema.JsonSchemas; +import io.github.sebastiantoepfer.jsonschema.keyword.Applicator; +import io.github.sebastiantoepfer.jsonschema.keyword.Keyword; +import io.github.sebastiantoepfer.jsonschema.keyword.KeywordType; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +final class PatternPropertiesKeywordType implements KeywordType { + + @Override + public String name() { + return "patternProperties"; + } + + @Override + public Keyword createKeyword(final JsonSchema schema, final JsonValue value) { + return new PatternPropertiesKeyword(value.asJsonObject()); + } + + private class PatternPropertiesKeyword implements Applicator { + + private final Map properties; + + public PatternPropertiesKeyword(final JsonObject schema) { + this.properties = + schema + .entrySet() + .stream() + .map(e -> Map.entry(Pattern.compile(e.getKey()), e.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public Collection categories() { + return List.of(Keyword.KeywordCategory.APPLICATOR, Keyword.KeywordCategory.ANNOTATION); + } + + @Override + public boolean hasName(final String name) { + return Objects.equals(name(), name); + } + + @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 properties + .entrySet() + .stream() + .filter(e -> e.getKey().matcher(property.getKey()).find()) + .findFirst() + .map(Map.Entry::getValue) + .map(JsonSchemas::load) + .map(schema -> schema.validator().validate(property.getValue()).isEmpty()) + .orElse(true); + } + } +} diff --git a/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordTypeTest.java b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordTypeTest.java new file mode 100644 index 00000000..f9fcadd1 --- /dev/null +++ b/core/src/test/java/io/github/sebastiantoepfer/jsonschema/core/vocab/applicator/PatternPropertiesKeywordTypeTest.java @@ -0,0 +1,120 @@ +/* + * 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.containsInAnyOrder; +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.junit.jupiter.api.Test; + +class PatternPropertiesKeywordTypeTest { + + @Test + void should_know_his_name() { + final Keyword keyword = new PatternPropertiesKeywordType() + .createKeyword(new DefaultJsonSchemaFactory().create(JsonValue.TRUE), JsonValue.EMPTY_JSON_OBJECT); + + assertThat(keyword.hasName("patternProperties"), is(true)); + assertThat(keyword.hasName("test"), is(false)); + } + + @Test + void should_be_an_applicator_and_an_annotation() { + assertThat( + new PatternPropertiesKeywordType() + .createKeyword(new DefaultJsonSchemaFactory().create(JsonValue.TRUE), JsonValue.EMPTY_JSON_OBJECT) + .categories(), + containsInAnyOrder(Keyword.KeywordCategory.APPLICATOR, Keyword.KeywordCategory.ANNOTATION) + ); + } + + @Test + void should_be_valid_for_non_object() { + assertThat( + new PatternPropertiesKeywordType() + .createKeyword(new DefaultJsonSchemaFactory().create(JsonValue.TRUE), JsonValue.EMPTY_JSON_OBJECT) + .asApplicator() + .applyTo(JsonValue.FALSE), + is(true) + ); + } + + @Test + void should_be_valid_for_empty_object() { + assertThat( + new PatternPropertiesKeywordType() + .createKeyword(new DefaultJsonSchemaFactory().create(JsonValue.TRUE), JsonValue.EMPTY_JSON_OBJECT) + .asApplicator() + .applyTo(JsonValue.EMPTY_JSON_OBJECT), + is(true) + ); + } + + @Test + void should_be_valid_if_properties_applies_to_his_schema() { + assertThat( + new PatternPropertiesKeywordType() + .createKeyword( + new DefaultJsonSchemaFactory().create(JsonValue.TRUE), + Json.createObjectBuilder().add("t.st", 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 PatternPropertiesKeywordType() + .createKeyword( + new DefaultJsonSchemaFactory().create(JsonValue.TRUE), + Json.createObjectBuilder().add("t.st", JsonValue.FALSE).build() + ) + .asApplicator() + .applyTo(Json.createObjectBuilder().add("test", 1).build()), + is(false) + ); + } + + @Test + void should_be_valid_if_properties_not_covered() { + assertThat( + new PatternPropertiesKeywordType() + .createKeyword( + new DefaultJsonSchemaFactory().create(JsonValue.TRUE), + Json.createObjectBuilder().add("t.st", JsonValue.FALSE).build() + ) + .asApplicator() + .applyTo(Json.createObjectBuilder().add("foo", 1).build()), + is(true) + ); + } +}