diff --git a/pom.xml b/pom.xml index 98e4f40..8f18ba7 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,12 @@ - + 4.0.0 org.springframework.boot spring-boot-starter-parent - 4.0.1 + 4.0.2 @@ -25,7 +26,7 @@ it.aboutbits spring-boot-toolbox - 2.1.0-RC1 + 2.1.0 it.aboutbits diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java index adc4fc4..14e809f 100644 --- a/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java @@ -14,8 +14,12 @@ import it.aboutbits.springboot.testing.validation.rule.PastRule; import it.aboutbits.springboot.testing.validation.rule.PositiveOrZeroRule; import it.aboutbits.springboot.testing.validation.rule.PositiveRule; +import it.aboutbits.springboot.testing.validation.rule.RepeatedFieldRule; import it.aboutbits.springboot.testing.validation.rule.SizeRule; import it.aboutbits.springboot.testing.validation.rule.ValidBeanRule; +import it.aboutbits.springboot.testing.validation.rule.ValidDateRangeRule; +import it.aboutbits.springboot.testing.validation.rule.ValidNumericRangeRule; +import it.aboutbits.springboot.testing.validation.rule.ValidPasswordRule; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -39,13 +43,17 @@ public abstract class BaseRuleBuilder> implements NotBlankRule, NotEmptyRule, NotNullRule, + NotValidatedRule, NullableRule, PastRule, PositiveOrZeroRule, PositiveRule, - NotValidatedRule, + RepeatedFieldRule, SizeRule, - ValidBeanRule { + ValidBeanRule, + ValidDateRangeRule, + ValidNumericRangeRule, + ValidPasswordRule { @Getter(AccessLevel.PACKAGE) private final List rules = new ArrayList<>(); diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/RepeatedFieldRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/RepeatedFieldRule.java new file mode 100644 index 0000000..a2a069b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/RepeatedFieldRule.java @@ -0,0 +1,46 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.CustomValidationFunction; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.toolbox.validation.annotation.RepeatedField; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; + +@SuppressWarnings("unchecked") +@NullMarked +public interface RepeatedFieldRule> extends ValidationRulesData { + default V repeatedField(String originalField, String repeatedField) { + addValidationFunction( + (Object o) -> { + var isValid = false; + var currentClass = o.getClass(); + + while (currentClass != null) { + var annotations = currentClass.getDeclaredAnnotationsByType(RepeatedField.class); + + isValid = Arrays.stream(annotations).anyMatch( + a -> a.originalField().equals(originalField) && a.repeatedField() + .equals(repeatedField) + ); + + if (isValid) { + break; + } + currentClass = currentClass.getSuperclass(); + } + + return new CustomValidationFunction.Result( + isValid, + "@RepeatedField(originalField = \"%s\", repeatedField = \"%s\") is missing.".formatted( + originalField, + repeatedField + ) + ); + } + ); + + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidDateRangeRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidDateRangeRule.java new file mode 100644 index 0000000..b26baf1 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidDateRangeRule.java @@ -0,0 +1,82 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.CustomValidationFunction; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.toolbox.validation.annotation.ValidDateRange; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; + +@SuppressWarnings("unchecked") +@NullMarked +public interface ValidDateRangeRule> extends ValidationRulesData { + default V validDateRange(String fromDateField, String toDateField) { + addValidationFunction( + (Object o) -> { + var isValid = false; + var currentClass = o.getClass(); + + while (currentClass != null) { + var annotations = currentClass.getDeclaredAnnotationsByType(ValidDateRange.class); + + isValid = Arrays.stream(annotations).anyMatch( + a -> a.fromDateField().equals(fromDateField) && a.toDateField() + .equals(toDateField) + ); + + if (isValid) { + break; + } + currentClass = currentClass.getSuperclass(); + } + + return new CustomValidationFunction.Result( + isValid, + "@ValidDateRange(fromDateField = \"%s\", toDateField = \"%s\") is missing.".formatted( + fromDateField, + toDateField + ) + ); + } + ); + + return (V) this; + } + + default V validDateRange( + String fromDateField, + String toDateField, + boolean allowEmptyRange + ) { + addValidationFunction( + (Object o) -> { + var isValid = false; + var currentClass = o.getClass(); + + while (currentClass != null) { + var annotations = currentClass.getDeclaredAnnotationsByType(ValidDateRange.class); + + isValid = Arrays.stream(annotations).anyMatch( + a -> a.fromDateField().equals(fromDateField) + && a.toDateField().equals(toDateField) + && a.allowEmptyRange() == allowEmptyRange + ); + + if (isValid) { + break; + } + currentClass = currentClass.getSuperclass(); + } + + return new CustomValidationFunction.Result( + isValid, + "@ValidDateRange(fromDateField = \"%s\", toDateField = \"%s\", allowEmptyRange = %s) is missing." + .formatted(fromDateField, toDateField, allowEmptyRange) + ); + } + ); + + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidNumericRangeRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidNumericRangeRule.java new file mode 100644 index 0000000..9ed5a52 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidNumericRangeRule.java @@ -0,0 +1,82 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.CustomValidationFunction; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.toolbox.validation.annotation.ValidNumericRange; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; + +@SuppressWarnings("unchecked") +@NullMarked +public interface ValidNumericRangeRule> extends ValidationRulesData { + default V validNumericRange(String lowerBoundField, String upperBoundField) { + addValidationFunction( + (Object o) -> { + var isValid = false; + var currentClass = o.getClass(); + + while (currentClass != null) { + var annotations = currentClass.getDeclaredAnnotationsByType(ValidNumericRange.class); + + isValid = Arrays.stream(annotations).anyMatch( + a -> a.lowerBoundField().equals(lowerBoundField) && a.upperBoundField() + .equals(upperBoundField) + ); + + if (isValid) { + break; + } + currentClass = currentClass.getSuperclass(); + } + + return new CustomValidationFunction.Result( + isValid, + "@ValidNumericRange(lowerBoundField = \"%s\", upperBoundField = \"%s\") is missing.".formatted( + lowerBoundField, + upperBoundField + ) + ); + } + ); + + return (V) this; + } + + default V validNumericRange( + String lowerBoundField, + String upperBoundField, + boolean allowEqualValues + ) { + addValidationFunction( + (Object o) -> { + var isValid = false; + var currentClass = o.getClass(); + + while (currentClass != null) { + var annotations = currentClass.getDeclaredAnnotationsByType(ValidNumericRange.class); + + isValid = Arrays.stream(annotations).anyMatch( + a -> a.lowerBoundField().equals(lowerBoundField) + && a.upperBoundField().equals(upperBoundField) + && a.allowEqualValues() == allowEqualValues + ); + + if (isValid) { + break; + } + currentClass = currentClass.getSuperclass(); + } + + return new CustomValidationFunction.Result( + isValid, + "@ValidNumericRange(lowerBoundField = \"%s\", upperBoundField = \"%s\", allowEqualValues = %s) is missing." + .formatted(lowerBoundField, upperBoundField, allowEqualValues) + ); + } + ); + + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidPasswordRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidPasswordRule.java new file mode 100644 index 0000000..aad78c7 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidPasswordRule.java @@ -0,0 +1,22 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.LongerThanValueSource; +import it.aboutbits.springboot.testing.validation.source.ShorterThanValueSource; +import org.jspecify.annotations.NullMarked; + +@SuppressWarnings("unchecked") +@NullMarked +public interface ValidPasswordRule> extends ValidationRulesData { + default V validPassword(String property, long minLength, long maxLength) { + addRule( + new Rule(property, LongerThanValueSource.class, maxLength) + ); + addRule( + new Rule(property, ShorterThanValueSource.class, minLength) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/LongerThanValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/LongerThanValueSource.java new file mode 100644 index 0000000..d4239dc --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/LongerThanValueSource.java @@ -0,0 +1,61 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; +import org.jspecify.annotations.NullMarked; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.Stream; + +@NullMarked +public class LongerThanValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + private static final Random RANDOM = new Random(); + + static { + TYPE_SOURCES.put(String.class, LongerThanValueSource::getStream); + } + + @SuppressWarnings("unused") + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } + + private static Stream getStream(Object[] args) { + var length = Long.valueOf((long) args[0]).intValue(); + var minLength = length + 1; + + var maxLength = length + 1024; + + if (minLength == maxLength) { + return Stream.of(generateRandomString(minLength)); + } + + var randomLength = RANDOM.nextInt(minLength, maxLength); + + return Stream.of( + generateRandomString(minLength), + generateRandomString(maxLength), + generateRandomString(randomLength) + ).distinct(); + } + + private static String generateRandomString(int length) { + return RANDOM.ints(length, 32, 127) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/ShorterThanValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/ShorterThanValueSource.java new file mode 100644 index 0000000..2ef0a08 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/ShorterThanValueSource.java @@ -0,0 +1,64 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; +import org.jspecify.annotations.NullMarked; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.Stream; + +@NullMarked +public class ShorterThanValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + private static final Random RANDOM = new Random(); + + static { + TYPE_SOURCES.put(String.class, ShorterThanValueSource::getStream); + } + + @SuppressWarnings("unused") + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } + + private static Stream getStream(Object[] args) { + var length = Long.valueOf((long) args[0]).intValue(); + if (length <= 0) { + return Stream.empty(); + } + + var minLength = 0; + var maxLength = length - 1; + + if (minLength == maxLength) { + return Stream.of(""); + } + + var randomLength = RANDOM.nextInt(minLength, maxLength); + + return Stream.of( + generateRandomString(minLength), + generateRandomString(maxLength), + generateRandomString(randomLength) + ).distinct(); + } + + private static String generateRandomString(int length) { + return RANDOM.ints(length, 32, 127) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java b/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java index db4a08d..4eb10b2 100644 --- a/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java +++ b/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java @@ -4,6 +4,7 @@ import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; import it.aboutbits.springboot.testing.validation.core.BaseValidationAssert; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import it.aboutbits.springboot.toolbox.validation.annotation.ValidPassword; import jakarta.validation.Valid; import jakarta.validation.constraints.Future; import jakarta.validation.constraints.Max; @@ -183,6 +184,10 @@ public record SomeValidParameter( // Nullable @Nullable Object nullable, + // ValidPassword + @ValidPassword + String password, + // Not validated Object notValidated ) { @@ -325,6 +330,9 @@ void testWithBeanValidation() { // Nullable .nullable("nullable") + // ValidPassword + .validPassword("password", 8, 50) + // Not validated .notValidated("notValidated") .isCompliant(); @@ -465,6 +473,9 @@ void invalidParameter_shouldFail() { // Nullable .nullable("nullable") + // ValidPassword + .validPassword("password", 8, 50) + // Not validated .notValidated("notValidated") .isCompliant()); @@ -606,6 +617,9 @@ void propertyMissingRule_shouldFail() { // Nullable .nullable("nullable") + // ValidPassword + .validPassword("password", 8, 50) + // Not validated .notValidated("notValidated") .isCompliant()); @@ -905,6 +919,9 @@ private static SomeValidParameter getSomeValidParameter() { // Nullable null, + // ValidPassword + "password123", + // Not validated null );