Skip to content

Commit a09f5ef

Browse files
Fix Optional Cascades (#343)
* Fix optional cascade Turns out that using Valid on optional never worked * Update PrimitiveOptional.java * Update PrimitiveOptional.java * final field in OptionalAdapter and format only --------- Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
1 parent 1308639 commit a09f5ef

File tree

9 files changed

+106
-26
lines changed

9 files changed

+106
-26
lines changed

blackbox-test/src/main/java/example/avaje/optional/CurseBearer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ public record CurseBearer(
1414
@NotBlank(message = "it'll happen to you too") Optional<String> name,
1515
@Positive OptionalInt estus,
1616
@Positive(message = "You Died") OptionalLong souls,
17-
@Positive(message = "you didn't pass the vigor check") OptionalDouble vigor) {}
17+
@Positive(message = "you didn't pass the vigor check") OptionalDouble vigor,
18+
@Valid Optional<DarkSign> ds) {
19+
20+
public record DarkSign(@NotBlank(message = "not cursed") String brand) {}
21+
}

blackbox-test/src/test/java/example/avaje/optional/OptionalTest.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99
import java.util.OptionalInt;
1010
import java.util.OptionalLong;
1111
import java.util.Set;
12-
import java.util.stream.Stream;
1312

1413
import org.junit.jupiter.api.Test;
1514

16-
import example.avaje.composable.Sans;
15+
import example.avaje.optional.CurseBearer.DarkSign;
1716
import io.avaje.validation.ConstraintViolation;
1817
import io.avaje.validation.ConstraintViolationException;
1918
import io.avaje.validation.Validator;
@@ -29,21 +28,26 @@ void valid() {
2928
Optional.of("Belmont"),
3029
OptionalInt.of(5),
3130
OptionalLong.of(10000),
32-
OptionalDouble.of(42.0));
31+
OptionalDouble.of(42.0),
32+
Optional.empty());
3333
validator.validate(monarch);
3434
}
3535

3636
@Test
3737
void validEmpty() {
3838
final var hollow =
3939
new CurseBearer(
40-
Optional.empty(), OptionalInt.empty(), OptionalLong.empty(), OptionalDouble.empty());
40+
Optional.empty(),
41+
OptionalInt.empty(),
42+
OptionalLong.empty(),
43+
OptionalDouble.empty(),
44+
Optional.empty());
4145
validator.validate(hollow);
4246
}
4347

4448
@Test
4549
void validNull() {
46-
final var hollow = new CurseBearer(null, null, null, null);
50+
final var hollow = new CurseBearer(null, null, null, null, null);
4751
validator.validate(hollow);
4852
}
4953

@@ -52,13 +56,18 @@ void invalid() {
5256
final var violations =
5357
violations(
5458
new CurseBearer(
55-
Optional.of(""), OptionalInt.of(0), OptionalLong.of(0), OptionalDouble.of(0)));
59+
Optional.of(""),
60+
OptionalInt.of(0),
61+
OptionalLong.of(0),
62+
OptionalDouble.of(0),
63+
Optional.of(new DarkSign(""))));
5664
assertThat(violations)
5765
.contains(
5866
"it'll happen to you too",
5967
"must be greater than 0",
6068
"You Died",
61-
"you didn't pass the vigor check");
69+
"you didn't pass the vigor check",
70+
"not cursed");
6271
}
6372

