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..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 @@ -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,25 @@ 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; + if (this.getAnnotationFromFieldOrGetter(member, AssertTrue.class, AssertTrue::groups) != null) { + values = Collections.singletonList(true); + } else if (this.getAnnotationFromFieldOrGetter(member, AssertFalse.class, AssertFalse::groups) != null) { + values = Collections.singletonList(false); + } else { + values = null; + } + return 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..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 @@ -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,11 @@ static class TestClass { @DecimalMin(value = "0", inclusive = false) @DecimalMax(value = "1", inclusive = false) public double exclusiveRangeDouble; + + @AssertTrue + public boolean trueBoolean; + @AssertFalse + public boolean falseBoolean; } 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..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 @@ -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}), + Arguments.of("trueAndFalseOnGetterBoolean", new Object[]{true}) + ); + } + + @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..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 @@ -7,6 +7,10 @@ "exclusiveMinimum": 0, "exclusiveMaximum": 1 }, + "falseBoolean": { + "type": "boolean", + "const": false + }, "inclusiveRangeInt": { "type": "integer", "minimum": 7, @@ -113,6 +117,10 @@ "type": "string", "minLength": 5, "maxLength": 12 + }, + "trueBoolean": { + "type": "boolean", + "const": true } }, "required": ["notBlankText", "notEmptyList", "notEmptyMap", "notEmptyPatternText", "notNullEmail", "notNullList"] 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.