> NON_BEAN_TYPES = new HashSet<>(
+ Set.of(
+ CustomType.class
+ )
+ );
+
+ private Object parameterUnderTest;
+
+ @Setter(AccessLevel.PRIVATE)
+ private Consumer> functionToCallWithParameter = null;
+
+ /**
+ * Configure a class that is not required to have a @Valid annotation. Sub-structures are assumed to always require @Valid.
+ *
+ * @param type The class to whitelist.
+ */
+ public static void registerNonBeanType(Class> type) {
+ NON_BEAN_TYPES.add(type);
+ }
+
+ public CallBuilder of(@NonNull P parameterUnderTest) {
+ this.parameterUnderTest = parameterUnderTest;
+ ruleBuilder.setTriggerValidation(this::assertValidation);
+ return new CallBuilder<>(this);
+ }
+
+ @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+ public static final class CallBuilder, P> {
+ private final BaseValidationAssert parent;
+
+ public R calling(@NonNull Consumer functionToCallWithParameter) {
+ parent.setFunctionToCallWithParameter(functionToCallWithParameter);
+ return parent.ruleBuilder;
+ }
+
+ public R usingBeanValidation() {
+ return parent.ruleBuilder;
+ }
+
+ @SuppressWarnings("unchecked")
+ public R calling(
+ @NonNull BiConsumer functionToCallWithParameter,
+ @NonNull ID id
+ ) {
+ parent.setFunctionToCallWithParameter(
+ p -> functionToCallWithParameter.accept(id, (P) p)
+ );
+ return parent.ruleBuilder;
+ }
+ }
+
+ private void assertValidation() {
+ new RuleValidator<>().assertValidation(
+ new RuleValidator.AssertionParameter<>(
+ parameterUnderTest,
+ functionToCallWithParameter,
+ ruleBuilder.getRules(),
+ NON_BEAN_TYPES
+ )
+ );
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/Rule.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/Rule.java
new file mode 100644
index 0000000..48cdbdf
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/Rule.java
@@ -0,0 +1,52 @@
+package it.aboutbits.springboot.testing.validation.core;
+
+import it.aboutbits.springboot.testing.validation.source.InertValueSource;
+import lombok.AccessLevel;
+import lombok.Getter;
+
+public final class Rule {
+ @Getter(AccessLevel.PACKAGE)
+ private final String property;
+
+ @Getter(AccessLevel.PACKAGE)
+ private final Class extends ValueSource> valueSource;
+
+ @Getter(AccessLevel.PACKAGE)
+ private final Object[] args;
+
+ @Getter(AccessLevel.PACKAGE)
+ private boolean requireValid = false;
+
+ @Getter(AccessLevel.PACKAGE)
+ private boolean requireNullable = false;
+
+ public Rule(String property, Class extends ValueSource> valueSource, Object... args) {
+ checkPropertyName(property);
+
+ this.property = property;
+ this.valueSource = valueSource;
+ this.args = args;
+ }
+
+ public static Rule validAnnotated(String property) {
+ checkPropertyName(property);
+
+ var rule = new Rule(property, InertValueSource.class);
+ rule.requireValid = true;
+ return rule;
+ }
+
+ public static Rule nullableAnnotated(String property) {
+ checkPropertyName(property);
+
+ var rule = new Rule(property, InertValueSource.class);
+ rule.requireNullable = true;
+ return rule;
+ }
+
+ private static void checkPropertyName(String property) {
+ if (property.contains(".")) {
+ throw new IllegalArgumentException("Referencing sub-objects using dot notation is not supported.");
+ }
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidationException.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidationException.java
new file mode 100644
index 0000000..3260b67
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidationException.java
@@ -0,0 +1,11 @@
+package it.aboutbits.springboot.testing.validation.core;
+
+class RuleValidationException extends RuntimeException {
+ RuleValidationException(String message) {
+ super(message);
+ }
+
+ RuleValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java
new file mode 100644
index 0000000..48bd79c
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java
@@ -0,0 +1,317 @@
+package it.aboutbits.springboot.testing.validation.core;
+
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Valid;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import lombok.NonNull;
+import lombok.SneakyThrows;
+import org.springframework.lang.Nullable;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * The main idea of the RuleValidator is to take in a valid parameter and then mutate it.
+ * Each mutation changes exactly one property`s value.
+ * Then we check if an exception is raised.
+ * We repeat this for each defined rule.
+ *
+ * Additionally, we also check if @Valid or @Nullable is present where required according to the rules.
+ * Also, we enforce that all properties must have at least one rule (with a rule existing that says "no-rule").
+ *
+ *
+ * @parameterUnderTest A valid parameter that we can use as the basis for our mutations. Validation for the unmodified parameter MUST succeed.
+ * @functionToCallWithParameter Optional. Instead of directly using bean validation, we can also validate a real function call. This makes sure the parameter is actually annotated with @Valid as well and that the class is using @Validated.
+ * @rules The list of rules to validate.
+ * @nonBeanTypes This is a whitelist that holds classes that don't implicitly require @Valid. We assume that @Valid is required
+ * for all substructures.
+ */
+final class RuleValidator {
+ private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory();
+
+ record AssertionParameter
(
+ @NonNull
+ P parameterUnderTest,
+ @Nullable
+ Consumer> functionToCallWithParameter,
+ @NonNull
+ List rules,
+ @NonNull
+ Set> nonBeanTypes
+ ) {
+ }
+
+ void assertValidation(AssertionParameter assertionParameter) {
+ var parameterUnderTest = assertionParameter.parameterUnderTest();
+ @SuppressWarnings("unchecked")
+ var functionToCallWithParameter = (Consumer
) assertionParameter.functionToCallWithParameter();
+ var rules = assertionParameter.rules();
+ var nonBeanTypes = assertionParameter.nonBeanTypes();
+
+ assertThat(rules)
+ .withFailMessage("Validation failed: no rules were defined.")
+ .isNotEmpty();
+
+ var validator = VALIDATOR_FACTORY.getValidator();
+
+ var propertiesWithRules = getPropertyNamesThatHaveRules(rules);
+
+ assertThatValidationIsCompliantForEachProperty(
+ rules,
+ parameterUnderTest,
+ functionToCallWithParameter,
+ validator
+ );
+
+ assertThatValidAnnotationsArePresent(rules, propertiesWithRules, parameterUnderTest);
+
+ assertThatNullableAnnotationsArePresent(rules, propertiesWithRules, parameterUnderTest);
+
+ assertThatAllPropertiesHaveRules(parameterUnderTest, propertiesWithRules);
+
+ checkIfNestedValidationIsEnabledForNestedRecords(parameterUnderTest, nonBeanTypes);
+ }
+
+ private static HashSet getPropertyNamesThatHaveRules(List rules) {
+ // Create a set to keep track of properties that have validation rules
+ var propertiesWithRules = new HashSet();
+ for (var rule : rules) {
+ propertiesWithRules.add(rule.getProperty());
+ }
+ return propertiesWithRules;
+ }
+
+ private static void assertThatValidationIsCompliantForEachProperty(
+ List rules,
+ P parameterUnderTest,
+ Consumer functionToCallWithParameter,
+ Validator validator
+ ) {
+ // Iterate through the rules and validate each property
+ for (var rule : rules) {
+ var values = getValues(rule, parameterUnderTest);
+ values.forEach(alteredValue -> {
+ // Create a copy of the original object
+ var copy = createCopyWithAlteredProperty(parameterUnderTest, rule.getProperty(), alteredValue);
+
+ if (functionToCallWithParameter != null) {
+ assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
+ () -> functionToCallWithParameter.accept(copy)
+ ).withMessageContaining(rule.getProperty());
+ } else {
+
+ // Use Bean Validation to validate the copy
+ var violations = validator.validate(copy);
+
+ // Check if there are any violations
+ assertThat(violations)
+ .withFailMessage(
+ "More than one property failed to validate during mutation. The supplied parameter is possibly contains invalid values.")
+ .hasSizeLessThan(2);
+
+ assertThat(violations)
+ .withFailMessage("Validation failed for property: " + rule.getProperty() + " [" + alteredValue + "]")
+ .hasSize(1);
+ }
+ });
+ }
+ }
+
+ private static
void assertThatValidAnnotationsArePresent(
+ List rules,
+ HashSet propertiesWithRules,
+ P parameterUnderTest
+ ) {
+ var propertiesWithValid = rules.stream()
+ .filter(Rule::isRequireValid)
+ .map(Rule::getProperty)
+ .collect(Collectors.toSet());
+
+ for (var property : propertiesWithValid) {
+ propertiesWithRules.add(property);
+
+ assertThat(hasValidAnnotation(property, parameterUnderTest))
+ .withFailMessage("Missing @Valid annotation for property: " + property)
+ .isTrue();
+ }
+ }
+
+ private static void assertThatNullableAnnotationsArePresent(
+ List rules,
+ HashSet propertiesWithRules,
+ P parameterUnderTest
+ ) {
+ var propertiesWithNullable = rules.stream()
+ .filter(Rule::isRequireNullable)
+ .map(Rule::getProperty)
+ .collect(Collectors.toSet());
+
+ for (var property : propertiesWithNullable) {
+ propertiesWithRules.add(property);
+
+ assertThat(hasNullableAnnotation(property, parameterUnderTest))
+ .withFailMessage("Missing @Nullable annotation for property: " + property + ". Note: This does not work with `org.jetbrains.annotations.Nullable` because of their retention policy. Use `org.springframework.lang.Nullable` or `jakarta.annotation.Nullable` instead.")
+ .isTrue();
+ }
+ }
+
+ private static void assertThatAllPropertiesHaveRules(
+ P parameterUnderTest,
+ HashSet propertiesWithRules
+ ) {
+ // Check if all properties have rules (you can also handle this differently based on your needs)
+ var allProperties = getAllPropertiesOf(parameterUnderTest);
+
+ var missingProperties = new HashSet<>(allProperties);
+ missingProperties.removeAll(propertiesWithRules);
+
+ assertThat(missingProperties)
+ .withFailMessage("Not all properties have validation rules: " + "[" + String.join(
+ ", ",
+ missingProperties
+ ) + "]")
+ .isEmpty();
+ }
+
+ private static void checkIfNestedValidationIsEnabledForNestedRecords(
+ P parameterUnderTest,
+ Set> nonBeanTypes
+ ) {
+ var clazz = parameterUnderTest.getClass();
+
+ for (var field : clazz.getDeclaredFields()) {
+ var isRecord = Record.class.isAssignableFrom(field.getType());
+ if (isRecord) {
+ var isBeanType = nonBeanTypes.stream().noneMatch(type -> type.isAssignableFrom(field.getType()));
+
+ if (isBeanType) {
+ var maybeAnnotation = field.getAnnotation(Valid.class);
+ assertThat(maybeAnnotation).withFailMessage("Missing @Valid annotation for property: " + field.getName() + ". Note: This is implicitly assumed for nested records. You can configure `BaseValidationAssert.registerNonBeanType` to add exceptions.")
+ .isNotNull();
+ }
+ }
+ }
+ }
+
+ @NonNull
+ @SneakyThrows(ReflectiveOperationException.class)
+ private static Stream> getValues(Rule rule, P parameterUnderTest) {
+ var source = (ValueSource) rule.getValueSource().getDeclaredConstructors()[0].newInstance();
+
+ return source.values(getPropertyType(rule.getProperty(), parameterUnderTest), rule.getArgs());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T createCopyWithAlteredProperty(T original, String property, Object alteredValue) {
+ try {
+ // Get the class of the original object
+ var clazz = original.getClass();
+
+ // Get all the declared fields of the class
+ var fields = clazz.getDeclaredFields();
+
+ // Create an array to hold the values of the original object's properties
+ var propertyValues = new Object[fields.length];
+
+ // Get the types of the fields
+ var parameterTypes = new Class>[fields.length];
+
+ // Populate the array with the current property values
+ for (var i = 0; i < fields.length; i++) {
+ fields[i].setAccessible(true);
+ propertyValues[i] = fields[i].get(original);
+ parameterTypes[i] = fields[i].getType();
+ }
+
+ // Get the constructor that accepts all properties as arguments
+ var constructor = clazz.getDeclaredConstructor(parameterTypes);
+ constructor.setAccessible(true);
+
+ // Create an array to hold the new property values
+ var newPropertyValues = new Object[propertyValues.length];
+
+ // Find the index of the property to be altered
+ var propertyIndex = -1;
+ for (var i = 0; i < fields.length; i++) {
+ if (fields[i].getName().equals(property)) {
+ propertyIndex = i;
+ break;
+ }
+ }
+
+ // Replace the value of the altered property
+ newPropertyValues[propertyIndex] = alteredValue;
+
+ // Copy the other property values
+ for (var i = 0; i < propertyValues.length; i++) {
+ if (i != propertyIndex) {
+ newPropertyValues[i] = propertyValues[i];
+ }
+ }
+
+ // Create a copy with the altered property value
+ return (T) constructor.newInstance(newPropertyValues);
+ } catch (NoSuchMethodException e) {
+ throw new RuleValidationException(
+ "Error creating copy with altered property. Maybe there is no all-args-constructor?",
+ e
+ );
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
+ throw new RuleValidationException("Error creating copy with altered property: " + property, e);
+ }
+ }
+
+ private static Set getAllPropertiesOf(T object) {
+ var clazz = object.getClass();
+
+ var properties = new HashSet();
+ for (var field : clazz.getDeclaredFields()) {
+ properties.add(field.getName());
+ }
+
+ return properties;
+ }
+
+ private static Class> getPropertyType(String propertyName, Object object) {
+ var field = getFieldOrFail(propertyName, object);
+
+ return field.getType();
+ }
+
+ private static boolean hasValidAnnotation(String propertyName, T object) {
+ var field = getFieldOrFail(propertyName, object);
+
+ return field.getAnnotation(Valid.class) != null;
+ }
+
+ private static boolean hasNullableAnnotation(String propertyName, Object object) {
+ var field = getFieldOrFail(propertyName, object);
+
+ return field.getAnnotation(org.springframework.lang.Nullable.class) != null
+ || field.getAnnotation(jakarta.annotation.Nullable.class) != null;
+ }
+
+ @NonNull
+ private static Field getFieldOrFail(String propertyName, Object object) {
+ var clazz = object.getClass();
+
+ Field field = null;
+ try {
+ field = clazz.getDeclaredField(propertyName);
+ } catch (NoSuchFieldException e) {
+ throw new RuleValidationException("Property does not exist: " + propertyName, e);
+ }
+ return field;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/ValidationRulesData.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValidationRulesData.java
new file mode 100644
index 0000000..2af0435
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValidationRulesData.java
@@ -0,0 +1,7 @@
+package it.aboutbits.springboot.testing.validation.core;
+
+import lombok.NonNull;
+
+public interface ValidationRulesData {
+ void addRule(@NonNull Rule rule);
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/ValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValueSource.java
new file mode 100644
index 0000000..290191f
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValueSource.java
@@ -0,0 +1,7 @@
+package it.aboutbits.springboot.testing.validation.core;
+
+import java.util.stream.Stream;
+
+public interface ValueSource {
+ Stream values(Class propertyClass, Object... args);
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/BetweenRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/BetweenRule.java
new file mode 100644
index 0000000..285cfe3
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/BetweenRule.java
@@ -0,0 +1,21 @@
+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.BiggerThanValueSource;
+import it.aboutbits.springboot.testing.validation.source.LessThanValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface BetweenRule> extends ValidationRulesData {
+ default V between(@NonNull String property, long min, long max) {
+ addRule(
+ new Rule(property, BiggerThanValueSource.class, max)
+ );
+ addRule(
+ new Rule(property, LessThanValueSource.class, min)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/FutureRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/FutureRule.java
new file mode 100644
index 0000000..7da2a2d
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/FutureRule.java
@@ -0,0 +1,17 @@
+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.PastValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface FutureRule> extends ValidationRulesData {
+ default V future(@NonNull String property) {
+ addRule(
+ new Rule(property, PastValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/MaxRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MaxRule.java
new file mode 100644
index 0000000..4858067
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MaxRule.java
@@ -0,0 +1,17 @@
+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.BiggerThanValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface MaxRule> extends ValidationRulesData {
+ default V max(@NonNull String property, long max) {
+ addRule(
+ new Rule(property, BiggerThanValueSource.class, max)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/MinRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MinRule.java
new file mode 100644
index 0000000..5484f08
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MinRule.java
@@ -0,0 +1,17 @@
+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.LessThanValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface MinRule> extends ValidationRulesData {
+ default V min(@NonNull String property, long min) {
+ addRule(
+ new Rule(property, LessThanValueSource.class, min)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeOrZeroRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeOrZeroRule.java
new file mode 100644
index 0000000..4852867
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeOrZeroRule.java
@@ -0,0 +1,17 @@
+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.BiggerThanValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface NegativeOrZeroRule> extends ValidationRulesData {
+ default V negativeOrZero(@NonNull String property) {
+ addRule(
+ new Rule(property, BiggerThanValueSource.class, 0L)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeRule.java
new file mode 100644
index 0000000..e420f2e
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeRule.java
@@ -0,0 +1,21 @@
+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.BiggerThanValueSource;
+import it.aboutbits.springboot.testing.validation.source.ZeroValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface NegativeRule> extends ValidationRulesData {
+ default V negative(@NonNull String property) {
+ addRule(
+ new Rule(property, BiggerThanValueSource.class, 0L)
+ );
+ addRule(
+ new Rule(property, ZeroValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotBlankRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotBlankRule.java
new file mode 100644
index 0000000..052cbb3
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotBlankRule.java
@@ -0,0 +1,17 @@
+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.BlankValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface NotBlankRule> extends ValidationRulesData {
+ default V notBlank(@NonNull String property) {
+ addRule(
+ new Rule(property, BlankValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotEmptyRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotEmptyRule.java
new file mode 100644
index 0000000..53ba30d
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotEmptyRule.java
@@ -0,0 +1,17 @@
+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.EmptyValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface NotEmptyRule> extends ValidationRulesData {
+ default V notEmpty(@NonNull String property) {
+ addRule(
+ new Rule(property, EmptyValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotNullRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotNullRule.java
new file mode 100644
index 0000000..5346ae9
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotNullRule.java
@@ -0,0 +1,17 @@
+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.NullValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface NotNullRule> extends ValidationRulesData {
+ default V notNull(@NonNull String property) {
+ addRule(
+ new Rule(property, NullValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotValidatedRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotValidatedRule.java
new file mode 100644
index 0000000..3e00e20
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotValidatedRule.java
@@ -0,0 +1,17 @@
+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.InertValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface NotValidatedRule> extends ValidationRulesData {
+ default V notValidated(@NonNull String property) {
+ addRule(
+ new Rule(property, InertValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NullableRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NullableRule.java
new file mode 100644
index 0000000..69a33c4
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NullableRule.java
@@ -0,0 +1,16 @@
+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 lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface NullableRule> extends ValidationRulesData {
+ default V nullable(@NonNull String property) {
+ addRule(
+ Rule.nullableAnnotated(property)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/PastRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PastRule.java
new file mode 100644
index 0000000..b61dd7d
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PastRule.java
@@ -0,0 +1,17 @@
+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.FutureValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface PastRule> extends ValidationRulesData {
+ default V past(@NonNull String property) {
+ addRule(
+ new Rule(property, FutureValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveOrZeroRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveOrZeroRule.java
new file mode 100644
index 0000000..ba9a3fe
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveOrZeroRule.java
@@ -0,0 +1,17 @@
+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.LessThanValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface PositiveOrZeroRule> extends ValidationRulesData {
+ default V positiveOrZero(@NonNull String property) {
+ addRule(
+ new Rule(property, LessThanValueSource.class, 0L)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveRule.java
new file mode 100644
index 0000000..bf9b25b
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveRule.java
@@ -0,0 +1,21 @@
+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.LessThanValueSource;
+import it.aboutbits.springboot.testing.validation.source.ZeroValueSource;
+import lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface PositiveRule> extends ValidationRulesData {
+ default V positive(@NonNull String property) {
+ addRule(
+ new Rule(property, LessThanValueSource.class, 0L)
+ );
+ addRule(
+ new Rule(property, ZeroValueSource.class)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidBeanRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidBeanRule.java
new file mode 100644
index 0000000..7c3e7ae
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidBeanRule.java
@@ -0,0 +1,16 @@
+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 lombok.NonNull;
+
+@SuppressWarnings("unchecked")
+public interface ValidBeanRule> extends ValidationRulesData {
+ default V validBean(@NonNull String property) {
+ addRule(
+ Rule.validAnnotated(property)
+ );
+ return (V) this;
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/BiggerThanValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/BiggerThanValueSource.java
new file mode 100644
index 0000000..4343980
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/BiggerThanValueSource.java
@@ -0,0 +1,117 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
+import lombok.NonNull;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class BiggerThanValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+ private static final Random RANDOM = new Random();
+
+ static {
+ TYPE_SOURCES.put(Integer.class, BiggerThanValueSource::getIntegerStream);
+ TYPE_SOURCES.put(int.class, BiggerThanValueSource::getIntegerStream);
+
+ TYPE_SOURCES.put(Long.class, BiggerThanValueSource::getLongStream);
+ TYPE_SOURCES.put(long.class, BiggerThanValueSource::getLongStream);
+
+ TYPE_SOURCES.put(Float.class, BiggerThanValueSource::getFloatStream);
+ TYPE_SOURCES.put(float.class, BiggerThanValueSource::getFloatStream);
+
+ TYPE_SOURCES.put(Double.class, BiggerThanValueSource::getDoubleStream);
+ TYPE_SOURCES.put(double.class, BiggerThanValueSource::getDoubleStream);
+
+ TYPE_SOURCES.put(BigDecimal.class, BiggerThanValueSource::getBigDecimalStream);
+ TYPE_SOURCES.put(ScaledBigDecimal.class, BiggerThanValueSource::getScaledBigDecimalStream);
+ }
+
+ 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!");
+ }
+
+ @NonNull
+ private static Stream getIntegerStream(Object[] args) {
+ var minValue = Long.valueOf((long) args[0]).intValue() + 1;
+ var maxValue = Integer.MAX_VALUE;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.ints(minValue, maxValue).limit(1).boxed()
+ );
+ }
+
+ @NonNull
+ private static Stream getLongStream(Object[] args) {
+ var minValue = (long) args[0] + 1;
+ var maxValue = Long.MAX_VALUE;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.longs(minValue, maxValue).limit(1).boxed()
+ );
+ }
+
+ @NonNull
+ private static Stream getFloatStream(Object[] args) {
+ var minValue = Long.valueOf((long) args[0]).floatValue() + 0.1f;
+ var maxValue = Float.MAX_VALUE;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.doubles(minValue, maxValue).limit(1).boxed().map(
+ Double::floatValue
+ )
+ );
+ }
+
+ @NonNull
+ private static Stream getDoubleStream(Object[] args) {
+ var minValue = Long.valueOf((long) args[0]).doubleValue() + 0.1d;
+ var maxValue = Double.MAX_VALUE;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.doubles(minValue, maxValue).limit(1).boxed()
+ );
+ }
+
+ @NonNull
+ private static Stream getScaledBigDecimalStream(Object[] args) {
+ var minValue = Long.valueOf((long) args[0]).doubleValue() + 0.1d;
+ var maxValue = Double.MAX_VALUE;
+
+ return Stream.concat(
+ Stream.of(ScaledBigDecimal.valueOf(minValue), ScaledBigDecimal.valueOf(maxValue)),
+ RANDOM.doubles(minValue, maxValue).limit(1).boxed().map(ScaledBigDecimal::valueOf)
+ );
+ }
+
+ @NonNull
+ private static Stream getBigDecimalStream(Object[] args) {
+ var minValue = Long.valueOf((long) args[0]).doubleValue() + 0.1d;
+ var maxValue = Double.MAX_VALUE;
+
+ return Stream.concat(
+ Stream.of(BigDecimal.valueOf(minValue), BigDecimal.valueOf(maxValue)),
+ RANDOM.doubles(minValue, maxValue).limit(1).boxed().map(BigDecimal::valueOf)
+ );
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/BlankValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/BlankValueSource.java
new file mode 100644
index 0000000..035d6d4
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/BlankValueSource.java
@@ -0,0 +1,34 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class BlankValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+
+ static {
+ TYPE_SOURCES.put(
+ String.class,
+ (Object[] args) -> Stream.of("", " ", " ", "\t", "\r", "\n", "\r\n")
+ );
+ }
+
+ 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!");
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/EmptyValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/EmptyValueSource.java
new file mode 100644
index 0000000..318c13d
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/EmptyValueSource.java
@@ -0,0 +1,46 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class EmptyValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+
+ static {
+ TYPE_SOURCES.put(
+ String.class,
+ (Object[] args) -> Stream.of("")
+ );
+ TYPE_SOURCES.put(
+ Set.class,
+ (Object[] args) -> Stream.of(new HashSet<>())
+ );
+ TYPE_SOURCES.put(
+ List.class,
+ (Object[] args) -> Stream.of(new ArrayList<>())
+ );
+ }
+
+ 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!");
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/FutureValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/FutureValueSource.java
new file mode 100644
index 0000000..2441a06
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/FutureValueSource.java
@@ -0,0 +1,58 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class FutureValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+
+ static {
+ TYPE_SOURCES.put(LocalDate.class, FutureValueSource::getLocalDateStream);
+ TYPE_SOURCES.put(LocalDateTime.class, FutureValueSource::getLocalDatetimeStream);
+ TYPE_SOURCES.put(OffsetDateTime.class, FutureValueSource::getOffsetDateTimeStream);
+ }
+
+ 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 getLocalDateStream(Object[] args) {
+ var currentDate = LocalDate.now();
+ var largestDate = LocalDate.MAX;
+
+ return Stream.of(currentDate.plusDays(1), largestDate);
+ }
+
+ private static Stream getLocalDatetimeStream(Object[] args) {
+ var currentDateTime = LocalDateTime.now();
+ var largestDateTime = LocalDateTime.MAX;
+
+ return Stream.of(currentDateTime.plusSeconds(1), largestDateTime);
+ }
+
+ private static Stream getOffsetDateTimeStream(Object[] args) {
+ var currentOffsetDateTime = OffsetDateTime.now(ZoneOffset.UTC);
+ var largestOffsetDateTime = OffsetDateTime.MAX;
+
+ return Stream.of(currentOffsetDateTime.plusSeconds(1), largestOffsetDateTime);
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/InertValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/InertValueSource.java
new file mode 100644
index 0000000..7e895b6
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/InertValueSource.java
@@ -0,0 +1,12 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+
+import java.util.stream.Stream;
+
+public class InertValueSource implements ValueSource {
+ @Override
+ public Stream values(Class propertyClass, Object... args) {
+ return Stream.empty();
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/LessThanValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/LessThanValueSource.java
new file mode 100644
index 0000000..df02ab9
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/LessThanValueSource.java
@@ -0,0 +1,118 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
+import lombok.NonNull;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+// For floating point values: The negative max value is the minimum, MIN_VALUE is the smallest positive
+public class LessThanValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+ private static final Random RANDOM = new Random();
+
+ static {
+ TYPE_SOURCES.put(Integer.class, LessThanValueSource::getIntegerStream);
+ TYPE_SOURCES.put(int.class, LessThanValueSource::getIntegerStream);
+
+ TYPE_SOURCES.put(Long.class, LessThanValueSource::getLongStream);
+ TYPE_SOURCES.put(long.class, LessThanValueSource::getLongStream);
+
+ TYPE_SOURCES.put(Float.class, LessThanValueSource::getFloatStream);
+ TYPE_SOURCES.put(float.class, LessThanValueSource::getFloatStream);
+
+ TYPE_SOURCES.put(Double.class, LessThanValueSource::getDoubleStream);
+ TYPE_SOURCES.put(double.class, LessThanValueSource::getDoubleStream);
+
+ TYPE_SOURCES.put(BigDecimal.class, LessThanValueSource::getBigDecimalStream);
+ TYPE_SOURCES.put(ScaledBigDecimal.class, LessThanValueSource::getScaledBigDecimalStream);
+ }
+
+ 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!");
+ }
+
+ @NonNull
+ private static Stream getIntegerStream(Object[] args) {
+ var minValue = Integer.MIN_VALUE;
+ var maxValue = Long.valueOf((long) args[0]).intValue() - 1;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.ints(minValue, maxValue).limit(1).boxed()
+ );
+ }
+
+ @NonNull
+ private static Stream getLongStream(Object[] args) {
+ var minValue = Long.MIN_VALUE;
+ var maxValue = (long) args[0] - 1;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.longs(minValue, maxValue).limit(1).boxed()
+ );
+ }
+
+ @NonNull
+ private static Stream getFloatStream(Object[] args) {
+ var minValue = Float.MAX_VALUE * -1;
+ var maxValue = Long.valueOf((long) args[0]).floatValue() - 0.1f;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed().map(
+ Double::floatValue
+ )
+ );
+ }
+
+ @NonNull
+ private static Stream getDoubleStream(Object[] args) {
+ var minValue = Double.MAX_VALUE * -1;
+ var maxValue = Long.valueOf((long) args[0]).doubleValue() - 0.1d;
+
+ return Stream.concat(
+ Stream.of(minValue, maxValue),
+ RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed()
+ );
+ }
+
+ @NonNull
+ private static Stream getBigDecimalStream(Object[] args) {
+ var minValue = Double.MAX_VALUE * -1;
+ var maxValue = Long.valueOf((long) args[0]).doubleValue() - 0.1d;
+
+ return Stream.concat(
+ Stream.of(BigDecimal.valueOf(minValue), BigDecimal.valueOf(maxValue)),
+ RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed().map(BigDecimal::valueOf)
+ );
+ }
+
+ @NonNull
+ private static Stream getScaledBigDecimalStream(Object[] args) {
+ var minValue = Double.MAX_VALUE * -1;
+ var maxValue = Long.valueOf((long) args[0]).doubleValue() - 0.1d;
+
+ return Stream.concat(
+ Stream.of(ScaledBigDecimal.valueOf(minValue), ScaledBigDecimal.valueOf(maxValue)),
+ RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed().map(ScaledBigDecimal::valueOf)
+ );
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/NullValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/NullValueSource.java
new file mode 100644
index 0000000..0656bdf
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/NullValueSource.java
@@ -0,0 +1,27 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class NullValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+
+ 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);
+ }
+
+ return Stream.of((T) null);
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/PastValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/PastValueSource.java
new file mode 100644
index 0000000..f8b2271
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/PastValueSource.java
@@ -0,0 +1,59 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class PastValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+
+ static {
+ TYPE_SOURCES.put(LocalDate.class, PastValueSource::getLocalDateStream);
+ TYPE_SOURCES.put(LocalDateTime.class, PastValueSource::getLocalDatetimeStream);
+ TYPE_SOURCES.put(OffsetDateTime.class, PastValueSource::getOffsetDateTimeStream);
+ }
+
+ 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 getLocalDateStream(Object[] args) {
+ var currentDate = LocalDate.now();
+ var smallestDate = LocalDate.MIN;
+
+ return Stream.of(smallestDate, currentDate.minusDays(1));
+ }
+
+ private static Stream getLocalDatetimeStream(Object[] args) {
+ var currentDateTime = LocalDateTime.now();
+ var smallestDateTime = LocalDateTime.MIN;
+
+ return Stream.of(smallestDateTime, currentDateTime.minusSeconds(1));
+ }
+
+ private static Stream getOffsetDateTimeStream(Object[] args) {
+ var currentOffsetDateTime = OffsetDateTime.now(ZoneOffset.UTC);
+ var smallestOffsetDateTime = OffsetDateTime.MIN;
+
+ return Stream.of(smallestOffsetDateTime, currentOffsetDateTime.minusSeconds(1));
+ }
+}
diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/ZeroValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/ZeroValueSource.java
new file mode 100644
index 0000000..b0a7c02
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/ZeroValueSource.java
@@ -0,0 +1,46 @@
+package it.aboutbits.springboot.testing.validation.source;
+
+import it.aboutbits.springboot.testing.validation.core.ValueSource;
+import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class ZeroValueSource implements ValueSource {
+ private static final Map, Function>> TYPE_SOURCES = new HashMap<>();
+
+ static {
+ TYPE_SOURCES.put(Integer.class, (Object[] args) -> Stream.of(0));
+ TYPE_SOURCES.put(int.class, (Object[] args) -> Stream.of(0));
+
+ TYPE_SOURCES.put(Long.class, (Object[] args) -> Stream.of(0L));
+ TYPE_SOURCES.put(long.class, (Object[] args) -> Stream.of(0L));
+
+ TYPE_SOURCES.put(Float.class, (Object[] args) -> Stream.of(0F));
+ TYPE_SOURCES.put(float.class, (Object[] args) -> Stream.of(0F));
+
+ TYPE_SOURCES.put(Double.class, (Object[] args) -> Stream.of(0D));
+ TYPE_SOURCES.put(double.class, (Object[] args) -> Stream.of(0D));
+
+ TYPE_SOURCES.put(BigDecimal.class, (Object[] args) -> Stream.of(BigDecimal.valueOf(0)));
+ TYPE_SOURCES.put(ScaledBigDecimal.class, (Object[] args) -> Stream.of(ScaledBigDecimal.valueOf(0)));
+ }
+
+ 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!");
+ }
+}
diff --git a/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java b/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java
new file mode 100644
index 0000000..e59d945
--- /dev/null
+++ b/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java
@@ -0,0 +1,436 @@
+package it.aboutbits.springboot.testing.validation;
+
+import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder;
+import it.aboutbits.springboot.testing.validation.core.BaseValidationAssert;
+import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Future;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Negative;
+import jakarta.validation.constraints.NegativeOrZero;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Past;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.With;
+import org.junit.jupiter.api.Test;
+import org.springframework.lang.Nullable;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+
+import static it.aboutbits.springboot.testing.validation.ValidationAssertTest.TestValidationAssert.assertThatValidation;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+class ValidationAssertTest {
+ @With
+ public record SomeValidParameter(
+ @NotNull
+ String notNullable,
+ @NotBlank
+ String notBlank,
+ @Min(5)
+ int biggerThanInt,
+ @Min(5)
+ long biggerThanLong,
+ @Min(5)
+ float biggerThanFloat,
+ @Min(5)
+ double biggerThanDouble,
+ @Min(5)
+ BigDecimal biggerThanBigDecimal,
+ @Min(5)
+ ScaledBigDecimal biggerThanScaledBigDecimal,
+ @Max(5)
+ int lessThanInt,
+ @Max(5)
+ long lessThanLong,
+ @Max(5)
+ float lessThanFloat,
+ @Max(5)
+ double lessThanDouble,
+ @Max(5)
+ BigDecimal lessThanBigDecimal,
+ @Max(5)
+ ScaledBigDecimal lessThanScaledBigDecimal,
+ @Min(-3)
+ @Max(5)
+ int betweenInt,
+ @Min(-3)
+ @Max(5)
+ long betweenLong,
+ @Min(-3)
+ @Max(5)
+ float betweenFloat,
+ @Min(-3)
+ @Max(5)
+ double betweenDouble,
+ @Min(-3)
+ @Max(5)
+ BigDecimal betweenBigDecimal,
+ @Min(-3)
+ @Max(5)
+ ScaledBigDecimal betweenScaledBigDecimal,
+ @Positive
+ int positiveInt,
+ @Positive
+ long positiveLong,
+ @Positive
+ float positiveFloat,
+ @Positive
+ double positiveDouble,
+ @Positive
+ BigDecimal positiveBigDecimal,
+ @Positive
+ ScaledBigDecimal positiveScaledBigDecimal,
+ @Negative
+ int negativeInt,
+ @Negative
+ long negativeLong,
+ @Negative
+ float negativeFloat,
+ @Negative
+ double negativeDouble,
+ @Negative
+ BigDecimal negativeBigDecimal,
+ @Negative
+ ScaledBigDecimal negativeScaledBigDecimal,
+ @PositiveOrZero
+ int positiveOrZeroInt,
+ @PositiveOrZero
+ long positiveOrZeroLong,
+ @PositiveOrZero
+ float positiveOrZeroFloat,
+ @PositiveOrZero
+ double positiveOrZeroDouble,
+ @PositiveOrZero
+ BigDecimal positiveOrZeroBigDecimal,
+ @PositiveOrZero
+ ScaledBigDecimal positiveOrZeroScaledBigDecimal,
+ @NegativeOrZero
+ int negativeOrZeroInt,
+ @NegativeOrZero
+ long negativeOrZeroLong,
+ @NegativeOrZero
+ float negativeOrZeroFloat,
+ @NegativeOrZero
+ double negativeOrZeroDouble,
+ @NegativeOrZero
+ BigDecimal negativeOrZeroBigDecimal,
+ @NegativeOrZero
+ ScaledBigDecimal negativeOrZeroScaledBigDecimal,
+ @Future
+ LocalDate futureDate,
+ @Future
+ LocalDateTime futureDateTime,
+ @Future
+ OffsetDateTime futureOffsetDateTime,
+ @Past
+ LocalDate pastDate,
+ @Past
+ LocalDateTime pastDateTime,
+ @Past
+ OffsetDateTime pastOffsetDateTime,
+ @Valid
+ Object validObject,
+ @Nullable
+ Object nullable,
+ Object notValidated
+ ) {
+
+ }
+
+ @Test
+ @SuppressWarnings("checkstyle:MethodLength")
+ void testWithBeanValidation() {
+ var validParameter = getSomeValidParameter();
+
+ assertThatValidation().of(validParameter)
+ .usingBeanValidation()
+ .notNull("notNullable")
+ .notBlank("notBlank")
+ .min("biggerThanInt", 5)
+ .min("biggerThanLong", 5)
+ .min("biggerThanFloat", 5)
+ .min("biggerThanDouble", 5)
+ .min("biggerThanBigDecimal", 5)
+ .min("biggerThanScaledBigDecimal", 5)
+ .max("lessThanInt", 5)
+ .max("lessThanLong", 5)
+ .max("lessThanFloat", 5)
+ .max("lessThanDouble", 5)
+ .max("lessThanBigDecimal", 5)
+ .max("lessThanScaledBigDecimal", 5)
+ .positive("positiveInt")
+ .positive("positiveLong")
+ .positive("positiveFloat")
+ .positive("positiveDouble")
+ .positive("positiveBigDecimal")
+ .positive("positiveScaledBigDecimal")
+ .negative("negativeInt")
+ .negative("negativeLong")
+ .negative("negativeFloat")
+ .negative("negativeDouble")
+ .negative("negativeBigDecimal")
+ .negative("negativeScaledBigDecimal")
+ .between("betweenInt", -3, 5)
+ .between("betweenLong", -3, 5)
+ .between("betweenFloat", -3, 5)
+ .between("betweenDouble", -3, 5)
+ .between("betweenBigDecimal", -3, 5)
+ .between("betweenScaledBigDecimal", -3, 5)
+ .positiveOrZero("positiveOrZeroInt")
+ .positiveOrZero("positiveOrZeroLong")
+ .positiveOrZero("positiveOrZeroFloat")
+ .positiveOrZero("positiveOrZeroDouble")
+ .positiveOrZero("positiveOrZeroBigDecimal")
+ .positiveOrZero("positiveOrZeroScaledBigDecimal")
+ .negativeOrZero("negativeOrZeroInt")
+ .negativeOrZero("negativeOrZeroLong")
+ .negativeOrZero("negativeOrZeroFloat")
+ .negativeOrZero("negativeOrZeroDouble")
+ .negativeOrZero("negativeOrZeroBigDecimal")
+ .negativeOrZero("negativeOrZeroScaledBigDecimal")
+ .future("futureDate")
+ .future("futureDateTime")
+ .future("futureOffsetDateTime")
+ .past("pastDate")
+ .past("pastDateTime")
+ .past("pastOffsetDateTime")
+ .validBean("validObject")
+ .nullable("nullable")
+ .notValidated("notValidated")
+ .isCompliant();
+ }
+
+ @Test
+ void invalidParameter_shouldFail() {
+ var validParameter = getSomeValidParameter();
+
+ var invalidParameter = validParameter.withFutureDate(LocalDate.EPOCH);
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(
+ () -> assertThatValidation().of(invalidParameter)
+ .usingBeanValidation()
+ .notNull("notNullable")
+ .notBlank("notBlank")
+ .min("biggerThanInt", 5)
+ .min("biggerThanLong", 5)
+ .min("biggerThanFloat", 5)
+ .min("biggerThanDouble", 5)
+ .min("biggerThanBigDecimal", 5)
+ .min("biggerThanScaledBigDecimal", 5)
+ .max("lessThanInt", 5)
+ .max("lessThanLong", 5)
+ .max("lessThanFloat", 5)
+ .max("lessThanDouble", 5)
+ .max("lessThanBigDecimal", 5)
+ .max("lessThanScaledBigDecimal", 5)
+ .positive("positiveInt")
+ .positive("positiveLong")
+ .positive("positiveFloat")
+ .positive("positiveDouble")
+ .positive("positiveBigDecimal")
+ .positive("positiveScaledBigDecimal")
+ .negative("negativeInt")
+ .negative("negativeLong")
+ .negative("negativeFloat")
+ .negative("negativeDouble")
+ .negative("negativeBigDecimal")
+ .negative("negativeScaledBigDecimal")
+ .between("betweenInt", -3, 5)
+ .between("betweenLong", -3, 5)
+ .between("betweenFloat", -3, 5)
+ .between("betweenDouble", -3, 5)
+ .between("betweenBigDecimal", -3, 5)
+ .between("betweenScaledBigDecimal", -3, 5)
+ .positiveOrZero("positiveOrZeroInt")
+ .positiveOrZero("positiveOrZeroLong")
+ .positiveOrZero("positiveOrZeroFloat")
+ .positiveOrZero("positiveOrZeroDouble")
+ .positiveOrZero("positiveOrZeroBigDecimal")
+ .positiveOrZero("positiveOrZeroScaledBigDecimal")
+ .negativeOrZero("negativeOrZeroInt")
+ .negativeOrZero("negativeOrZeroLong")
+ .negativeOrZero("negativeOrZeroFloat")
+ .negativeOrZero("negativeOrZeroDouble")
+ .negativeOrZero("negativeOrZeroBigDecimal")
+ .negativeOrZero("negativeOrZeroScaledBigDecimal")
+ .future("futureDate")
+ .future("futureDateTime")
+ .future("futureOffsetDateTime")
+ .past("pastDate")
+ .past("pastDateTime")
+ .past("pastOffsetDateTime")
+ .validBean("validObject")
+ .nullable("nullable")
+ .notValidated("notValidated")
+ .isCompliant());
+ }
+
+ @Test
+ void propertyMissingRule_shouldFail() {
+ var validParameter = getSomeValidParameter();
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(
+ () -> assertThatValidation().of(validParameter)
+ .usingBeanValidation()
+ .notNull("notNullable")
+ // this is now missing: .notBlank("notBlank")
+ .min("biggerThanInt", 5)
+ .min("biggerThanLong", 5)
+ .min("biggerThanFloat", 5)
+ .min("biggerThanDouble", 5)
+ .min("biggerThanBigDecimal", 5)
+ .min("biggerThanScaledBigDecimal", 5)
+ .max("lessThanInt", 5)
+ .max("lessThanLong", 5)
+ .max("lessThanFloat", 5)
+ .max("lessThanDouble", 5)
+ .max("lessThanBigDecimal", 5)
+ .max("lessThanScaledBigDecimal", 5)
+ .positive("positiveInt")
+ .positive("positiveLong")
+ .positive("positiveFloat")
+ .positive("positiveDouble")
+ .positive("positiveBigDecimal")
+ .positive("positiveScaledBigDecimal")
+ .negative("negativeInt")
+ .negative("negativeLong")
+ .negative("negativeFloat")
+ .negative("negativeDouble")
+ .negative("negativeBigDecimal")
+ .negative("negativeScaledBigDecimal")
+ .between("betweenInt", -3, 5)
+ .between("betweenLong", -3, 5)
+ .between("betweenFloat", -3, 5)
+ .between("betweenDouble", -3, 5)
+ .between("betweenBigDecimal", -3, 5)
+ .between("betweenScaledBigDecimal", -3, 5)
+ .positiveOrZero("positiveOrZeroInt")
+ .positiveOrZero("positiveOrZeroLong")
+ .positiveOrZero("positiveOrZeroFloat")
+ .positiveOrZero("positiveOrZeroDouble")
+ .positiveOrZero("positiveOrZeroBigDecimal")
+ .positiveOrZero("positiveOrZeroScaledBigDecimal")
+ .negativeOrZero("negativeOrZeroInt")
+ .negativeOrZero("negativeOrZeroLong")
+ .negativeOrZero("negativeOrZeroFloat")
+ .negativeOrZero("negativeOrZeroDouble")
+ .negativeOrZero("negativeOrZeroBigDecimal")
+ .negativeOrZero("negativeOrZeroScaledBigDecimal")
+ .future("futureDate")
+ .future("futureDateTime")
+ .future("futureOffsetDateTime")
+ .past("pastDate")
+ .past("pastDateTime")
+ .past("pastOffsetDateTime")
+ .validBean("validObject")
+ .nullable("nullable")
+ .notValidated("notValidated")
+ .isCompliant());
+ }
+
+ private static SomeValidParameter getSomeValidParameter() {
+ return new SomeValidParameter(
+ // notNull
+ "",
+
+ // notBlank
+ "something",
+
+ // min
+ 6,
+ 6,
+ 6,
+ 6,
+ BigDecimal.valueOf(6),
+ ScaledBigDecimal.valueOf(6),
+
+ // max
+ 4,
+ 4,
+ 4,
+ 4,
+ BigDecimal.valueOf(4),
+ ScaledBigDecimal.valueOf(4),
+
+ // positive
+ 4,
+ 4,
+ 4,
+ 4,
+ BigDecimal.valueOf(4),
+ ScaledBigDecimal.valueOf(4),
+
+ // negative
+ 1,
+ 1,
+ 1,
+ 1,
+ BigDecimal.valueOf(1),
+ ScaledBigDecimal.valueOf(1),
+
+ // between
+ -1,
+ -1,
+ -1,
+ -1,
+ BigDecimal.valueOf(-1),
+ ScaledBigDecimal.valueOf(-1),
+
+ // positiveOrZero
+ 0,
+ 0,
+ 0,
+ 0,
+ BigDecimal.valueOf(0),
+ ScaledBigDecimal.valueOf(0),
+
+ // negativeOrZero
+ 0,
+ 0,
+ 0,
+ 0,
+ BigDecimal.valueOf(0),
+ ScaledBigDecimal.valueOf(0),
+
+ // future
+ LocalDate.now().plusDays(1),
+ LocalDateTime.now().plusDays(1),
+ OffsetDateTime.now().plusDays(1),
+
+ // past
+ LocalDate.now().minusDays(1),
+ LocalDateTime.now().minusDays(1),
+ OffsetDateTime.now().minusDays(1),
+
+ // valid
+ null,
+
+ // nullable
+ null,
+
+ // not validated
+ null
+ );
+ }
+
+ public static final class TestValidationAssert extends BaseValidationAssert> {
+ private TestValidationAssert() {
+ super(new TestRuleBuilder());
+ }
+
+ public static TestValidationAssert assertThatValidation() {
+ return new TestValidationAssert();
+ }
+
+ public static final class TestRuleBuilder extends BaseRuleBuilder {
+
+ }
+ }
+}