Skip to content

Commit fb3a11c

Browse files
committed
#42 - Support @Valid per methods rather than per controller
1 parent 768f48a commit fb3a11c

File tree

12 files changed

+205
-41
lines changed

12 files changed

+205
-41
lines changed

http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,25 @@
2626
public class ControllerReader {
2727

2828
private final ProcessingContext ctx;
29-
3029
private final TypeElement beanType;
31-
3230
private final List<Element> interfaces;
33-
3431
private final List<ExecutableElement> interfaceMethods;
35-
3632
private final List<String> roles;
37-
3833
private final List<MethodReader> methods = new ArrayList<>();
39-
4034
private final Set<String> staticImportTypes = new TreeSet<>();
41-
4235
private final Set<String> importTypes = new TreeSet<>();
4336

4437
/**
4538
* The produces media type for the controller. Null implies JSON.
4639
*/
4740
private final String produces;
48-
49-
private final boolean includeValidator;
41+
private final boolean hasValid;
42+
private boolean methodHasValid;
5043

5144
/**
5245
* Flag set when the controller is dependant on a request scope type.
5346
*/
5447
private boolean requestScope;
55-
5648
private boolean docHidden;
5749

5850
public ControllerReader(TypeElement beanType, ProcessingContext ctx) {
@@ -64,14 +56,14 @@ public ControllerReader(TypeElement beanType, ProcessingContext ctx) {
6456
if (ctx.isOpenApiAvailable()) {
6557
docHidden = initDocHidden();
6658
}
67-
includeValidator = initIncludeValidator();
59+
this.hasValid = initHasValid();
6860
this.produces = initProduces();
6961
}
7062

7163
protected void addImports(boolean withSingleton) {
7264
importTypes.add(Constants.IMPORT_HTTP_API);
7365
importTypes.add(beanType.getQualifiedName().toString());
74-
if (includeValidator) {
66+
if (hasValid || methodHasValid) {
7567
importTypes.add(Constants.VALIDATOR);
7668
}
7769
if (withSingleton) {
@@ -137,7 +129,7 @@ private boolean initDocHidden() {
137129
return findAnnotation(Hidden.class) != null;
138130
}
139131

140-
private boolean initIncludeValidator() {
132+
private boolean initHasValid() {
141133
return findAnnotation(Valid.class) != null;
142134
}
143135

@@ -154,7 +146,11 @@ public boolean isDocHidden() {
154146
}
155147

156148
public boolean isIncludeValidator() {
157-
return includeValidator;
149+
return hasValid || methodHasValid;
150+
}
151+
152+
public boolean hasValid() {
153+
return hasValid;
158154
}
159155

160156
/**
@@ -166,7 +162,6 @@ boolean isRequestScoped() {
166162
}
167163

168164
public void read(boolean withSingleton) {
169-
addImports(withSingleton);
170165
if (!roles.isEmpty()) {
171166
ctx.platform().controllerRoles(roles, this);
172167
}
@@ -178,6 +173,21 @@ public void read(boolean withSingleton) {
178173
}
179174
}
180175
readSuper(beanType);
176+
deriveIncludeValidation();
177+
addImports(withSingleton);
178+
}
179+
180+
private void deriveIncludeValidation() {
181+
methodHasValid = methodHasValid();
182+
}
183+
184+
private boolean methodHasValid() {
185+
for (MethodReader method : methods) {
186+
if (method.hasValid()) {
187+
return true;
188+
}
189+
}
190+
return false;
181191
}
182192

183193
private void readField(Element element) {
@@ -215,13 +225,11 @@ private void readMethod(ExecutableElement element) {
215225
}
216226

217227
private void readMethod(ExecutableElement method, DeclaredType declaredType) {
218-
219228
ExecutableType actualExecutable = null;
220229
if (declaredType != null) {
221230
// actual taking into account generics
222231
actualExecutable = (ExecutableType) ctx.asMemberOf(declaredType, method);
223232
}
224-
225233
MethodReader methodReader = new MethodReader(this, method, actualExecutable, ctx);
226234
if (methodReader.isWebMethod()) {
227235
methodReader.read();

http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
package io.avaje.http.generator.core;
22

3-
import io.avaje.http.api.BeanParam;
4-
import io.avaje.http.api.Cookie;
5-
import io.avaje.http.api.Default;
6-
import io.avaje.http.api.Form;
7-
import io.avaje.http.api.FormParam;
8-
import io.avaje.http.api.Header;
9-
import io.avaje.http.api.QueryParam;
3+
import io.avaje.http.api.*;
104
import io.avaje.http.generator.core.openapi.MethodDocBuilder;
115
import io.avaje.http.generator.core.openapi.MethodParamDocBuilder;
126

@@ -26,6 +20,7 @@ public class ElementReader {
2620
private final String snakeName;
2721
private final boolean formMarker;
2822
private final boolean contextType;
23+
private final boolean useValidation;
2924

3025
private String paramName;
3126
private ParamType paramType;
@@ -53,11 +48,21 @@ public class ElementReader {
5348
this.paramName = varName;
5449
if (!contextType) {
5550
readAnnotations(element, defaultType);
51+
useValidation = useValidation();
5652
} else {
5753
paramType = ParamType.CONTEXT;
54+
useValidation = false;
5855
}
5956
}
6057

58+
private boolean useValidation() {
59+
if (typeHandler != null) {
60+
return false;
61+
}
62+
TypeElement elementType = ctx.getTypeElement(rawType);
63+
return elementType != null && elementType.getAnnotation(Valid.class) != null;
64+
}
65+
6166
private void readAnnotations(Element element, ParamType defaultType) {
6267

6368
notNullKotlin = (element.getAnnotation(org.jetbrains.annotations.NotNull.class) != null);
@@ -179,17 +184,13 @@ void buildApiDocumentation(MethodDocBuilder methodDoc) {
179184
}
180185

181186
void writeValidate(Append writer) {
182-
if (!isPlatformContext() && typeHandler == null) {
183-
TypeElement formBeanType = ctx.getTypeElement(rawType);
184-
if (formBeanType != null) {
185-
final Valid valid = formBeanType.getAnnotation(Valid.class);
186-
if (valid != null) {
187-
writer.append("validator.validate(%s);", varName).eol();
188-
} else {
189-
writer.append("// no validation required on %s", varName).eol();
190-
}
191-
writer.append(" ");
187+
if (!contextType && typeHandler == null) {
188+
if (useValidation) {
189+
writer.append("validator.validate(%s);", varName).eol();
190+
} else {
191+
writer.append("// no validation required on %s", varName).eol();
192192
}
193+
writer.append(" ");
193194
}
194195
}
195196

http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import javax.lang.model.type.ExecutableType;
1919
import javax.lang.model.type.TypeKind;
2020
import javax.lang.model.type.TypeMirror;
21+
import javax.validation.Valid;
2122
import java.lang.annotation.Annotation;
2223
import java.util.ArrayList;
2324
import java.util.List;
@@ -49,6 +50,7 @@ public class MethodReader {
4950
private final List<? extends TypeMirror> actualParams;
5051

5152
private final PathSegments pathSegments;
53+
private final boolean hasValid;
5254

5355
MethodReader(ControllerReader bean, ExecutableElement element, ExecutableType actualExecutable, ProcessingContext ctx) {
5456
this.ctx = ctx;
@@ -60,11 +62,12 @@ public class MethodReader {
6062
this.methodRoles = Util.findRoles(element);
6163
this.javadoc = Javadoc.parse(ctx.getDocComment(element));
6264
this.produces = produces(bean);
63-
6465
initWebMethodViaAnnotation();
6566
if (isWebMethod()) {
67+
this.hasValid = findAnnotation(Valid.class) != null;
6668
this.pathSegments = PathSegments.parse(Util.combinePath(bean.getPath(), webMethodPath));
6769
} else {
70+
this.hasValid = false;
6871
this.pathSegments = null;
6972
}
7073
}
@@ -233,7 +236,11 @@ public String getFullPath() {
233236
}
234237

235238
public boolean includeValidate() {
236-
return bean.isIncludeValidator();
239+
return bean.hasValid() || hasValid;
240+
}
241+
242+
boolean hasValid() {
243+
return hasValid;
237244
}
238245

239246
public String simpleName() {

tests/test-javalin/src/main/resources/public/openapi.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,32 @@
482482
}
483483
}
484484
},
485+
"/hello/withValidBean" : {
486+
"get" : {
487+
"tags" : [ ],
488+
"summary" : "",
489+
"description" : "",
490+
"parameters" : [ {
491+
"name" : "bean",
492+
"in" : "bean",
493+
"schema" : {
494+
"$ref" : "#/components/schemas/GetBeanForm"
495+
}
496+
} ],
497+
"responses" : {
498+
"200" : {
499+
"description" : "",
500+
"content" : {
501+
"text/plain" : {
502+
"schema" : {
503+
"type" : "string"
504+
}
505+
}
506+
}
507+
}
508+
}
509+
}
510+
},
485511
"/hello/{id}" : {
486512
"delete" : {
487513
"tags" : [ ],
@@ -602,6 +628,22 @@
602628
}
603629
}
604630
},
631+
"GetBeanForm" : {
632+
"type" : "object",
633+
"properties" : {
634+
"name" : {
635+
"maxLength" : 150,
636+
"minLength" : 2,
637+
"type" : "string",
638+
"nullable" : false
639+
},
640+
"email" : {
641+
"maxLength" : 100,
642+
"type" : "string",
643+
"format" : "email"
644+
}
645+
}
646+
},
605647
"HelloDto" : {
606648
"type" : "object",
607649
"properties" : {

tests/test-jex/src/main/java/org/example/Main.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package org.example;
22

3+
import io.avaje.http.api.ValidationException;
34
import io.avaje.inject.ApplicationScope;
45
import io.avaje.inject.BeanScope;
56
import io.avaje.jex.Jex;
67
import io.avaje.jex.Routing;
78

9+
import java.util.LinkedHashMap;
10+
import java.util.Map;
11+
812
public class Main {
913

1014
public static void main(String[] args) {
@@ -18,6 +22,15 @@ public static Jex.Server start(int port) {
1822
public static Jex.Server start(int port, BeanScope context) {
1923
final Jex jex = Jex.create();
2024
jex.routing().addAll(context.list(Routing.Service.class));
25+
26+
jex.exception(ValidationException.class, (exception, ctx) -> {
27+
Map<String, Object> map = new LinkedHashMap<>();
28+
map.put("message", exception.getMessage());
29+
map.put("errors", exception.getErrors());
30+
ctx.status(exception.getStatus());
31+
ctx.json(map);
32+
});
33+
2134
return jex.port(port).start();
2235
}
2336
}

tests/test-jex/src/main/java/org/example/web/HelloController.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package org.example.web;
22

3-
import io.avaje.http.api.Controller;
4-
import io.avaje.http.api.Get;
5-
import io.avaje.http.api.Path;
6-
import io.avaje.http.api.Produces;
3+
import io.avaje.http.api.*;
74
import io.avaje.jex.Context;
85

6+
import javax.validation.Valid;
7+
98
// @Roles(AppRoles.BASIC_USER)
109
@Controller
1110
@Path("/")
@@ -37,4 +36,10 @@ String name(String name) {
3736
String splat(String name, Context ctx) {
3837
return "got name:" + name + " splat0:" + ctx.splat(0) + " splat1:" + ctx.splat(1);
3938
}
39+
40+
@Valid
41+
@Put
42+
void put(HelloDto dto) {
43+
dto.hashCode();
44+
}
4045
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package org.example.web;
22

3+
import javax.validation.Valid;
4+
import javax.validation.constraints.NotNull;
5+
6+
@Valid
37
public class HelloDto {
48
public int id;
9+
@NotNull
510
public String name;
611
}

tests/test-jex/src/main/resources/public/openapi.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@
2222
}
2323
}
2424
}
25+
},
26+
"put" : {
27+
"tags" : [ ],
28+
"summary" : "",
29+
"description" : "",
30+
"requestBody" : {
31+
"content" : {
32+
"application/json" : {
33+
"schema" : {
34+
"$ref" : "#/components/schemas/HelloDto"
35+
}
36+
}
37+
},
38+
"required" : true
39+
},
40+
"responses" : {
41+
"204" : {
42+
"description" : "No content"
43+
}
44+
}
2545
}
2646
},
2747
"/other/{name}" : {

tests/test-jex/src/test/java/org/example/web/BaseWebTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import org.junit.jupiter.api.AfterAll;
1010
import org.junit.jupiter.api.BeforeAll;
1111

12+
import java.time.Duration;
13+
1214
public class BaseWebTest {
1315

1416
static Jex.Server webServer;
@@ -29,6 +31,7 @@ public static void shutdown() {
2931
public static HttpClientContext client() {
3032
return HttpClientContext.newBuilder()
3133
.withBaseUrl(baseUrl)
34+
.withRequestTimeout(Duration.ofMinutes(2))
3235
.withRequestListener(new RequestLogger())
3336
.withBodyAdapter(new JacksonBodyAdapter(new ObjectMapper()))
3437
.build();

0 commit comments

Comments
 (0)