From 00b94262f14b019a3b4b8b0e2cad7a8e0f73dba2 Mon Sep 17 00:00:00 2001 From: Jeffrey Douangpaseuth <11084623+Nephery@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:42:48 -0400 Subject: [PATCH 1/4] add support for Jakarta AssertTrue AssertFalse --- .../generator/impl/AttributeCollector.java | 1 + .../README.md | 1 + .../validation/JakartaValidationModule.java | 32 ++++++- .../jakarta/validation/IntegrationTest.java | 10 +++ .../JakartaValidationModuleTest.java | 89 ++++++++++++++++++- .../validation/integration-test-result.json | 11 +++ 6 files changed, 140 insertions(+), 4 deletions(-) diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java index 5f093774..22a8e803 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java @@ -435,6 +435,7 @@ private boolean isSupportedEnumValue(Object target) { } Class targetType = target.getClass(); return targetType.isPrimitive() + || Boolean.class.isAssignableFrom(targetType) || Number.class.isAssignableFrom(targetType) || CharSequence.class.isAssignableFrom(targetType) || Enum.class.isAssignableFrom(targetType); diff --git a/jsonschema-module-jakarta-validation/README.md b/jsonschema-module-jakarta-validation/README.md index f31acc30..cf1a0d0e 100644 --- a/jsonschema-module-jakarta-validation/README.md +++ b/jsonschema-module-jakarta-validation/README.md @@ -13,6 +13,7 @@ Module for the [jsonschema-generator](../jsonschema-generator) – deriving JSON 7. Populate "pattern" for strings. Based on `@Pattern`/`@Email`, when corresponding `JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS` is being provided in constructor. 8. Populate "minimum"/"exclusiveMinimum" for numbers. Based on `@Min`/`@DecimalMin`/`@Positive`/`@PositiveOrZero`. 9. Populate "maximum"/"exclusiveMaximum" for numbers. Based on `@Max`/`@DecimalMax`/`@Negative`/`@NegativeOrZero`. +10. Populate "enum"/"const" for booleans. Based on `@AssertTrue`/`@AssertFalse`. Schema attributes derived from validation annotations on fields are also applied to their respective getter methods. Schema attributes derived from validation annotations on getter methods are also applied to their associated fields. diff --git a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java index 3bfc2574..5b84c15a 100644 --- a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java +++ b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java @@ -28,6 +28,8 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart; import com.github.victools.jsonschema.generator.SchemaKeyword; import jakarta.validation.Constraint; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.Email; @@ -45,12 +47,13 @@ import jakarta.validation.constraints.Size; import java.lang.annotation.Annotation; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.ToIntBiFunction; @@ -116,8 +119,9 @@ public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) { if (this.options.contains(JakartaValidationOption.NOT_NULLABLE_METHOD_IS_REQUIRED)) { methodConfigPart.withRequiredCheck(this::isRequired); } - Stream.of(DecimalMax.class, DecimalMin.class, Email.class, Max.class, Min.class, Negative.class, NegativeOrZero.class, - NotBlank.class, NotEmpty.class, Null.class, NotNull.class, Pattern.class, Positive.class, PositiveOrZero.class, Size.class) + Stream.of(AssertFalse.class, AssertTrue.class, DecimalMax.class, DecimalMin.class, Email.class, Max.class, Min.class, + Negative.class, NegativeOrZero.class, NotBlank.class, NotEmpty.class, Null.class, NotNull.class, + Pattern.class, Positive.class, PositiveOrZero.class, Size.class) .forEach(annotationType -> builder.withAnnotationInclusionOverride(annotationType, AnnotationInclusion.INCLUDE_AND_INHERIT)); } @@ -137,6 +141,7 @@ private void applyToConfigPart(SchemaGeneratorConfigPart configPart) { configPart.withNumberExclusiveMinimumResolver(this::resolveNumberExclusiveMinimum); configPart.withNumberInclusiveMaximumResolver(this::resolveNumberInclusiveMaximum); configPart.withNumberExclusiveMaximumResolver(this::resolveNumberExclusiveMaximum); + configPart.withEnumResolver(this::resolveEnum); configPart.withInstanceAttributeOverride(this::overrideInstanceAttributes); if (this.options.contains(JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS)) { @@ -499,6 +504,27 @@ protected BigDecimal resolveNumberExclusiveMaximum(MemberScope member) { return null; } + /** + * Look-up the finite list of possible values. + * @param member field/method to determine allowed values for + * @return applicable "const"/"enum" values or null + * @see AssertTrue + * @see AssertFalse + */ + protected List resolveEnum(MemberScope member) { + List values = new ArrayList<>(); + + if (this.getAnnotationFromFieldOrGetter(member, AssertTrue.class, AssertTrue::groups) != null) { + values.add(true); + } + + if (this.getAnnotationFromFieldOrGetter(member, AssertFalse.class, AssertFalse::groups) != null) { + values.add(false); + } + + return values.isEmpty() ? null : values; + } + /** * Implementation of the functional {@code InstanceAttributeOverrideV2} interface. * diff --git a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java index 3a8f4385..14891ae7 100644 --- a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java +++ b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java @@ -24,6 +24,8 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaVersion; import jakarta.validation.Constraint; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.Email; @@ -134,6 +136,14 @@ static class TestClass { @DecimalMin(value = "0", inclusive = false) @DecimalMax(value = "1", inclusive = false) public double exclusiveRangeDouble; + + @AssertTrue + public boolean trueBoolean; + @AssertFalse + public boolean falseBoolean; + @AssertTrue + @AssertFalse + public Object allEnumResolvedObject; } static class Book implements Publication { diff --git a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java index a73997cc..c8cecc89 100644 --- a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java +++ b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java @@ -23,6 +23,8 @@ import com.github.victools.jsonschema.generator.MethodScope; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.Email; @@ -40,6 +42,8 @@ import jakarta.validation.constraints.Size; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -146,6 +150,7 @@ private void verifyCommonConfigurations() { Mockito.verify(this.fieldConfigPart).withNumberExclusiveMinimumResolver(Mockito.any()); Mockito.verify(this.fieldConfigPart).withNumberInclusiveMaximumResolver(Mockito.any()); Mockito.verify(this.fieldConfigPart).withNumberExclusiveMaximumResolver(Mockito.any()); + Mockito.verify(this.fieldConfigPart).withEnumResolver(Mockito.any()); Mockito.verify(this.fieldConfigPart).withInstanceAttributeOverride(Mockito.any(InstanceAttributeOverrideV2.class)); Mockito.verify(this.methodConfigPart).withNullableCheck(Mockito.any()); @@ -158,9 +163,10 @@ private void verifyCommonConfigurations() { Mockito.verify(this.methodConfigPart).withNumberExclusiveMinimumResolver(Mockito.any()); Mockito.verify(this.methodConfigPart).withNumberInclusiveMaximumResolver(Mockito.any()); Mockito.verify(this.methodConfigPart).withNumberExclusiveMaximumResolver(Mockito.any()); + Mockito.verify(this.methodConfigPart).withEnumResolver(Mockito.any()); Mockito.verify(this.methodConfigPart).withInstanceAttributeOverride(Mockito.any(InstanceAttributeOverrideV2.class)); - Mockito.verify(this.configBuilder, Mockito.times(15)) + Mockito.verify(this.configBuilder, Mockito.times(17)) .withAnnotationInclusionOverride(Mockito.any(), Mockito.eq(AnnotationInclusion.INCLUDE_AND_INHERIT)); } @@ -533,6 +539,57 @@ private void testNumberMinMaxResolvers(String fieldName, BigDecimal expectedMinI Assertions.assertEquals(expectedMaxExclusive, maxExclusive); } + static Stream parametersForTestEnumResolvers() { + return Stream.of( + Arguments.of("unannotatedBoolean", new Object[]{}), + Arguments.of("trueBoolean", new Object[]{true}), + Arguments.of("trueOnGetterBoolean", new Object[]{true}), + Arguments.of("falseBoolean", new Object[]{false}), + Arguments.of("falseOnGetterBoolean", new Object[]{false}), + Arguments.of("trueAndFalseBoolean", new Object[]{true, false}), + Arguments.of("trueAndFalseOnGetterBoolean", new Object[]{true, false}) + ); + } + + @ParameterizedTest + @MethodSource("parametersForTestEnumResolvers") + public void testEnumResolversNoValidationGroup(String fieldName, Object[] expectedValues) { + new JakartaValidationModule().applyToConfigBuilder(this.configBuilder); + + this.testEnumResolvers(fieldName, expectedValues); + } + + @ParameterizedTest + @MethodSource("parametersForTestEnumResolvers") + public void testEnumResolversMatchingValidationGroup(String fieldName, Object[] expectedValues) { + new JakartaValidationModule() + .forValidationGroups(Test.class) + .applyToConfigBuilder(this.configBuilder); + + this.testEnumResolvers(fieldName, expectedValues); + } + + @ParameterizedTest + @MethodSource("parametersForTestEnumResolvers") + public void testEnumResolversDifferentValidationGroup(String fieldName, Object[] ignoredExpectedValues) { + new JakartaValidationModule() + .forValidationGroups(Object.class) + .applyToConfigBuilder(this.configBuilder); + + // none of the annotated values are actually expected to be returned + this.testEnumResolvers(fieldName); + } + + private void testEnumResolvers(String fieldName, Object... values) { + TestType testType = new TestType(TestClassForEnums.class); + FieldScope field = testType.getMemberField(fieldName); + + ArgumentCaptor>> enumCaptor = ArgumentCaptor.forClass(ConfigFunction.class); + Mockito.verify(this.fieldConfigPart).withEnumResolver(enumCaptor.capture()); + Collection enumValues = enumCaptor.getValue().apply(field); + Assertions.assertEquals(values.length > 0 ? Arrays.asList(values) : null, enumValues); + } + static Stream parametersForTestValidationGroupSetting() { return Stream.of( Arguments.of("skippedConfiguringGroups", "fieldWithoutValidationGroup", Boolean.TRUE, null), @@ -818,6 +875,36 @@ public Long getNegativeOrZeroOnGetterLong() { } } + private static class TestClassForEnums { + boolean unannotatedBoolean; + @AssertTrue(groups = Test.class) + boolean trueBoolean; + boolean trueOnGetterBoolean; + @AssertFalse(groups = Test.class) + boolean falseBoolean; + boolean falseOnGetterBoolean; + @AssertTrue(groups = Test.class) + @AssertFalse(groups = Test.class) + boolean trueAndFalseBoolean; + boolean trueAndFalseOnGetterBoolean; + + @AssertTrue(groups = Test.class) + public boolean isTrueOnGetterBoolean() { + return trueOnGetterBoolean; + } + + @AssertFalse(groups = Test.class) + public boolean isFalseOnGetterBoolean() { + return falseOnGetterBoolean; + } + + @AssertTrue(groups = Test.class) + @AssertFalse(groups = Test.class) + public boolean isTrueAndFalseOnGetterBoolean() { + return trueAndFalseOnGetterBoolean; + } + } + private static class TestClassForValidationGroups { @Null diff --git a/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json b/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json index fe43abf7..7301c6c1 100644 --- a/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json +++ b/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json @@ -2,11 +2,18 @@ "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "object", "properties": { + "allEnumResolvedObject": { + "enum": [true, false] + }, "exclusiveRangeDouble": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1 }, + "falseBoolean": { + "type": "boolean", + "const": false + }, "inclusiveRangeInt": { "type": "integer", "minimum": 7, @@ -113,6 +120,10 @@ "type": "string", "minLength": 5, "maxLength": 12 + }, + "trueBoolean": { + "type": "boolean", + "const": true } }, "required": ["notBlankText", "notEmptyList", "notEmptyMap", "notEmptyPatternText", "notNullEmail", "notNullList"] From caca6b5742dd65be5d4bf8128aa6ad61657cd03a Mon Sep 17 00:00:00 2001 From: Jeffrey D <11084623+Nephery@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:19:00 -0400 Subject: [PATCH 2/4] Make AssertTrue and AssertFalse mutually exclusive Co-authored-by: Carsten Wickner <11309681+CarstenWickner@users.noreply.github.com> --- .../validation/JakartaValidationModule.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java index 5b84c15a..b3b26153 100644 --- a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java +++ b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java @@ -512,17 +512,15 @@ protected BigDecimal resolveNumberExclusiveMaximum(MemberScope member) { * @see AssertFalse */ protected List resolveEnum(MemberScope member) { - List values = new ArrayList<>(); - + List values; if (this.getAnnotationFromFieldOrGetter(member, AssertTrue.class, AssertTrue::groups) != null) { - values.add(true); - } - - if (this.getAnnotationFromFieldOrGetter(member, AssertFalse.class, AssertFalse::groups) != null) { - values.add(false); + values = Collections.singletonList(true); + } else if (this.getAnnotationFromFieldOrGetter(member, AssertFalse.class, AssertFalse::groups) != null) { + values = Collections.singletonList(false); + } else { + values = null; } - - return values.isEmpty() ? null : values; + return values; } /** From f3e438b5ca847fbc95321c8a18194831d37cdae2 Mon Sep 17 00:00:00 2001 From: Jeffrey Douangpaseuth <11084623+Nephery@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:27:27 -0400 Subject: [PATCH 3/4] fix AssertTrue and AssertFalse tests --- .../jsonschema/module/jakarta/validation/IntegrationTest.java | 3 --- .../jakarta/validation/JakartaValidationModuleTest.java | 4 ++-- .../module/jakarta/validation/integration-test-result.json | 3 --- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java index 14891ae7..ae0b4555 100644 --- a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java +++ b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/IntegrationTest.java @@ -141,9 +141,6 @@ static class TestClass { public boolean trueBoolean; @AssertFalse public boolean falseBoolean; - @AssertTrue - @AssertFalse - public Object allEnumResolvedObject; } static class Book implements Publication { diff --git a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java index c8cecc89..ebd506c5 100644 --- a/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java +++ b/jsonschema-module-jakarta-validation/src/test/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModuleTest.java @@ -546,8 +546,8 @@ static Stream parametersForTestEnumResolvers() { Arguments.of("trueOnGetterBoolean", new Object[]{true}), Arguments.of("falseBoolean", new Object[]{false}), Arguments.of("falseOnGetterBoolean", new Object[]{false}), - Arguments.of("trueAndFalseBoolean", new Object[]{true, false}), - Arguments.of("trueAndFalseOnGetterBoolean", new Object[]{true, false}) + Arguments.of("trueAndFalseBoolean", new Object[]{true}), + Arguments.of("trueAndFalseOnGetterBoolean", new Object[]{true}) ); } diff --git a/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json b/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json index 7301c6c1..7c13df22 100644 --- a/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json +++ b/jsonschema-module-jakarta-validation/src/test/resources/com/github/victools/jsonschema/module/jakarta/validation/integration-test-result.json @@ -2,9 +2,6 @@ "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "object", "properties": { - "allEnumResolvedObject": { - "enum": [true, false] - }, "exclusiveRangeDouble": { "type": "number", "exclusiveMinimum": 0, From 277526a5a1f8f41498c416a02dab4908308da47e Mon Sep 17 00:00:00 2001 From: Jeffrey Douangpaseuth <11084623+Nephery@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:40:49 -0400 Subject: [PATCH 4/4] update jakarta slate-docs for AssertTrue and AssertFalse --- slate-docs/source/includes/_jakarta-validation-module.md | 1 + 1 file changed, 1 insertion(+) diff --git a/slate-docs/source/includes/_jakarta-validation-module.md b/slate-docs/source/includes/_jakarta-validation-module.md index 620fe804..20538ab8 100644 --- a/slate-docs/source/includes/_jakarta-validation-module.md +++ b/slate-docs/source/includes/_jakarta-validation-module.md @@ -24,6 +24,7 @@ SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(Sc 7. Populate "pattern" for strings. Based on `@Pattern`/`@Email`, if `JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS` is being provided (i.e. this is an "opt-in"). 8. Populate "minimum"/"exclusiveMinimum" for numbers. Based on `@Min`/`@DecimalMin`/`@Positive`/`@PositiveOrZero`. 9. Populate "maximum"/"exclusiveMaximum" for numbers. Based on `@Max`/`@DecimalMax`/`@Negative`/`@NegativeOrZero`. +10. Populate "enum"/"const" for booleans. Based on `@AssertTrue`/`@AssertFalse`. Schema attributes derived from validation annotations on fields are also applied to their respective getter methods. Schema attributes derived from validation annotations on getter methods are also applied to their associated fields.