6473
Set<String> violations(Object any) {

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
final class AdapterHelper {
99

10+
private static final String OPTIONAL = "java.util.Optional";
1011
private final Append writer;
1112
private final ElementAnnotationContainer elementAnnotations;
1213
private final String indent;
@@ -68,7 +69,7 @@ void write() {
6869
return;
6970
}
7071

71-
if (!typeUse1.isEmpty() && (isAssignable(genericType.mainType(), "java.lang.Iterable"))) {
72+
if (!typeUse1.isEmpty() && isAssignable(genericType.mainType(), "java.lang.Iterable")) {
7273
writer.eol().append("%s .list()", indent);
7374
writeTypeUse(genericType.param0(), typeUse1);
7475

@@ -100,22 +101,37 @@ void write() {
100101
indent, mainType.shortWithoutAnnotations().replace("[]", ""));
101102
}
102103

103-
} else if (hasValid) {
104-
if (!classLevel) {
105-
if (genericType.mainType().equals(recursiveType)) {
106-
writer.eol().append("%s .andThen(this)", indent);
104+
} else if (OPTIONAL.equals(genericType.mainType())) {
105+
// cascade validate
106+
if (hasValid) {
107+
if (mainType.param0().fullWithoutAnnotations().equals(recursiveType)) {
108+
// cascade validate
109+
writer.eol().append("%s .andThen(this)", indent, mainType.param0().shortType());
107110
} else {
108-
writer.eol().append("%s .andThen(ctx.adapter(%s.class))", indent, genericType.shortWithoutAnnotations());
111+
// cascade validate
112+
writer.eol().append("%s .andThen(ctx.adapter(%s.class))", indent, mainType.param0().shortType());
109113
}
110114
}
111-
112-
} else if (genericType.mainType().contains("java.util.Optional")) {
113115
writer.eol().append("%s .optional()", indent);
116+
} else if (genericType.mainType().contains(OPTIONAL)) {
117+
writer.eol().append("%s .primitiveOptional()", indent);
118+
} else if (hasValid && !classLevel) {
119+
if (genericType.mainType().equals(recursiveType)) {
120+
writer.eol().append("%s .andThen(this)", indent);
121+
} else {
122+
writer.eol().append("%s .andThen(ctx.adapter(%s.class))", indent, genericType.shortWithoutAnnotations());
123+
}
114124
}
115125
}
116126

117127
private void writeFirst(List<Entry<UType, String>> annotations) {
118128
boolean first = true;
129+
130+
var type =
131+
OPTIONAL.equals(genericType.mainType())
132+
? genericType.param0().shortWithoutAnnotations()
133+
: this.type;
134+
119135
for (final var a : annotations) {
120136
if (first) {
121137
writer.append("%sctx.<%s>adapter(%s.class, %s)", indent, type, a.getKey().shortWithoutAnnotations(), a.getValue());

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ void cascadeTypes(Set<String> types) {
119119
}
120120

121121
final String mainType = genericType.mainType();
122-
if ("java.util.List".equals(mainType) || "java.util.Set".equals(mainType)) {
122+
if ("java.util.List".equals(mainType) || "java.util.Set".equals(mainType) || "java.util.Optional".equals(mainType)) {
123123
types.add(genericType.param0().fullWithoutAnnotations());
124124
} else if ("java.util.Map".equals(mainType)) {
125125
types.add(genericType.param1().fullWithoutAnnotations());

validator-generator/src/test/java/io/avaje/validation/generator/models/valid/Contact.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class Contact {
99
public String firstName;
1010
public String lastName;
1111

12+
@Valid
1213
public Optional<Address> address;
1314

1415
public Contact() {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.avaje.validation.generator.models.valid.optional;
2+
3+
import java.util.Optional;
4+
import java.util.OptionalDouble;
5+
import java.util.OptionalInt;
6+
import java.util.OptionalLong;
7+
8+
import io.avaje.validation.constraints.NotBlank;
9+
import io.avaje.validation.constraints.Positive;
10+
import io.avaje.validation.constraints.Valid;
11+
12+
@Valid
13+
public record CurseBearer(
14+
@NotBlank(message = "it'll happen to you too") Optional<String> name,
15+
@Positive OptionalInt estus,
16+
@Positive(message = "You Died") OptionalLong souls,
17+
@Positive(message = "you didn't pass the vigor check") OptionalDouble vigor,
18+
@Valid Optional<DarkSign> ds) {
19+
20+
public record DarkSign(@NotBlank(message = "not cursed") String brand) {}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.avaje.validation.adapter;
2+
3+
import java.util.Optional;
4+
5+
final class OptionalAdapter<T> implements ValidationAdapter<Optional<T>> {
6+
7+
private final ValidationAdapter<T> adapter;
8+
9+
OptionalAdapter(ValidationAdapter<T> adapter) {
10+
this.adapter = adapter;
11+
}
12+
13+
@Override
14+
public boolean validate(Optional<T> value, ValidationRequest req, String propertyName) {
15+
if (value == null) {
16+
return true;
17+
}
18+
19+
value.ifPresent(v -> adapter.validate(v, req, propertyName));
20+
return true;
21+
}
22+
}

validator/src/main/java/io/avaje/validation/adapter/OptionalValidationAdapter.java renamed to validator/src/main/java/io/avaje/validation/adapter/PrimitiveOptional.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package io.avaje.validation.adapter;
22

3-
import java.util.Optional;
43
import java.util.OptionalDouble;
54
import java.util.OptionalInt;
65
import java.util.OptionalLong;
76

8-
final class OptionalValidationAdapter<T> extends ContainerAdapter<T> {
7+
final class PrimitiveOptional<T> extends ContainerAdapter<T> {
98

10-
OptionalValidationAdapter(ValidationAdapter<T> adapters) {
11-
super(adapters);
9+
PrimitiveOptional(ValidationAdapter<T> adapter) {
10+
super(adapter);
1211
}
1312

1413
@Override
@@ -17,9 +16,7 @@ public boolean validate(T value, ValidationRequest req, String propertyName) {
1716
if (value == null) {
1817
return true;
1918
}
20-
if (value instanceof final Optional<?> o) {
21-
o.ifPresent(v -> initalAdapter.validate((T) v, req, propertyName));
22-
} else if (value instanceof final OptionalInt i) {
19+
if (value instanceof final OptionalInt i) {
2320
i.ifPresent(v -> initalAdapter.validate((T) (Integer) v, req, propertyName));
2421
} else if (value instanceof final OptionalLong l) {
2522
l.ifPresent(v -> initalAdapter.validate((T) (Long) v, req, propertyName));

validator/src/main/java/io/avaje/validation/adapter/ValidationAdapter.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.avaje.validation.adapter;
22

33
import java.util.Objects;
4+
import java.util.Optional;
45
import java.util.Set;
56

67
/**
@@ -80,8 +81,17 @@ default ContainerAdapter<T> array() {
8081
*
8182
* @return The adapter for optional value validation
8283
*/
83-
default ContainerAdapter<T> optional() {
84-
return new OptionalValidationAdapter<>(this);
84+
default ValidationAdapter<Optional<T>> optional() {
85+
return new OptionalAdapter<>(this);
86+
}
87+
88+
/**
89+
* Create an adapter for validating a primitive optional value. (OptionalInt, OptionalDouble, etc.)
90+
*
91+
* @return The adapter for optional value validation
92+
*/
93+
default ValidationAdapter<T> primitiveOptional() {
94+
return new PrimitiveOptional<>(this);
8595
}
8696

8797
/**

0 commit comments

Comments
 (0)