Skip to content

Commit 6a2603f

Browse files
authored
Merge pull request #1 from micronaut-projects/andriy/validation-generic-annotations
Improve validation of generic parameters
2 parents 0784b3e + ecf796e commit 6a2603f

File tree

8 files changed

+179
-53
lines changed

8 files changed

+179
-53
lines changed

openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,11 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
662662
}
663663

664664
for (var param : op.allParams) {
665-
processGenericAnnotations(param.dataType, param.datatypeWithEnum, param.isArray, param.items, param.vendorExtensions);
665+
processGenericAnnotations(param);
666+
}
667+
if (op.returnProperty != null) {
668+
processGenericAnnotations(op.returnProperty);
669+
op.returnType = op.returnProperty.vendorExtensions.get("typeWithEnumWithGenericAnnotations").toString();
666670
}
667671
}
668672

@@ -688,6 +692,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
688692

689693
if (op.isResponseFile) {
690694
op.returnType = typeMapping.get("responseFile");
695+
op.returnProperty.dataType = op.returnType;
696+
op.returnProperty.datatypeWithEnum = op.returnType;
691697
op.imports.add(op.returnType);
692698
}
693699

@@ -806,14 +812,17 @@ private void wrapOperationReturnType(CodegenOperation op, String wrapperType, bo
806812
originalReturnType = "Void";
807813
op.returnProperty = new CodegenProperty();
808814
op.returnProperty.dataType = "Void";
815+
op.returnProperty.openApiType = "";
809816
}
810817
newReturnType.dataType = typeName + '<' + originalReturnType + '>';
811818
newReturnType.items = op.returnProperty;
812819
}
820+
newReturnType.containerTypeMapped = typeName;
821+
newReturnType.containerType = typeName;
813822
op.vendorExtensions.put("originalReturnType", originalReturnType);
814823

815824
op.returnType = newReturnType.dataType;
816-
op.returnContainer = null;
825+
op.returnContainer = newReturnType.containerTypeMapped;
817826
op.returnProperty = newReturnType;
818827
op.isArray = op.returnProperty.isArray;
819828
}
@@ -893,53 +902,57 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
893902
property.vendorExtensions.put("lombok", lombok);
894903
property.vendorExtensions.put("defaultValueIsNotNull", property.defaultValue != null && !property.defaultValue.equals("null"));
895904
property.vendorExtensions.put("isServer", isServer);
896-
processGenericAnnotations(property.dataType, property.datatypeWithEnum, property.isArray, property.items, property.vendorExtensions);
905+
processGenericAnnotations(property);
897906
}
898907
model.vendorExtensions.put("isServer", isServer);
899908
for (var property : model.requiredVars) {
900909
property.vendorExtensions.put("lombok", lombok);
901910
property.vendorExtensions.put("isServer", isServer);
902911
property.vendorExtensions.put("defaultValueIsNotNull", property.defaultValue != null && !property.defaultValue.equals("null"));
903-
processGenericAnnotations(property.dataType, property.datatypeWithEnum, property.isArray, property.items, property.vendorExtensions);
912+
processGenericAnnotations(property);
904913
}
905914
}
906915

907916
return objs;
908917
}
909918

