Add bean validation for primitive container type 17450 (#17157)

* Add optional parameter for request body

* Adapt Test

* Add test

* Format code

* Remove extra method

* Format code
This commit is contained in:
Dennis Melzer
2023-11-30 13:28:45 +01:00
committed by GitHub
parent 4c4d0e485a
commit 939ffdd73c
3 changed files with 341 additions and 21 deletions

View File

@@ -21,13 +21,22 @@ import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.StringUtils.camelize;
import com.samskivert.mustache.Mustache;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.openapitools.codegen.CliOption;
@@ -63,23 +72,9 @@ import org.openapitools.codegen.utils.URLPathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.samskivert.mustache.Mustache;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
public class SpringCodegen extends AbstractJavaCodegen
implements BeanValidationFeatures, PerformBeanValidationFeatures, OptionalFeatures, SwaggerUIFeatures {
private final Logger LOGGER = LoggerFactory.getLogger(SpringCodegen.class);
public static final String TITLE = "title";
public static final String SERVER_PORT = "serverPort";
public static final String CONFIG_PACKAGE = "configPackage";
@@ -1458,17 +1453,86 @@ public class SpringCodegen extends AbstractJavaCodegen
return codegenParameter.isContainer && !codegenParameter.isMap;
}
private String replaceBeanValidationCollectionType(CodegenProperty codegenProperty, String dataType) {
if (!useBeanValidation() || !codegenProperty.isModel || isResponseType(codegenProperty)) {
return dataType;
private String replaceBeanValidationCollectionType(CodegenProperty codegenProperty, String dataType) {
if (!useBeanValidation() || isResponseType(codegenProperty)) {
return dataType;
}
if (StringUtils.isEmpty(dataType) || dataType.contains("@Valid")) {
return dataType;
}
if (codegenProperty.isModel) {
return dataType.replace("<", "<@Valid ");
}
String beanValidation = getPrimitiveBeanValidation(codegenProperty);
if (beanValidation == null) {
return dataType;
}
return dataType.replace("<", "<" + beanValidation + " ");
}
/**
* This method should be in sync with beanValidationCore.mustache
* @param codegenProperty the code property
* @return the bean validation semantic for container primitive types
*/
private String getPrimitiveBeanValidation(CodegenProperty codegenProperty) {
if (StringUtils.isNotEmpty(codegenProperty.pattern) && !codegenProperty.isByteArray) {
return "@Pattern(regexp = \""+codegenProperty.pattern+"\")";
}
if (StringUtils.isEmpty( dataType ) || dataType.contains( "@Valid" )) {
return dataType;
if (codegenProperty.minLength != null && codegenProperty.maxLength != null) {
return "@Size(min = " + codegenProperty.minLength + ", max = " + codegenProperty.maxLength + ")";
}
return dataType.replace( "<", "<@Valid " );
if (codegenProperty.minLength != null) {
return "@Size(min = " + codegenProperty.minLength + ")";
}
if (codegenProperty.maxLength != null) {
return "@Size(max = " + codegenProperty.maxLength + ")";
}
if (codegenProperty.isEmail) {
return "@" + additionalProperties.get(JAVAX_PACKAGE)+".validation.constraints.Email";
}
if (codegenProperty.isLong || codegenProperty.isInteger) {
if (StringUtils.isNotEmpty(codegenProperty.minimum) && StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@Min("+codegenProperty.minimum+") @Max("+codegenProperty.maximum+")";
}
if (StringUtils.isNotEmpty(codegenProperty.minimum)) {
return "@Min("+codegenProperty.minimum+")";
}
if (StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@Max("+codegenProperty.maximum+")";
}
}
if (StringUtils.isNotEmpty(codegenProperty.minimum) && StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@DecimalMin(value = \""+codegenProperty.minimum+"\", inclusive = false) @DecimalMax(value = \""+codegenProperty.maximum+"\", inclusive = false)";
}
if (StringUtils.isNotEmpty(codegenProperty.minimum)) {
return "@DecimalMin( value = \""+codegenProperty.minimum+"\", inclusive = false)";
}
if (StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@DecimalMax( value = \""+codegenProperty.maximum+"\", inclusive = false)";
}
return null;
}
public void setResourceFolder( String resourceFolder ) {
this.resourceFolder = resourceFolder;
}

View File

@@ -893,6 +893,101 @@ public class SpringCodegenTest {
.withType( "Set<Integer>" );
}
@Test
public void shouldAddValidAnnotationIntoCollectionWhenBeanValidationIsEnabled_issue17150() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/spring/issue_17150.yaml", null, new ParseOptions()).getOpenAPI();
SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary(SPRING_CLOUD_LIBRARY);
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(SpringCodegen.USE_BEANVALIDATION, "true");
// codegen.additionalProperties().put(SpringCodegen.PERFORM_BEANVALIDATION, "true");
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "xyz.model");
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "xyz.controller");
codegen.setUseSpringBoot3(true);
ClientOptInput input = new ClientOptInput()
.openAPI(openAPI)
.config(codegen);
DefaultGenerator generator = new DefaultGenerator();
Map<String, File> files = generator.opts(input).generate().stream()
.collect(Collectors.toMap(File::getName, Function.identity()));
JavaFileAssert.assertThat(files.get("Foo.java"))
.isNormalClass()
.hasImports("jakarta.validation.Valid")
.hasImports("jakarta.validation.constraints")
.hasProperty("stringPattern")
.withType( "Set<@Pattern(regexp = \"[a-z]\") String>" )
.toType()
.hasProperty("stringMaxMinLength")
.withType( "Set<@Size(min = 1, max = 10) String>" )
.toType()
.hasProperty("stringMinLength")
.withType( "List<@Size(min = 1) String>" )
.toType()
.hasProperty("stringMaxLength")
.withType( "Set<@Size(max = 1) String>" )
.toType()
.hasProperty("intMinMax")
.withType( "List<@Min(1) @Max(10) Integer>" )
.toType()
.hasProperty("intMin")
.withType( "List<@Min(1) Integer>" )
.toType()
.hasProperty("intMax")
.withType( "List<@Max(10) Integer>" )
.toType()
.hasProperty("numberMinMax")
.withType( "List<@DecimalMin(value = \"1\", inclusive = false) @DecimalMax(value = \"10\", inclusive = false) BigDecimal>" )
.toType()
.hasProperty("numberMin")
.withType( "List<@DecimalMin(value = \"1\", inclusive = false) BigDecimal>" )
.toType()
.hasProperty("numberMax")
.withType( "List<@DecimalMax(value = \"10\", inclusive = false) BigDecimal>" )
.toType()
.hasProperty("stringPatternNullable")
.withType( "JsonNullable<Set<@Pattern(regexp = \"[a-z]\") String>>" )
.toType()
.hasProperty("stringMaxMinLengthNullable")
.withType( "JsonNullable<Set<@Size(min = 1, max = 10) String>>" )
.toType()
.hasProperty("stringMinLengthNullable")
.withType( "JsonNullable<List<@Size(min = 1) String>>" )
.toType()
.hasProperty("stringMaxLengthNullable")
.withType( "JsonNullable<Set<@Size(max = 1) String>>" )
.toType()
.hasProperty("intMinMaxNullable")
.withType( "JsonNullable<List<@Min(1) @Max(10) Integer>>" )
.toType()
.hasProperty("intMinNullable")
.withType( "JsonNullable<List<@Min(1) Integer>>" )
.toType()
.hasProperty("intMaxNullable")
.withType( "JsonNullable<List<@Max(10) Integer>>" )
.toType()
.hasProperty("numberMinMaxNullable")
.withType( "JsonNullable<List<@DecimalMin(value = \"1\", inclusive = false) @DecimalMax(value = \"10\", inclusive = false) BigDecimal>>" )
.toType()
.hasProperty("numberMinNullable")
.withType( "JsonNullable<List<@DecimalMin(value = \"1\", inclusive = false) BigDecimal>>" )
.toType()
.hasProperty("numberMaxNullable")
.withType( "JsonNullable<List<@DecimalMax(value = \"10\", inclusive = false) BigDecimal>>" )
.toType()
;
}
// Helper function, intended to reduce boilerplate
private Map<String, File> generateFiles(SpringCodegen codegen, String filePath) throws IOException {
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();

View File

@@ -0,0 +1,161 @@
openapi: 3.0.3
info:
title: Test Issue
version: v1
paths:
/test:
get:
responses:
'200':
description: default response
content:
'*/*':
schema:
$ref: '#/components/schemas/Foo'
components:
schemas:
Foo:
type: object
properties:
stringPattern:
type: array
uniqueItems: true
items:
type: string
pattern: "[a-z]"
stringMaxMinLength:
type: array
uniqueItems: true
items:
type: string
minLength: 1
maxLength: 10
maxItems: 10
stringMinLength:
type: array
items:
type: string
minLength: 1
maxItems: 10
stringMaxLength:
type: array
uniqueItems: true
items:
type: string
maxLength: 1
maxItems: 10
stringEmail:
type: array
items:
type: string
format: email
intMinMax:
type: array
items:
type: integer
minimum: 1
maximum: 10
intMin:
type: array
items:
type: integer
minimum: 1
intMax:
type: array
items:
type: integer
maximum: 10
numberMinMax:
type: array
items:
type: number
minimum: 1
maximum: 10
numberMin:
type: array
items:
type: number
minimum: 1
numberMax:
type: array
items:
type: number
maximum: 10
stringPatternNullable:
nullable: true
type: array
uniqueItems: true
items:
type: string
pattern: "[a-z]"
stringMaxMinLengthNullable:
nullable: true
type: array
uniqueItems: true
items:
type: string
minLength: 1
maxLength: 10
maxItems: 10
stringMinLengthNullable:
nullable: true
type: array
items:
type: string
minLength: 1
maxItems: 10
stringMaxLengthNullable:
nullable: true
type: array
uniqueItems: true
items:
type: string
maxLength: 1
maxItems: 10
stringEmailNullable:
nullable: true
type: array
items:
type: string
format: email
intMinMaxNullable:
nullable: true
type: array
items:
type: integer
minimum: 1
maximum: 10
intMinNullable:
nullable: true
type: array
items:
type: integer
minimum: 1
intMaxNullable:
nullable: true
type: array
items:
type: integer
maximum: 10
numberMinMaxNullable:
nullable: true
type: array
items:
type: number
minimum: 1
maximum: 10
numberMinNullable:
nullable: true
type: array
items:
type: number
minimum: 1
numberMaxNullable:
nullable: true
type: array
items:
type: number
maximum: 10