Skip to content

Commit ded32fc

Browse files
committed
Support Jakarta DecimalMax
1 parent a35b9a2 commit ded32fc

File tree

7 files changed

+194
-15
lines changed

7 files changed

+194
-15
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package example.jakarta;
2+
3+
import jakarta.validation.Valid;
4+
import jakarta.validation.constraints.DecimalMax;
5+
import jakarta.validation.constraints.Digits;
6+
7+
import java.math.BigDecimal;
8+
9+
@Valid
10+
public class JMyNumbers {
11+
12+
@DecimalMax("10.50")
13+
final BigDecimal price;
14+
15+
@DecimalMax(value = "9.30", inclusive = false)
16+
final BigDecimal priceInc;
17+
18+
@Digits(integer = 5, fraction = 3)
19+
final String someDigits;
20+
21+
public JMyNumbers(BigDecimal price, BigDecimal priceInc, String someDigits) {
22+
this.price = price;
23+
this.priceInc = priceInc;
24+
this.someDigits = someDigits;
25+
}
26+
27+
public BigDecimal price() {
28+
return price;
29+
}
30+
31+
public BigDecimal priceInc() {
32+
return priceInc;
33+
}
34+
35+
public String someDigits() {
36+
return someDigits;
37+
}
38+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package example.jakarta;
2+
3+
import io.avaje.validation.ConstraintViolation;
4+
import io.avaje.validation.ConstraintViolationException;
5+
import io.avaje.validation.Validator;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.math.BigDecimal;
9+
import java.util.ArrayList;
10+
import java.util.Locale;
11+
12+
import static java.math.BigDecimal.ONE;
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.assertj.core.api.Assertions.fail;
15+
16+
class JMyNumbersTest {
17+
18+
final Validator validator = Validator.builder().build();
19+
20+
@Test
21+
void valid() {
22+
var bean = new JMyNumbers(ONE, ONE, "12345.123");
23+
validator.validate(bean);
24+
}
25+
26+
@Test
27+
void decimalMax() {
28+
var violation = one(new JMyNumbers(new BigDecimal("11"), ONE, "12345.123"));
29+
assertThat(violation.message()).isEqualTo("must be less than or equal to 10.50");
30+
}
31+
32+
@Test
33+
void decimalMaxDE() {
34+
var violation = one(new JMyNumbers(new BigDecimal("11"), ONE, "12345.123"), Locale.GERMAN);
35+
assertThat(violation.message()).isEqualTo("muss kleiner oder gleich 10.50 sein");
36+
}
37+
38+
@Test
39+
void decimalMaxExclusive() {
40+
var violation = one(new JMyNumbers(ONE, new BigDecimal("11"), "12345.123"));
41+
assertThat(violation.message()).isEqualTo("must be less than 9.30");
42+
}
43+
44+
@Test
45+
void decimalMaxExclusiveDE() {
46+
var violation = one(new JMyNumbers(ONE, new BigDecimal("11"), "12345.123"), Locale.GERMAN);
47+
assertThat(violation.message()).isEqualTo("muss kleiner 9.30 sein");
48+
}
49+
50+
ConstraintViolation one(Object any) {
51+
return one(any, Locale.ENGLISH);
52+
}
53+
54+
ConstraintViolation one(Object any, Locale locale) {
55+
try {
56+
validator.validate(any, locale);
57+
fail("not expected");
58+
return null;
59+
} catch (ConstraintViolationException e) {
60+
var violations = new ArrayList<>(e.violations());
61+
assertThat(violations).hasSize(1);
62+
return violations.get(0);
63+
}
64+
}
65+
}

validator-generator/src/main/java/io/avaje/validation/generator/AnnotationUtil.java

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ interface Handler {
1818
handlers.put("avaje.Pattern", pattern);
1919
handlers.put("jakarta.validation.constraints.Pattern", pattern);
2020

21-
Handler jakartaHandler = new JakartaHandler();
21+
final var jakartaHandler = new JakartaHandler();
2222
handlers.put("jakarta.validation.constraints.NotBlank", jakartaHandler);
2323
handlers.put("jakarta.validation.constraints.Size", jakartaHandler);
24+
25+
final var jakartaDecimal = new JakartaDecimal();
26+
handlers.put("jakarta.validation.constraints.DecimalMax", jakartaDecimal);
2427
}
2528

2629
private AnnotationUtil() {}
@@ -90,12 +93,26 @@ private static void pattern(StringBuilder sb, PatternPrism prism) {
9093

9194
static class StandardHandler extends BaseHandler {
9295

96+
protected final AnnotationMirror annotationMirror;
97+
protected final Element element;
98+
99+
/** Prototype factory */
100+
StandardHandler() {
101+
this.annotationMirror = null;
102+
this.element = null;
103+
}
104+
105+
StandardHandler(AnnotationMirror annotationMirror, Element element) {
106+
this.annotationMirror = annotationMirror;
107+
this.element = element;
108+
}
109+
93110
@Override
94111
public String attributes(AnnotationMirror annotationMirror, Element element) {
95-
return new StandardHandler().writeAttributes(annotationMirror, element);
112+
return new StandardHandler(annotationMirror, element).writeAttributes();
96113
}
97114

98-
String writeAttributes(AnnotationMirror annotationMirror, Element element) {
115+
String writeAttributes() {
99116
for (final ExecutableElement member : ElementFilter.methodsIn(element.getEnclosedElements())) {
100117
final AnnotationValue value = annotationMirror.getElementValues().get(member);
101118
final AnnotationValue defaultValue = member.getDefaultValue();
@@ -124,30 +141,78 @@ void writeAttributeKey(String name) {
124141
first = false;
125142
sb.append("\"").append(name).append("\",");
126143
}
144+
145+
AnnotationValue memberValue(String nameMatch) {
146+
for (final ExecutableElement member : ElementFilter.methodsIn(element.getEnclosedElements())) {
147+
if (nameMatch.equals(member.getSimpleName().toString())) {
148+
return annotationMirror.getElementValues().get(member);
149+
}
150+
}
151+
return null;
152+
}
127153
}
128154

129155
static class JakartaHandler extends StandardHandler {
130156

157+
/** Prototype factory only */
158+
JakartaHandler() {
159+
super();
160+
}
161+
162+
JakartaHandler(AnnotationMirror annotationMirror, Element element) {
163+
super(annotationMirror, element);
164+
}
165+
131166
@Override
132167
public String attributes(AnnotationMirror annotationMirror, Element element) {
133-
return new JakartaHandler().writeAttributes(annotationMirror, element);
168+
return new JakartaHandler(annotationMirror, element).writeAttributes();
134169
}
135170

136171
@Override
137172
void writeAttribute(Name simpleName, AnnotationValue value, AnnotationValue defaultValue) {
138173
final String name = simpleName.toString();
139174
if (value == null) {
140175
if ("message".equals(name)) {
141-
final String msgKey = defaultValue.toString().replace("{jakarta.validation.constraints.", "{avaje.");
142176
writeAttributeKey("message");
143-
sb.append(msgKey);
177+
sb.append(messageKey(defaultValue));
144178
} else if (!name.equals("payload") && !name.equals("groups")) {
145179
super.writeAttribute(simpleName, null, defaultValue);
146180
}
147181
} else {
148182
super.writeAttribute(simpleName, value, defaultValue);
149183
}
150184
}
185+
186+
String messageKey(AnnotationValue defaultValue) {
187+
return defaultValue.toString().replace("{jakarta.validation.constraints.", "{avaje.");
188+
}
189+
}
190+
191+
static class JakartaDecimal extends JakartaHandler {
192+
193+
/** Prototype factory only */
194+
JakartaDecimal() {
195+
super();
196+
}
197+
198+
@Override
199+
public String attributes(AnnotationMirror annotationMirror, Element element) {
200+
return new JakartaDecimal(annotationMirror, element).writeAttributes();
201+
}
202+
203+
JakartaDecimal(AnnotationMirror annotationMirror, Element element) {
204+
super(annotationMirror, element);
205+
}
206+
207+
String messageKey(AnnotationValue defaultValue) {
208+
final AnnotationValue inclusiveValue = memberValue("inclusive");
209+
final boolean inclusive = (inclusiveValue == null || "true".equals(inclusiveValue.toString()));
210+
String messageKey = super.messageKey(defaultValue);
211+
if (!inclusive) {
212+
messageKey = messageKey.replace("DecimalMax.message", "DecimalMax.exclusive.message");
213+
}
214+
return messageKey;
215+
}
151216
}
152217

153218
}

validator-generator/src/main/java/io/avaje/validation/generator/FieldReader.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.util.stream.Collectors.toMap;
44

5+
import java.util.HashSet;
56
import java.util.List;
67
import java.util.Map;
78
import java.util.Set;
@@ -12,6 +13,12 @@
1213

1314
final class FieldReader {
1415

16+
static final Set<String> BASIC_TYPES = new HashSet<>();
17+
static {
18+
BASIC_TYPES.add("java.lang.String");
19+
BASIC_TYPES.add("java.math.BigDecimal");
20+
}
21+
1522
private final List<String> genericTypeParams;
1623
private final boolean publicField;
1724
private final String rawType;
@@ -241,6 +248,6 @@ public void writeConstructor(Append writer) {
241248
}
242249

243250
private boolean isBasicType(final String topType) {
244-
return "java.lang.String".equals(topType) || GenericTypeMap.typeOfRaw(topType) != null;
251+
return BASIC_TYPES.contains(topType) || GenericTypeMap.typeOfRaw(topType) != null;
245252
}
246253
}

validator/src/main/java/io/avaje/validation/core/adapters/NumberAdapters.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private NumberAdapters() {}
2727
case "Max" -> new MaxAdapter(context.message("Max", attributes), attributes);
2828
case "Min" -> new MinAdapter(context.message("Min", attributes), attributes);
2929
case "DecimalMax" -> new DecimalMaxAdapter(
30-
context.message("DecimalMax", attributes), attributes);
30+
context.message2(attributes), attributes);
3131
case "DecimalMin" -> new DecimalMinAdapter(
3232
context.message("DecimalMin", attributes), attributes);
3333

@@ -36,11 +36,11 @@ private NumberAdapters() {}
3636

3737
private static final class DecimalMaxAdapter implements ValidationAdapter<Number> {
3838

39-
private final String message;
39+
private final ValidationContext.Message message;
4040
private final BigDecimal value;
4141
private final boolean inclusive;
4242

43-
public DecimalMaxAdapter(String message, Map<String, Object> attributes) {
43+
public DecimalMaxAdapter(ValidationContext.Message message, Map<String, Object> attributes) {
4444
this.message = message;
4545
this.value = new BigDecimal((String) attributes.get("value"));
4646
this.inclusive = Optional.ofNullable((Boolean) attributes.get("inclusive")).orElse(true);
@@ -56,7 +56,7 @@ public boolean validate(Number number, ValidationRequest req, String propertyNam
5656

5757
final int comparisonResult = NumberComparatorHelper.compareDecimal(number, value, LESS_THAN);
5858

59-
if (inclusive ? comparisonResult <= 0 : comparisonResult < 0) {
59+
if (inclusive ? comparisonResult > 0 : comparisonResult >= 0) {
6060
req.addViolation(message, propertyName);
6161
return false;
6262
}

validator/src/main/resources/io/avaje/validation/Messages.properties

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
avaje.AssertFalse.message = must be false
22
avaje.AssertTrue.message = must be true
3-
avaje.DecimalMax.message = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
4-
avaje.DecimalMin.message = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
3+
avaje.DecimalMax.message = must be less than or equal to {value}
4+
avaje.DecimalMax.exclusive.message = must be less than {value}
5+
avaje.DecimalMin.message = must be greater than or equal to {value}
6+
avaje.DecimalMin.exclusive.message = must be greater than {value}
57
avaje.Digits.message = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
68
avaje.Email.message = must be a well-formed email address
79
avaje.Future.message = must be a future date

validator/src/main/resources/io/avaje/validation/Messages_de.properties

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
avaje.AssertFalse.message = muss falsch sein
22
avaje.AssertTrue.message = muss wahr sein
3-
avaje.DecimalMax.message = muss kleiner ${inclusive == true ? 'oder gleich ' : ''}{value} sein
4-
avaje.DecimalMin.message = muss gr\u00f6\u00dfer ${inclusive == true ? 'oder gleich ' : ''}{value} sein
3+
avaje.DecimalMax.message = muss kleiner oder gleich {value} sein
4+
avaje.DecimalMax.exclusive.message = muss kleiner {value} sein
5+
avaje.DecimalMin.message = muss gr\u00f6\u00dfer oder gleich {value} sein
6+
avaje.DecimalMin.exclusive.message = muss gr\u00f6\u00dfer {value} sein
57
avaje.Digits.message = numerischer Wert au\u00dferhalb des g\u00fcltigen Bereichs (<{integer} digits>.<{fraction} digits> erwartet)
68
avaje.Email.message = muss eine korrekt formatierte E-Mail-Adresse sein
79
avaje.Future.message = muss ein Datum in der Zukunft sein

0 commit comments

Comments
 (0)