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 fda5fed..1a7460a 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 @@ -23,9 +23,10 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; @RequiredArgsConstructor -public abstract class BaseRuleBuilder> implements +public abstract class BaseRuleBuilder> implements ValidationRulesData, BetweenRule, FutureRule, @@ -53,6 +54,12 @@ public void addRule(@NonNull Rule rule) { rules.add(rule); } + public > T withAdditionalRules(Consumer registrar) { + var self = (T) this; + registrar.accept(self); + return self; + } + public void isCompliant() { triggerValidation.run(); } 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 index 1d51f77..9a4a3da 100644 --- a/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java @@ -11,6 +11,8 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -217,10 +219,10 @@ private static

Stream getValues(Rule rule, P parameterUnderTest) { private static T createCopyWithAlteredProperty(T original, String property, Object alteredValue) { try { // Get the class of the original object - var clazz = original.getClass(); + var clazz = (Class) original.getClass(); // Get all the declared fields of the class - var fields = clazz.getDeclaredFields(); + var fields = getAllFields(clazz); // Create an array to hold the values of the original object's properties var propertyValues = new Object[fields.length]; @@ -235,10 +237,6 @@ private static T createCopyWithAlteredProperty(T original, String property, 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]; @@ -261,11 +259,10 @@ private static T createCopyWithAlteredProperty(T original, String property, } } - // Create a copy with the altered property value - return (T) constructor.newInstance(newPropertyValues); + return createCopyWithAlteredValues(clazz, parameterTypes, newPropertyValues, fields); } catch (NoSuchMethodException e) { throw new RuleValidationException( - "Error creating copy with altered property. Maybe there is no all-args-constructor?", + "Error creating copy with altered property. Maybe there is no eligible-constructor?", e ); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { @@ -273,6 +270,35 @@ private static T createCopyWithAlteredProperty(T original, String property, } } + private static T createCopyWithAlteredValues( + Class clazz, + Class[] parameterTypes, + Object[] newPropertyValues, + Field[] fields + ) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { + T instance = null; + try { + // Get the constructor that accepts all properties as arguments + var constructor = clazz.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + + // Create a copy with the altered property value + instance = constructor.newInstance(newPropertyValues); + } catch (NoSuchMethodException e) { + // Get the no args constructor + var constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + + // Create a copy with the altered property value + instance = constructor.newInstance(); + for (var i = 0; i < newPropertyValues.length; i++) { + fields[i].setAccessible(true); + fields[i].set(instance, newPropertyValues[i]); + } + } + return instance; + } + private static Set getAllPropertiesOf(T object) { var clazz = object.getClass(); @@ -307,12 +333,23 @@ private static boolean hasNullableAnnotation(String propertyName, Object object) 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); + var field = Arrays.stream(getAllFields(clazz)) + .filter( + f -> f.getName().equals(propertyName) + ) + .findFirst(); + + return field.orElseThrow(() -> new RuleValidationException("Property does not exist: " + propertyName)); + } + + private static Field[] getAllFields(Class initialClazz) { + Class clazz = initialClazz; + + var fields = new ArrayList(); + while (clazz != null) { + fields.addAll(Arrays.asList(clazz.getDeclaredFields())); + clazz = clazz.getSuperclass(); } - return field; + return fields.toArray(new Field[0]); } } 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 870e944..a210553 100644 --- a/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java +++ b/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java @@ -32,6 +32,7 @@ import java.time.YearMonth; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.function.Consumer; import static it.aboutbits.springboot.testing.validation.ValidationAssertTest.TestValidationAssert.assertThatValidation; import static org.assertj.core.api.Assertions.assertThatCode; @@ -618,6 +619,49 @@ void givenNotValidatedClass_shouldAlwaysFail() { } } + @Test + void shouldAlsoWorkForExtendedClassesEvenWithoutAllArgsConstructors() { + var item = new SomeExtendingClass(); + item.notNull = "notNull"; + item.notNullPositiveOrZero = ScaledBigDecimal.ONE; + + assertThatValidation().of(item) + .usingBeanValidation() + .notNull("notNull") + .notNull("notNull") + .positiveOrZero("notNullPositiveOrZero") + .isCompliant(); + + var invalidItem = new SomeExtendingClass(); + + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThatValidation().of(invalidItem) + .usingBeanValidation() + .notNull("notNull") + .notNull("notNull") + .positiveOrZero("notNullPositiveOrZero") + .isCompliant() + ); + } + + @Test + void usingRuleRegistrarShouldWork() { + var item = new SomeExtendingClass(); + item.notNull = "notNull"; + item.notNullPositiveOrZero = ScaledBigDecimal.ONE; + + var registrar = (Consumer) ruleBuilder -> ruleBuilder + .notNull("notNull") + .notNull("notNull") + .positiveOrZero("notNullPositiveOrZero"); + + assertThatValidation().of(item) + .usingBeanValidation() + .withAdditionalRules(registrar) + .isCompliant(); + } + + private static SomeValidParameter getSomeValidParameter() { return new SomeValidParameter( // NotNull @@ -791,4 +835,15 @@ public void someMethodWithoutValidParameter(Long first, String last) { public void someMethodWithoutValidParameter(Long first, Integer second, String last) { } } + + public abstract static class SomeBaseClass { + @NotNull + protected String notNull; + } + + public static class SomeExtendingClass extends SomeBaseClass { + @NotNull + @PositiveOrZero + private ScaledBigDecimal notNullPositiveOrZero; + } }