-
Notifications
You must be signed in to change notification settings - Fork 0
Add exhaustive jqwik schema-document integration test #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,304 @@ | ||||||||||||||||||||||||||||
| package io.github.simbo1905.json.schema; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import jdk.sandbox.java.util.json.JsonArray; | ||||||||||||||||||||||||||||
| import jdk.sandbox.java.util.json.JsonBoolean; | ||||||||||||||||||||||||||||
| import jdk.sandbox.java.util.json.JsonNull; | ||||||||||||||||||||||||||||
| import jdk.sandbox.java.util.json.JsonNumber; | ||||||||||||||||||||||||||||
| import jdk.sandbox.java.util.json.JsonObject; | ||||||||||||||||||||||||||||
| import jdk.sandbox.java.util.json.JsonString; | ||||||||||||||||||||||||||||
| import jdk.sandbox.java.util.json.JsonValue; | ||||||||||||||||||||||||||||
| import net.jqwik.api.Arbitraries; | ||||||||||||||||||||||||||||
| import net.jqwik.api.Arbitrary; | ||||||||||||||||||||||||||||
| import net.jqwik.api.Combinators; | ||||||||||||||||||||||||||||
| import net.jqwik.api.ForAll; | ||||||||||||||||||||||||||||
| import net.jqwik.api.GenerationMode; | ||||||||||||||||||||||||||||
| import net.jqwik.api.Property; | ||||||||||||||||||||||||||||
| import net.jqwik.api.UseArbitraryProviders; | ||||||||||||||||||||||||||||
| import net.jqwik.api.providers.ArbitraryProvider; | ||||||||||||||||||||||||||||
| import net.jqwik.api.providers.SubtypeProvider; | ||||||||||||||||||||||||||||
| import net.jqwik.api.providers.TypeUsage; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import java.math.BigDecimal; | ||||||||||||||||||||||||||||
| import java.util.LinkedHashMap; | ||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||
| import java.util.Objects; | ||||||||||||||||||||||||||||
| import java.util.Set; | ||||||||||||||||||||||||||||
| import java.util.logging.Logger; | ||||||||||||||||||||||||||||
| import java.util.stream.Collectors; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @UseArbitraryProviders(ITJsonSchemaExhaustiveTest.SchemaArbitraryProvider.class) | ||||||||||||||||||||||||||||
| class ITJsonSchemaExhaustiveTest extends JsonSchemaLoggingConfig { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static final Logger LOGGER = Logger.getLogger(io.github.simbo1905.json.schema.JsonSchema.class.getName()); | ||||||||||||||||||||||||||||
| private static final int MAX_DEPTH = 3; | ||||||||||||||||||||||||||||
| private static final List<String> PROPERTY_NAMES = List.of("alpha", "beta", "gamma", "delta"); | ||||||||||||||||||||||||||||
| private static final List<List<String>> PROPERTY_PAIRS = List.of( | ||||||||||||||||||||||||||||
| List.of("alpha", "beta"), | ||||||||||||||||||||||||||||
| List.of("alpha", "gamma"), | ||||||||||||||||||||||||||||
| List.of("alpha", "delta"), | ||||||||||||||||||||||||||||
| List.of("beta", "gamma"), | ||||||||||||||||||||||||||||
| List.of("beta", "delta"), | ||||||||||||||||||||||||||||
| List.of("gamma", "delta") | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @Property(generation = GenerationMode.EXHAUSTIVE) | ||||||||||||||||||||||||||||
| void exhaustiveRoundTrip(@ForAll ITJsonSchemaExhaustiveTest.JsonSchema schema) { | ||||||||||||||||||||||||||||
| LOGGER.info(() -> "Executing exhaustiveRoundTrip property test"); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| final var schemaDescription = describeSchema(schema); | ||||||||||||||||||||||||||||
| LOGGER.fine(() -> "Schema descriptor: " + schemaDescription); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| final var schemaJson = schemaToJsonObject(schema); | ||||||||||||||||||||||||||||
| LOGGER.finer(() -> "Schema JSON: " + schemaJson); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| final var compiled = io.github.simbo1905.json.schema.JsonSchema.compile(schemaJson); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| final var compliantDocument = buildCompliantDocument(schema); | ||||||||||||||||||||||||||||
| LOGGER.finer(() -> "Compliant document: " + compliantDocument); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| final var validation = compiled.validate(compliantDocument); | ||||||||||||||||||||||||||||
| assertThat(validation.valid()) | ||||||||||||||||||||||||||||
| .as("Compliant document should validate for schema %s", schemaDescription) | ||||||||||||||||||||||||||||
| .isTrue(); | ||||||||||||||||||||||||||||
| assertThat(validation.errors()) | ||||||||||||||||||||||||||||
| .as("No validation errors expected for compliant document") | ||||||||||||||||||||||||||||
| .isEmpty(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| final var failingDocuments = failingDocuments(schema, compliantDocument); | ||||||||||||||||||||||||||||
| assertThat(failingDocuments) | ||||||||||||||||||||||||||||
| .as("Negative cases should be generated for schema %s", schemaDescription) | ||||||||||||||||||||||||||||
| .isNotEmpty(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| final var failingDocumentStrings = failingDocuments.stream() | ||||||||||||||||||||||||||||
| .map(Object::toString) | ||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||
| LOGGER.finest(() -> "Failing documents: " + failingDocumentStrings); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| failingDocuments.forEach(failing -> { | ||||||||||||||||||||||||||||
| final var failingResult = compiled.validate(failing); | ||||||||||||||||||||||||||||
| assertThat(failingResult.valid()) | ||||||||||||||||||||||||||||
| .as("Expected validation failure for %s against schema %s", failing, schemaDescription) | ||||||||||||||||||||||||||||
| .isFalse(); | ||||||||||||||||||||||||||||
| assertThat(failingResult.errors()) | ||||||||||||||||||||||||||||
| .as("Expected validation errors for %s against schema %s", failing, schemaDescription) | ||||||||||||||||||||||||||||
| .isNotEmpty(); | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static JsonValue buildCompliantDocument(JsonSchema schema) { | ||||||||||||||||||||||||||||
| return switch (schema) { | ||||||||||||||||||||||||||||
| case ObjectSchema(var properties) -> JsonObject.of(properties.stream() | ||||||||||||||||||||||||||||
| .collect(Collectors.toMap( | ||||||||||||||||||||||||||||
| JsonSchema.Property::name, | ||||||||||||||||||||||||||||
| property -> buildCompliantDocument(property.schema()), | ||||||||||||||||||||||||||||
| (left, right) -> left, | ||||||||||||||||||||||||||||
| LinkedHashMap::new | ||||||||||||||||||||||||||||
| ))); | ||||||||||||||||||||||||||||
| case ArraySchema(var items) -> JsonArray.of(items.stream() | ||||||||||||||||||||||||||||
| .map(ITJsonSchemaExhaustiveTest::buildCompliantDocument) | ||||||||||||||||||||||||||||
| .toList()); | ||||||||||||||||||||||||||||
| case StringSchema ignored -> JsonString.of("valid"); | ||||||||||||||||||||||||||||
| case NumberSchema ignored -> JsonNumber.of(BigDecimal.ONE); | ||||||||||||||||||||||||||||
| case BooleanSchema ignored -> JsonBoolean.of(true); | ||||||||||||||||||||||||||||
| case NullSchema ignored -> JsonNull.of(); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static List<JsonValue> failingDocuments(JsonSchema schema, JsonValue compliant) { | ||||||||||||||||||||||||||||
| return switch (schema) { | ||||||||||||||||||||||||||||
| case ObjectSchema(var properties) -> properties.isEmpty() | ||||||||||||||||||||||||||||
| ? List.<JsonValue>of(JsonNull.of()) | ||||||||||||||||||||||||||||
| : properties.stream() | ||||||||||||||||||||||||||||
| .map(JsonSchema.Property::name) | ||||||||||||||||||||||||||||
| .map(name -> removeProperty((JsonObject) compliant, name)) | ||||||||||||||||||||||||||||
| .map(json -> (JsonValue) json) | ||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||
| case ArraySchema(var items) -> { | ||||||||||||||||||||||||||||
| final var values = ((JsonArray) compliant).values(); | ||||||||||||||||||||||||||||
| if (values.isEmpty()) { | ||||||||||||||||||||||||||||
| yield List.<JsonValue>of(JsonNull.of()); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| final var truncated = JsonArray.of(values.stream().limit(values.size() - 1L).toList()); | ||||||||||||||||||||||||||||
| yield List.<JsonValue>of(truncated); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| case StringSchema ignored -> List.<JsonValue>of(JsonNumber.of(BigDecimal.TWO)); | ||||||||||||||||||||||||||||
| case NumberSchema ignored -> List.<JsonValue>of(JsonString.of("not-a-number")); | ||||||||||||||||||||||||||||
| case BooleanSchema ignored -> List.<JsonValue>of(JsonNull.of()); | ||||||||||||||||||||||||||||
| case NullSchema ignored -> List.<JsonValue>of(JsonBoolean.of(true)); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static JsonObject removeProperty(JsonObject original, String missingProperty) { | ||||||||||||||||||||||||||||
| final var filtered = original.members().entrySet().stream() | ||||||||||||||||||||||||||||
| .filter(entry -> !Objects.equals(entry.getKey(), missingProperty)) | ||||||||||||||||||||||||||||
| .collect(Collectors.toMap( | ||||||||||||||||||||||||||||
| Map.Entry::getKey, | ||||||||||||||||||||||||||||
| Map.Entry::getValue, | ||||||||||||||||||||||||||||
| (left, right) -> left, | ||||||||||||||||||||||||||||
| LinkedHashMap::new | ||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||
| return JsonObject.of(filtered); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static JsonObject schemaToJsonObject(JsonSchema schema) { | ||||||||||||||||||||||||||||
| return switch (schema) { | ||||||||||||||||||||||||||||
| case ObjectSchema(var properties) -> { | ||||||||||||||||||||||||||||
| final var schemaMap = new LinkedHashMap<String, JsonValue>(); | ||||||||||||||||||||||||||||
| schemaMap.put("type", JsonString.of("object")); | ||||||||||||||||||||||||||||
| final var propertyMap = properties.isEmpty() | ||||||||||||||||||||||||||||
| ? JsonObject.of(Map.<String, JsonValue>of()) | ||||||||||||||||||||||||||||
| : JsonObject.of(properties.stream() | ||||||||||||||||||||||||||||
| .collect(Collectors.toMap( | ||||||||||||||||||||||||||||
| JsonSchema.Property::name, | ||||||||||||||||||||||||||||
| property -> (JsonValue) schemaToJsonObject(property.schema()), | ||||||||||||||||||||||||||||
| (left, right) -> left, | ||||||||||||||||||||||||||||
| LinkedHashMap::new | ||||||||||||||||||||||||||||
| ))); | ||||||||||||||||||||||||||||
| schemaMap.put("properties", propertyMap); | ||||||||||||||||||||||||||||
| final var requiredValues = properties.stream() | ||||||||||||||||||||||||||||
| .map(JsonSchema.Property::name) | ||||||||||||||||||||||||||||
| .map(JsonString::of) | ||||||||||||||||||||||||||||
| .map(value -> (JsonValue) value) | ||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||
| schemaMap.put("required", JsonArray.of(requiredValues)); | ||||||||||||||||||||||||||||
| schemaMap.put("additionalProperties", JsonBoolean.of(false)); | ||||||||||||||||||||||||||||
| yield JsonObject.of(schemaMap); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| case ArraySchema(var items) -> { | ||||||||||||||||||||||||||||
| final var schemaMap = new LinkedHashMap<String, JsonValue>(); | ||||||||||||||||||||||||||||
| schemaMap.put("type", JsonString.of("array")); | ||||||||||||||||||||||||||||
| final var prefixItems = items.stream() | ||||||||||||||||||||||||||||
| .map(ITJsonSchemaExhaustiveTest::schemaToJsonObject) | ||||||||||||||||||||||||||||
| .map(value -> (JsonValue) value) | ||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||
| schemaMap.put("prefixItems", JsonArray.of(prefixItems)); | ||||||||||||||||||||||||||||
| schemaMap.put("items", JsonBoolean.of(false)); | ||||||||||||||||||||||||||||
| schemaMap.put("minItems", JsonNumber.of((long) items.size())); | ||||||||||||||||||||||||||||
| schemaMap.put("maxItems", JsonNumber.of((long) items.size())); | ||||||||||||||||||||||||||||
| yield JsonObject.of(schemaMap); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| case StringSchema ignored -> primitiveSchema("string"); | ||||||||||||||||||||||||||||
| case NumberSchema ignored -> primitiveSchema("number"); | ||||||||||||||||||||||||||||
| case BooleanSchema ignored -> primitiveSchema("boolean"); | ||||||||||||||||||||||||||||
| case NullSchema ignored -> primitiveSchema("null"); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static JsonObject primitiveSchema(String type) { | ||||||||||||||||||||||||||||
| final var schemaMap = new LinkedHashMap<String, JsonValue>(); | ||||||||||||||||||||||||||||
| schemaMap.put("type", JsonString.of(type)); | ||||||||||||||||||||||||||||
| return JsonObject.of(schemaMap); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static String describeSchema(JsonSchema schema) { | ||||||||||||||||||||||||||||
| return switch (schema) { | ||||||||||||||||||||||||||||
| case ObjectSchema(var properties) -> properties.stream() | ||||||||||||||||||||||||||||
| .map(property -> property.name() + ":" + describeSchema(property.schema())) | ||||||||||||||||||||||||||||
| .collect(Collectors.joining(",", "object{", "}")); | ||||||||||||||||||||||||||||
| case ArraySchema(var items) -> items.stream() | ||||||||||||||||||||||||||||
| .map(ITJsonSchemaExhaustiveTest::describeSchema) | ||||||||||||||||||||||||||||
| .collect(Collectors.joining(",", "array[", "]")); | ||||||||||||||||||||||||||||
| case StringSchema ignored -> "string"; | ||||||||||||||||||||||||||||
| case NumberSchema ignored -> "number"; | ||||||||||||||||||||||||||||
| case BooleanSchema ignored -> "boolean"; | ||||||||||||||||||||||||||||
| case NullSchema ignored -> "null"; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| static final class SchemaArbitraryProvider implements ArbitraryProvider { | ||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||
| public boolean canProvideFor(TypeUsage targetType) { | ||||||||||||||||||||||||||||
| return targetType.isOfType(ITJsonSchemaExhaustiveTest.JsonSchema.class); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||
| public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) { | ||||||||||||||||||||||||||||
| return Set.of(schemaArbitrary(MAX_DEPTH)); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static Arbitrary<JsonSchema> schemaArbitrary(int depth) { | ||||||||||||||||||||||||||||
| final var primitives = Arbitraries.of( | ||||||||||||||||||||||||||||
| new StringSchema(), | ||||||||||||||||||||||||||||
| new NumberSchema(), | ||||||||||||||||||||||||||||
| new BooleanSchema(), | ||||||||||||||||||||||||||||
| new NullSchema() | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| if (depth == 0) { | ||||||||||||||||||||||||||||
| return primitives; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return Arbitraries.oneOf( | ||||||||||||||||||||||||||||
| primitives, | ||||||||||||||||||||||||||||
| objectSchemaArbitrary(depth), | ||||||||||||||||||||||||||||
| arraySchemaArbitrary(depth) | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private static Arbitrary<JsonSchema> objectSchemaArbitrary(int depth) { | ||||||||||||||||||||||||||||
| if (depth == 1) { | ||||||||||||||||||||||||||||
| return Arbitraries.of(new ObjectSchema(List.of())); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| final var childDepth = depth - 1; | ||||||||||||||||||||||||||||
| final var empty = Arbitraries.of(new ObjectSchema(List.of())); | ||||||||||||||||||||||||||||
| final var single = Combinators.combine( | ||||||||||||||||||||||||||||
| Arbitraries.of(PROPERTY_NAMES), | ||||||||||||||||||||||||||||
| schemaArbitrary(childDepth) | ||||||||||||||||||||||||||||
| ).as((name, child) -> new ObjectSchema(List.of(new Property(name, child)))); | ||||||||||||||||||||||||||||
| final var pair = Combinators.combine( | ||||||||||||||||||||||||||||
| Arbitraries.of(PROPERTY_PAIRS), | ||||||||||||||||||||||||||||
| schemaArbitrary(childDepth), | ||||||||||||||||||||||||||||
| schemaArbitrary(childDepth) | ||||||||||||||||||||||||||||
| ).as((names, first, second) -> new ObjectSchema(List.of( | ||||||||||||||||||||||||||||
| new Property(names.getFirst(), first), | ||||||||||||||||||||||||||||
| new Property(names.getLast(), second) | ||||||||||||||||||||||||||||
| ))); | ||||||||||||||||||||||||||||
|
Comment on lines
+254
to
+257
|
||||||||||||||||||||||||||||
| ).as((names, first, second) -> new ObjectSchema(List.of( | |
| new Property(names.getFirst(), first), | |
| new Property(names.getLast(), second) | |
| ))); | |
| ).as((names, first, second) -> { | |
| if (names.size() != 2) { | |
| throw new IllegalArgumentException("Expected names list of size 2, got " + names.size()); | |
| } | |
| return new ObjectSchema(List.of( | |
| new Property(names.get(0), first), | |
| new Property(names.get(1), second) | |
| )); | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P0] Nested schema records declared inside interface do not compile
The helper schema types are declared as records inside the nested JsonSchema interface, but the rest of this test refers to them as if they were top‑level members (new ObjectSchema(...), pattern matching, permits ObjectSchema). Because nested interface members must be referenced with their enclosing type, ObjectSchema and friends need to be named JsonSchema.ObjectSchema (and the permits clause must use the same qualified names). As written the file does not compile, so the test suite cannot run.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cast to
longis repeated for both minItems and maxItems. Consider extracting(long) items.size()into a local variable to reduce duplication and improve readability.