910-
private void processGenericAnnotations(String dataType, String dataTypeWithEnum, boolean isArray, CodegenProperty itemsProp, Map<String, Object> ext) {
919+
private void processGenericAnnotations(CodegenParameter parameter) {
920+
CodegenProperty items = parameter.isMap ? parameter.additionalProperties : parameter.items;
921+
String datatypeWithEnum = parameter.datatypeWithEnum == null ? parameter.dataType : parameter.datatypeWithEnum;
922+
processGenericAnnotations(parameter.dataType, datatypeWithEnum, parameter.isArray, parameter.isMap, parameter.containerTypeMapped, items, parameter.vendorExtensions);
923+
}
924+
925+
private void processGenericAnnotations(CodegenProperty property) {
926+
CodegenProperty items = property.isMap ? property.additionalProperties : property.items;
927+
String datatypeWithEnum = property.datatypeWithEnum == null ? property.dataType : property.datatypeWithEnum;
928+
processGenericAnnotations(property.dataType, datatypeWithEnum, property.isArray, property.isMap, property.containerTypeMapped, items, property.vendorExtensions);
929+
}
930+
931+
private void processGenericAnnotations(String dataType, String dataTypeWithEnum, boolean isArray, boolean isMap, String containerType, CodegenProperty itemsProp, Map<String, Object> ext) {
911932
var typeWithGenericAnnotations = dataType;
912933
var typeWithEnumWithGenericAnnotations = dataTypeWithEnum;
913-
var addedGenericAnnotations = false;
914-
if (useBeanValidation && isArray && itemsProp != null && isPrimitive(itemsProp.openApiType) && dataType.contains("<")) {
915-
var genericAnnotations = genericAnnotations(itemsProp);
916-
if (itemsProp.isArray) {
917-
processGenericAnnotations(itemsProp.dataType, itemsProp.datatypeWithEnum, true, itemsProp.items, itemsProp.vendorExtensions);
918-
typeWithGenericAnnotations = addGenericAnnotations((String) itemsProp.vendorExtensions.get("typeWithGenericAnnotations"), dataType, genericAnnotations);
919-
typeWithEnumWithGenericAnnotations = addGenericAnnotations((String) itemsProp.vendorExtensions.get("typeWithEnumWithGenericAnnotations"), dataTypeWithEnum, genericAnnotations);
920-
} else {
921-
typeWithGenericAnnotations = addGenericAnnotations(dataType, null, genericAnnotations);
922-
typeWithEnumWithGenericAnnotations = addGenericAnnotations(dataTypeWithEnum, null, genericAnnotations);
934+
if (useBeanValidation && itemsProp != null && dataType.contains("<")) {
935+
if (isMap) {
936+
var genericAnnotations = genericAnnotations(itemsProp);
937+
processGenericAnnotations(itemsProp);
938+
typeWithGenericAnnotations = "Map<String, " + genericAnnotations + itemsProp.vendorExtensions.get("typeWithGenericAnnotations") + ">";
939+
typeWithEnumWithGenericAnnotations = "Map<String, " + genericAnnotations + itemsProp.vendorExtensions.get("typeWithEnumWithGenericAnnotations") + ">";
940+
} else if (containerType != null) {
941+
var genericAnnotations = genericAnnotations(itemsProp);
942+
processGenericAnnotations(itemsProp);
943+
typeWithGenericAnnotations = containerType + "<" + genericAnnotations + itemsProp.vendorExtensions.get("typeWithGenericAnnotations") + ">";
944+
typeWithEnumWithGenericAnnotations = containerType + "<" + genericAnnotations + itemsProp.vendorExtensions.get("typeWithEnumWithGenericAnnotations") + ">";
923945
}
924946
}
925947
ext.put("typeWithGenericAnnotations", typeWithGenericAnnotations);
926948
ext.put("typeWithEnumWithGenericAnnotations", typeWithEnumWithGenericAnnotations);
927949
}
928950

929-
private String addGenericAnnotations(String type, String wrapType, String genericAnnotations) {
930-
if (StringUtils.isEmpty(type) || StringUtils.isEmpty(genericAnnotations)) {
931-
return type;
932-
}
933-
var t = wrapType != null ? wrapType : type;
934-
var diamondOpen = t.indexOf('<');
935-
var diamondClose = t.lastIndexOf('>');
936-
var containerType = t.substring(0, diamondOpen);
937-
var elementType = t.substring(diamondOpen + 1, diamondClose);
938-
return containerType + '<' + genericAnnotations + (wrapType != null ? type : elementType) + '>';
939-
}
940-
941951
private boolean isPrimitive(String type) {
942-
return switch (type.toLowerCase()) {
952+
if (type == null) {
953+
return false;
954+
}
955+
return switch (type) {
943956
case "array", "string", "boolean", "byte", "uri", "url", "uuid", "email", "integer", "long", "float", "double",
944957
"number", "partial-time", "date", "date-time", "bigdecimal", "biginteger" -> true;
945958
default -> false;
@@ -948,11 +961,19 @@ private boolean isPrimitive(String type) {
948961

949962
private String genericAnnotations(CodegenProperty prop) {
950963

951-
var type = prop.openApiType.toLowerCase();
964+
var type = prop.openApiType == null ? null : prop.openApiType.toLowerCase();
952965

953966
var result = new StringBuilder();
967+
968+
if (prop.isModel) {
969+
result.append("@Valid ");
970+
}
971+
if (!isPrimitive(type)) {
972+
return result.toString();
973+
}
974+
954975
if (StringUtils.isNotEmpty(prop.pattern)) {
955-
if (type.equals("email")) {
976+
if ("email".equals(type)) {
956977
result.append("@Email(regexp = \"");
957978
} else {
958979
result.append("@Pattern(regexp = \"");

openapi-generator/src/test/java/io/micronaut/openapi/generator/MicronautServerCodegenTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ void doGenerateMonoWrapHttpResponse() {
207207
String outputPath = generateFiles(codegen, PETSTORE_PATH, CodegenConstants.MODELS, CodegenConstants.APIS);
208208

209209
String apiPath = outputPath + "src/main/java/org/openapitools/api/";
210-
assertFileContains(apiPath + "PetApi.java", "Mono<HttpResponse<Pet>>");
210+
assertFileContains(apiPath + "PetApi.java", "Mono<HttpResponse<@Valid Pet>>");
211211
}
212212

213213
@Test
@@ -219,8 +219,8 @@ void doGenerateMono() {
219219
String outputPath = generateFiles(codegen, PETSTORE_PATH, CodegenConstants.MODELS, CodegenConstants.APIS);
220220

221221
String apiPath = outputPath + "src/main/java/org/openapitools/api/";
222-
assertFileContains(apiPath + "PetApi.java", "Mono<Pet>");
223-
assertFileNotContains(apiPath + "PetApi.java", "Flux<Pet>");
222+
assertFileContains(apiPath + "PetApi.java", "Mono<@Valid Pet>");
223+
assertFileNotContains(apiPath + "PetApi.java", "Flux<@Valid Pet>");
224224
assertFileNotContains(apiPath + "PetApi.java", "HttpResponse");
225225
}
226226

@@ -233,8 +233,8 @@ void doGenerateMonoAndFlux() {
233233
String outputPath = generateFiles(codegen, PETSTORE_PATH, CodegenConstants.MODELS, CodegenConstants.APIS);
234234

235235
String apiPath = outputPath + "src/main/java/org/openapitools/api/";
236-
assertFileContains(apiPath + "PetApi.java", "Mono<Pet>");
237-
assertFileContains(apiPath + "PetApi.java", "Flux<Pet>");
236+
assertFileContains(apiPath + "PetApi.java", "Mono<@Valid Pet>");
237+
assertFileContains(apiPath + "PetApi.java", "Flux<@Valid Pet>");
238238
assertFileNotContains(apiPath + "PetApi.java", "HttpResponse");
239239
}
240240

@@ -246,7 +246,7 @@ void doGenerateWrapHttpResponse() {
246246
String outputPath = generateFiles(codegen, PETSTORE_PATH, CodegenConstants.MODELS, CodegenConstants.APIS);
247247

248248
String apiPath = outputPath + "src/main/java/org/openapitools/api/";
249-
assertFileContains(apiPath + "PetApi.java", "HttpResponse<Pet>");
249+
assertFileContains(apiPath + "PetApi.java", "HttpResponse<@Valid Pet>");
250250
assertFileNotContains(apiPath + "PetApi.java", "Mono");
251251
}
252252

test-suite-server-generator/build.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ dependencies {
2020
because("Required for Swagger")
2121
}
2222

23-
testAnnotationProcessor("io.micronaut.validation:micronaut-validation-processor")
24-
2523
testCompileOnly("io.micronaut:micronaut-inject-groovy-test")
2624
testCompileOnly("io.micronaut:micronaut-inject-java-test")
2725
testImplementation("io.micronaut.test:micronaut-test-spock")

test-suite-server-generator/spec.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,20 @@ paths:
358358
application/json:
359359
schema:
360360
$ref: '#/components/schemas/ModelWithMapProperty'
361+
/sendModelWithValidatedListProperty:
362+
post:
363+
operationId: sendModelWithValidatedListProperty
364+
tags: [ requestBody ]
365+
description: A method to send a model that contains a list with validated items
366+
requestBody:
367+
required: true
368+
content:
369+
application/json:
370+
schema:
371+
$ref: '#/components/schemas/ModelWithValidatedListProperty'
372+
responses:
373+
200:
374+
description: Success
361375
/sendNestedModel:
362376
post:
363377
operationId: sendNestedModel
@@ -537,6 +551,20 @@ paths:
537551
$ref: '#/components/schemas/SimpleModel'
538552
default:
539553
$ref: '#/responses/Error'
554+
/getModelWithValidatedList:
555+
get:
556+
operationId: getModelWithValidatedList
557+
tags: [ responseBody ]
558+
description: A method to get a model with validated list as a response
559+
responses:
560+
200:
561+
description: Success
562+
content:
563+
application/json:
564+
schema:
565+
$ref: '#/components/schemas/ModelWithValidatedListProperty'
566+
default:
567+
$ref: '#/responses/Error'
540568
/getPaginatedSimpleModel:
541569
get:
542570
operationId: getPaginatedSimpleModel
@@ -763,6 +791,26 @@ components:
763791
type: object
764792
additionalProperties:
765793
$ref: '#/components/schemas/SimpleModel'
794+
ModelWithValidatedListProperty:
795+
type: object
796+
properties:
797+
stringList:
798+
type: array
799+
items:
800+
type: string
801+
minLength: 3
802+
objectList:
803+
type: array
804+
maxItems: 2
805+
items:
806+
$ref: '#/components/schemas/SimpleModel'
807+
mapList:
808+
type: array
809+
items:
810+
type: object
811+
additionalProperties:
812+
type: string
813+
minLength: 2
766814
StateEnum:
767815
type: string
768816
enum: ['starting', 'running', 'stopped', 'deleted']

test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/RequestBodyController.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,7 @@
77
import io.micronaut.http.annotation.Controller;
88
import io.micronaut.http.annotation.Post;
99
import io.micronaut.http.multipart.CompletedFileUpload;
10-
import io.micronaut.openapi.test.model.Animal;
11-
import io.micronaut.openapi.test.model.ColorEnum;
12-
import io.micronaut.openapi.test.model.DateModel;
13-
import io.micronaut.openapi.test.model.ModelWithEnumList;
14-
import io.micronaut.openapi.test.model.ModelWithInnerEnum;
15-
import io.micronaut.openapi.test.model.ModelWithMapProperty;
16-
import io.micronaut.openapi.test.model.ModelWithRequiredProperties;
17-
import io.micronaut.openapi.test.model.NestedModel;
18-
import io.micronaut.openapi.test.model.SimpleModel;
10+
import io.micronaut.openapi.test.model.*;
1911

2012
import jakarta.validation.Valid;
2113
import reactor.core.publisher.Mono;
@@ -29,13 +21,13 @@ public String echo(String request) {
2921
}
3022

3123
@Override
32-
public Mono<Void> sendValidatedCollection(List<List<String>> collection) {
33-
return Mono.empty();
24+
public Mono<SimpleModel> sendSimpleModel(SimpleModel simpleModel) {
25+
return Mono.just(simpleModel);
3426
}
3527

3628
@Override
37-
public Mono<SimpleModel> sendSimpleModel(SimpleModel simpleModel) {
38-
return Mono.just(simpleModel);
29+
public Mono<Void> sendValidatedCollection(List<List<String>> requestBody) {
30+
return null;
3931
}
4032

4133
@Override
@@ -69,6 +61,11 @@ public Mono<ModelWithMapProperty> sendModelWithMapProperty(ModelWithMapProperty
6961
return Mono.just(model);
7062
}
7163

64+
@Override
65+
public Mono<Void> sendModelWithValidatedListProperty(ModelWithValidatedListProperty model) {
66+
return Mono.empty();
67+
}
68+
7269
@Override
7370
public Mono<NestedModel> sendNestedModel(NestedModel model) {
7471
return Mono.just(model);

test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/ResponseBodyController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.micronaut.http.server.types.files.StreamedFile;
1919
import io.micronaut.openapi.test.dated.DatedResponse;
2020
import io.micronaut.openapi.test.model.DateModel;
21+
import io.micronaut.openapi.test.model.ModelWithValidatedListProperty;
2122
import io.micronaut.openapi.test.model.SimpleModel;
2223
import io.micronaut.openapi.test.model.StateEnum;
2324

@@ -112,4 +113,10 @@ public Mono<FileCustomizableResponseType> getFile() {
112113
var stream = new ByteArrayInputStream("My file content".getBytes());
113114
return Mono.just(new StreamedFile(stream, MediaType.TEXT_PLAIN_TYPE));
114115
}
116+
117+
@Override
118+
public Mono<ModelWithValidatedListProperty> getModelWithValidatedList() {
119+
List<SimpleModel> objectList = List.of(new SimpleModel().color("a"));
120+
return Mono.just(new ModelWithValidatedListProperty().objectList(objectList));
121+
}
115122
}

0 commit comments

Comments
 (0)