diff --git a/blackbox-test/src/test/java/example/avaje/range/APrimitiveLongRange.java b/blackbox-test/src/test/java/example/avaje/range/APrimitiveLongRange.java new file mode 100644 index 00000000..833ad137 --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/range/APrimitiveLongRange.java @@ -0,0 +1,11 @@ +package example.avaje.range; + +import io.avaje.validation.constraints.Range; +import jakarta.validation.Valid; + +@Valid +public record APrimitiveLongRange( + @Range(min = 1, max = 3) + long value +) { +} diff --git a/blackbox-test/src/test/java/example/avaje/range/APrimitiveTest.java b/blackbox-test/src/test/java/example/avaje/range/APrimitiveTest.java new file mode 100644 index 00000000..2a141f8a --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/range/APrimitiveTest.java @@ -0,0 +1,34 @@ +package example.avaje.range; + +import io.avaje.validation.Validator; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThat; + +class APrimitiveTest { + + Validator validator = Validator.builder().build(); + + @Test + void rangeLongValid() { + validator.validate(new APrimitiveLongRange(1)); + validator.validate(new APrimitiveLongRange(2)); + validator.validate(new APrimitiveLongRange(3)); + } + + @Test + void rangeLongBelowMin() { + var violations = new ArrayList<>(validator.check(new APrimitiveLongRange(0))); + assertThat(violations).hasSize(1); + assertThat(violations.get(0).message()).isEqualTo("must be between 1 and 3"); + } + + @Test + void rangeLongAboveMax() { + var violations = new ArrayList<>(validator.check(new APrimitiveLongRange(4))); + assertThat(violations).hasSize(1); + assertThat(violations.get(0).message()).isEqualTo("must be between 1 and 3"); + } +} diff --git a/blackbox-test/src/test/java/example/avaje/range/ARange.java b/blackbox-test/src/test/java/example/avaje/range/ARange.java index 47d1d418..5da2d1d1 100644 --- a/blackbox-test/src/test/java/example/avaje/range/ARange.java +++ b/blackbox-test/src/test/java/example/avaje/range/ARange.java @@ -1,6 +1,5 @@ package example.avaje.range; -import io.avaje.validation.constraints.Length; import io.avaje.validation.constraints.Range; import jakarta.validation.Valid; diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/AdapterHelper.java b/validator-generator/src/main/java/io/avaje/validation/generator/AdapterHelper.java index 2a4828fe..a4836dc0 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/AdapterHelper.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/AdapterHelper.java @@ -14,6 +14,7 @@ final class AdapterHelper { private final GenericType genericType; private final boolean classLevel; private final boolean crossParam; + private boolean usePrimitiveValidation; AdapterHelper(Append writer, ElementAnnotationContainer elementAnnotations, String indent) { this(writer, elementAnnotations, indent,"Object", null, false, false); @@ -47,6 +48,11 @@ final class AdapterHelper { this.crossParam = crossParam; } + AdapterHelper usePrimitiveValidation(boolean usePrimitiveValidation) { + this.usePrimitiveValidation = usePrimitiveValidation; + return this; + } + void write() { final var typeUse1 = elementAnnotations.typeUse1(); final var typeUse2 = elementAnnotations.typeUse2(); @@ -55,6 +61,10 @@ void write() { if (crossParam) { return; } + if (usePrimitiveValidation) { + writer.eol().append("%s .primitive()", indent); + return; + } if (!typeUse1.isEmpty() && (isAssignable2Interface(genericType.topType(), "java.lang.Iterable"))) { writer.eol().append("%s .list()", indent); diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java b/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java index d8f2cf7f..97523be7 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java @@ -1,5 +1,6 @@ package io.avaje.validation.generator; +import static io.avaje.validation.generator.PrimitiveUtil.isPrimitiveValidationAnnotations; import static java.util.function.Predicate.not; import static java.util.stream.Collectors.toMap; @@ -80,7 +81,6 @@ static boolean hasMetaConstraintAnnotation(AnnotationMirror m) { } static boolean hasMetaConstraintAnnotation(Element element) { - return ConstraintPrism.isPresent(element); } @@ -89,7 +89,6 @@ static boolean hasMetaConstraintAnnotation(Element element) { static ElementAnnotationContainer create(VariableElement varElement) { final var asString = varElement.asType().toString(); - final var noGeneric = AnnotationUtil.splitString(asString, "<")[0]; final var annotations = @@ -125,4 +124,14 @@ public void addImports(Set importTypes) { boolean isEmpty() { return annotations.isEmpty() && typeUse1.isEmpty() && typeUse2.isEmpty(); } + + boolean supportsPrimitiveValidation() { + for (GenericType validationAnnotation : annotations.keySet()) { + if (!isPrimitiveValidationAnnotations(validationAnnotation.shortName())) { + return false; + } + } + return true; + } + } diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/FieldReader.java b/validator-generator/src/main/java/io/avaje/validation/generator/FieldReader.java index 8c4f38f3..987a7274 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/FieldReader.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/FieldReader.java @@ -1,5 +1,6 @@ package io.avaje.validation.generator; +import static io.avaje.validation.generator.PrimitiveUtil.isPrimitiveValidationType; import static io.avaje.validation.generator.ProcessingContext.logError; import java.util.List; @@ -10,8 +11,6 @@ final class FieldReader { - static final Set BASIC_TYPES = Set.of("java.lang.String", "java.math.BigDecimal"); - private final List genericTypeParams; private final boolean publicField; private final GenericType genericType; @@ -25,6 +24,7 @@ final class FieldReader { private final Element element; private final ElementAnnotationContainer elementAnnotations; private final boolean classLevel; + private final boolean usePrimitiveValidation; FieldReader(Element element, List genericTypeParams) { this(element, genericTypeParams, false); @@ -38,6 +38,7 @@ final class FieldReader { this.elementAnnotations = ElementAnnotationContainer.create(element, classLevel); this.genericType = elementAnnotations.genericType(); final String shortType = genericType.shortType(); + usePrimitiveValidation = isPrimitiveValidationType(shortType) && elementAnnotations.supportsPrimitiveValidation(); adapterShortType = initAdapterShortType(shortType); adapterFieldName = initShortName(); this.optionalValidation = Util.isNullable(element); @@ -45,6 +46,9 @@ final class FieldReader { } private String initAdapterShortType(String shortType) { + if (usePrimitiveValidation) { + return "ValidationAdapter.Primitive"; + } String typeWrapped = "ValidationAdapter<" + PrimitiveUtil.wrap(shortType) + ">"; for (final String typeParam : genericTypeParams) { if (typeWrapped.contains("<" + typeParam + ">")) { @@ -186,6 +190,7 @@ public void writeConstructor(Append writer) { PrimitiveUtil.wrap(genericType.shortType()), genericType, classLevel) + .usePrimitiveValidation(usePrimitiveValidation) .write(); writer.append(";").eol().eol(); } diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/PrimitiveUtil.java b/validator-generator/src/main/java/io/avaje/validation/generator/PrimitiveUtil.java index 68706d71..12fd285f 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/PrimitiveUtil.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/PrimitiveUtil.java @@ -2,10 +2,14 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; final class PrimitiveUtil { - static Map wrapperMap = new HashMap<>(); + private static final Set primitiveValidationTypes = Set.of("int", "long"); + private static final Set primitiveValidationAnnotations = + Set.of("Range", "Min", "Max", "Positive", "PositiveOrZero", "Negative", "NegativeOrZero"); + private static final Map wrapperMap = new HashMap<>(); static { wrapperMap.put("char", "Character"); @@ -27,6 +31,14 @@ static boolean isPrimitive(String typeShortName) { return wrapperMap.containsKey(typeShortName); } + static boolean isPrimitiveValidationType(String typeShortName) { + return primitiveValidationTypes.contains(typeShortName); + } + + static boolean isPrimitiveValidationAnnotations(String annotationShortName) { + return primitiveValidationAnnotations.contains(annotationShortName); + } + static String defaultValue(String shortType) { return "boolean".equals(shortType) ? "false" : "0"; } diff --git a/validator/src/main/java/io/avaje/validation/adapter/ValidationAdapter.java b/validator/src/main/java/io/avaje/validation/adapter/ValidationAdapter.java index 225c9130..462d07db 100644 --- a/validator/src/main/java/io/avaje/validation/adapter/ValidationAdapter.java +++ b/validator/src/main/java/io/avaje/validation/adapter/ValidationAdapter.java @@ -34,6 +34,13 @@ default boolean validate(T value, ValidationRequest req) { return validate(value, req, null); } + /** + * Return a primitive adapter. Supports int, long with Range, Min, Max, Positive. + */ + default Primitive primitive() { + throw new UnsupportedOperationException(); + } + /** * Create an adapter for validating a list of values. * @@ -113,4 +120,20 @@ default boolean checkGroups(Set> adapterGroups, ValidationRequest reque } return false; } + + /** + * Validation adapter that supports and uses the primitive type. + */ + interface Primitive { + + /** + * Validate using primitive int. + */ + boolean validate(int value, ValidationRequest req, String propertyName); + + /** + * Validate using primitive long. + */ + boolean validate(long value, ValidationRequest req, String propertyName); + } } diff --git a/validator/src/main/java/io/avaje/validation/core/adapters/NumberAdapters.java b/validator/src/main/java/io/avaje/validation/core/adapters/NumberAdapters.java index 704d7f8c..55458165 100644 --- a/validator/src/main/java/io/avaje/validation/core/adapters/NumberAdapters.java +++ b/validator/src/main/java/io/avaje/validation/core/adapters/NumberAdapters.java @@ -13,6 +13,7 @@ import io.avaje.validation.adapter.ValidationAdapter; import io.avaje.validation.adapter.ValidationContext; import io.avaje.validation.adapter.ValidationContext.AdapterCreateRequest; +import io.avaje.validation.adapter.ValidationRequest; public final class NumberAdapters { private NumberAdapters() {} @@ -111,15 +112,25 @@ public interface NumberAdapter { boolean isValid(T number); } - private static final class MaxAdapter extends AbstractConstraintAdapter implements NumberAdapter { + private static final class MaxAdapter extends PrimitiveAdapter implements NumberAdapter { - private final long value; + private final long max; private final String targetType; MaxAdapter(AdapterCreateRequest request) { super(request); this.targetType = request.targetType(); - this.value = (long) request.attribute("value"); + this.max = (long) request.attribute("value"); + } + + @Override + boolean isValid(int value) { + return value <= max; + } + + @Override + boolean isValid(long value) { + return value <= max; } @Override @@ -129,9 +140,9 @@ public boolean isValid(Number number) { return true; } return switch (targetType) { - case "Integer", "Long", "Short", "Byte" -> number.longValue() <= value; - case "Double", "Number" -> compareDouble(number.doubleValue(), value, GREATER_THAN) <= 0; - case "Float" -> compareFloat((Float)number, value, GREATER_THAN) <= 0; + case "Integer", "Long", "Short", "Byte" -> number.longValue() <= max; + case "Double", "Number" -> compareDouble(number.doubleValue(), max, GREATER_THAN) <= 0; + case "Float" -> compareFloat((Float)number, max, GREATER_THAN) <= 0; default -> throw new IllegalStateException(); }; } @@ -167,15 +178,25 @@ public boolean isValid(BigInteger number) { } } - private static final class MinAdapter extends AbstractConstraintAdapter implements NumberAdapter { + private static final class MinAdapter extends PrimitiveAdapter implements NumberAdapter { - private final long value; + private final long min; private final String targetType; MinAdapter(AdapterCreateRequest request) { super(request); this.targetType = request.targetType(); - this.value = (long) request.attribute("value"); + this.min = (long) request.attribute("value"); + } + + @Override + boolean isValid(int value) { + return value >= min; + } + + @Override + boolean isValid(long value) { + return value >= min; } @Override @@ -184,9 +205,9 @@ public boolean isValid(Number number) { return true; } return switch (targetType) { - case "Integer", "Long", "Short", "Byte" -> number.longValue() >= value; - case "Double" -> compareDouble(number.doubleValue(), value, LESS_THAN) >= 0; - case "Float" -> compareFloat((Float)number, value, LESS_THAN) >= 0; + case "Integer", "Long", "Short", "Byte" -> number.longValue() >= min; + case "Double" -> compareDouble(number.doubleValue(), min, LESS_THAN) >= 0; + case "Float" -> compareFloat((Float)number, min, LESS_THAN) >= 0; default -> throw new IllegalStateException(); }; } @@ -253,7 +274,7 @@ public boolean isValid(Object value) { } } - private static final class PositiveAdapter extends AbstractConstraintAdapter { + private static final class PositiveAdapter extends PrimitiveAdapter { private final boolean inclusive; private final String targetType; @@ -273,9 +294,19 @@ public boolean isValid(Object value) { final int sign = NumberSignHelper.signum(targetType, value, LESS_THAN); return !(inclusive ? sign < 0 : sign <= 0); } + + @Override + boolean isValid(int value) { + return inclusive ? value >= 0 : value > 0; + } + + @Override + boolean isValid(long value) { + return inclusive ? value >= 0 : value > 0; + } } - private static final class NegativeAdapter extends AbstractConstraintAdapter { + private static final class NegativeAdapter extends PrimitiveAdapter { private final boolean inclusive; private final String targetType; @@ -295,22 +326,44 @@ public boolean isValid(Object value) { final int sign = NumberSignHelper.signum(targetType, value, GREATER_THAN); return !(inclusive ? sign > 0 : sign >= 0); } + + @Override + boolean isValid(int value) { + return inclusive ? value <= 0 : value < 0; + } + + @Override + boolean isValid(long value) { + return inclusive ? value <= 0 : value < 0; + } } - private static final class RangeAdapter extends AbstractConstraintAdapter { + private static final class RangeAdapter extends PrimitiveAdapter { private final NumberAdapter maxAdapter; private final NumberAdapter minAdapter; + private final long min; + private final long max; @SuppressWarnings("unchecked") RangeAdapter(AdapterCreateRequest request) { super(request); - final var min = (long) request.attribute("min"); - final var max = (long) request.attribute("max"); + this.min = (long) request.attribute("min"); + this.max = (long) request.attribute("max"); this.maxAdapter = (NumberAdapter) max(request.withValue(max)); this.minAdapter = (NumberAdapter) min(request.withValue(min)); } + @Override + boolean isValid(int value) { + return value >= min && value <= max; + } + + @Override + boolean isValid(long value) { + return value >= min && value <= max; + } + @Override public boolean isValid(Number value) { if (value == null) { @@ -320,6 +373,46 @@ public boolean isValid(Number value) { } } + private static abstract class PrimitiveAdapter extends AbstractConstraintAdapter implements ValidationAdapter.Primitive { + + PrimitiveAdapter(AdapterCreateRequest request) { + super(request); + } + + @Override + public final Primitive primitive() { + return this; + } + + abstract boolean isValid(int value); + + abstract boolean isValid(long value); + + @Override + public final boolean validate(int value, ValidationRequest req, String propertyName) { + if (!checkGroups(groups, req)) { + return true; + } + if (!isValid(value)) { + req.addViolation(message, propertyName); + return false; + } + return true; + } + + @Override + public final boolean validate(long value, ValidationRequest req, String propertyName) { + if (!checkGroups(groups, req)) { + return true; + } + if (!isValid(value)) { + req.addViolation(message, propertyName); + return false; + } + return true; + } + } + private static final class RangeStringAdapter extends AbstractConstraintAdapter { private final BigDecimal min;