Fix model generation of array with items unique = true (#18104)

* Fix optional empty collection as default

* Fix test

* Fix test

* Fix default value collection handling
This commit is contained in:
Dennis Melzer 2024-03-14 13:51:11 +01:00 committed by GitHub
parent 96bf7ac915
commit 5ed2283e01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 197 additions and 19 deletions

View File

@ -1036,6 +1036,7 @@ public class OpenAPINormalizer {
as.setMaxItems(schema.getMaxItems());
as.setExtensions(schema.getExtensions());
as.setXml(schema.getXml());
as.setUniqueItems(schema.getUniqueItems());
if (schema.getItems() != null) {
// `items` is also a json schema
if (StringUtils.isNotEmpty(schema.getItems().get$ref())) {

View File

@ -1103,13 +1103,13 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
*/
public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
if (schema.getDefault() != null) { // has default value
if (cp.isArray && !cp.getUniqueItems()) { // array
if (cp.isArray) {
List<String> _values = new ArrayList<>();
if (schema.getDefault() instanceof ArrayNode) { // array of default values
ArrayNode _default = (ArrayNode) schema.getDefault();
if (_default.isEmpty()) { // e.g. default: []
return "new ArrayList<>()";
return getDefaultCollectionType(schema);
}
List<String> final_values = _values;
@ -1155,14 +1155,12 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
defaultValue = StringUtils.join(_values, ", ");
}
} else {
return "new ArrayList<>()";
return getDefaultCollectionType(schema);
}
return String.format(Locale.ROOT, "new ArrayList<>(Arrays.asList(%s))", defaultValue);
} else if (cp.isArray && cp.getUniqueItems()) { // set
// TODO
return null;
} else if (cp.isMap) { // map
return getDefaultCollectionType(schema, defaultValue);
}
if (cp.isMap) { // map
// TODO
return null;
} else {
@ -1181,18 +1179,10 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
// nullable or containerDefaultToNull set to true
if (cp.isNullable || containerDefaultToNull) {
return null;
} else {
if (ModelUtils.isSet(schema)) {
return String.format(Locale.ROOT, "new %s<>()",
instantiationTypes().getOrDefault("set", "LinkedHashSet"));
} else {
return String.format(Locale.ROOT, "new %s<>()",
instantiationTypes().getOrDefault("array", "ArrayList"));
}
}
} else { // has default value
return toArrayDefaultValue(cp, schema);
return getDefaultCollectionType(schema);
}
return toArrayDefaultValue(cp, schema);
} else if (ModelUtils.isMapSchema(schema) && !(ModelUtils.isComposedSchema(schema))) {
if (schema.getProperties() != null && schema.getProperties().size() > 0) {
// object is complex object with free-form additional properties
@ -1202,7 +1192,8 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
return null;
}
if (cp.isNullable || containerDefaultToNull) { // nullable or containerDefaultToNull set to true
// nullable or containerDefaultToNull set to true
if (cp.isNullable || containerDefaultToNull) {
return null;
}
@ -1290,6 +1281,24 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
return super.toDefaultValue(schema);
}
private String getDefaultCollectionType(Schema schema) {
return getDefaultCollectionType(schema, null);
}
private String getDefaultCollectionType(Schema schema, String defaultValues) {
String arrayFormat = "new %s<>(Arrays.asList(%s))";
if(defaultValues == null || defaultValues.isEmpty()){
defaultValues = "";
arrayFormat = "new %s<>()";
}
if (ModelUtils.isSet(schema)) {
return String.format(Locale.ROOT, arrayFormat,
instantiationTypes().getOrDefault("set", "LinkedHashSet"),defaultValues);
}
return String.format(Locale.ROOT, arrayFormat, instantiationTypes().getOrDefault("array", "ArrayList"),defaultValues);
}
@Override
public String toDefaultParameterValue(final Schema<?> schema) {
Object defaultValue = schema.get$ref() != null ? ModelUtils.getReferencedSchema(openAPI, schema).getDefault() : schema.getDefault();

View File

@ -161,6 +161,20 @@ public class JavaFileAssert extends AbstractAssert<JavaFileAssert, CompilationUn
return this;
}
public JavaFileAssert fileDoesNotContains(final String... lines) {
final String actualBody = actual.getTokenRange()
.orElseThrow(() -> new IllegalStateException("Empty file"))
.toString();
Assertions.assertThat(actualBody)
.withFailMessage(
"File should not contains lines\n====\n%s\n====\nbut actually was\n====\n%s\n====",
Arrays.stream(lines).collect(Collectors.joining(System.lineSeparator())), actualBody
)
.doesNotContain(lines);
return this;
}
public TypeAnnotationAssert assertTypeAnnotations() {
return new TypeAnnotationAssert(this, actual.getType(0).getAnnotations());
}

View File

@ -4548,4 +4548,46 @@ public class SpringCodegenTest {
.fileContains("private List<String> photoUrls = new ArrayList<>();");
}
@Test
public void testCollectionTypesWithDefaults_issue_18102() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_1/java/issue_18102.yaml", null, new ParseOptions()).getOpenAPI();
SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary(SPRING_CLOUD_LIBRARY);
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "xyz.model");
codegen.additionalProperties().put(CodegenConstants.API_NAME_SUFFIX, "Controller");
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "xyz.controller");
codegen.additionalProperties().put(CodegenConstants.MODEL_NAME_SUFFIX, "Dto");
codegen.setContainerDefaultToNull(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("PetDto.java"))
.fileContains("private List<@Valid TagDto> tags")
.fileContains("private List<@Valid TagDto> tagsDefaultList = new ArrayList<>()")
.fileContains("private Set<@Valid TagDto> tagsUnique")
.fileContains("private Set<@Valid TagDto> tagsDefaultSet = new LinkedHashSet<>();")
.fileContains("private List<String> stringList")
.fileContains("private List<String> stringDefaultList = new ArrayList<>(Arrays.asList(\"A\", \"B\"));")
.fileContains("private List<String> stringEmptyDefaultList = new ArrayList<>();")
.fileContains("Set<String> stringSet")
.fileContains("private Set<String> stringDefaultSet = new LinkedHashSet<>(Arrays.asList(\"A\", \"B\"));")
.fileContains("private Set<String> stringEmptyDefaultSet = new LinkedHashSet<>();")
.fileDoesNotContains("private List<@Valid TagDto> tags = new ArrayList<>()")
.fileDoesNotContains("private Set<@Valid TagDto> tagsUnique = new LinkedHashSet<>()")
.fileDoesNotContains("private List<String> stringList = new ArrayList<>()")
.fileDoesNotContains("private Set<String> stringSet = new LinkedHashSet<>()");
}
}

View File

@ -0,0 +1,112 @@
openapi: 3.1.0
servers:
- url: 'http://petstore.swagger.io/v2'
info:
description: >-
This is a sample server Petstore server. For this sample, you can use the api key
`special-key` to test the authorization filters.
version: 1.0.0
title: OpenAPI Petstore
license:
name: Apache-2.0
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
paths:
'/pet/{petId}':
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
components:
schemas:
Tag:
title: Pet Tag
description: A tag for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
Pet:
title: a Pet
description: A pet for sale in the pet store
type: object
properties:
tags:
type: array
items:
$ref: '#/components/schemas/Tag'
tagsDefaultList:
type: array
default: []
items:
$ref: '#/components/schemas/Tag'
tagsUnique:
type: array
uniqueItems: true
items:
$ref: '#/components/schemas/Tag'
tagsDefaultSet:
type: array
default: [ ]
uniqueItems: true
items:
$ref: '#/components/schemas/Tag'
stringList:
type: array
items:
type: string
stringDefaultList:
type: array
default:
- A
- B
items:
type: string
stringEmptyDefaultList:
type: array
default: []
items:
type: string
stringSet:
type: array
uniqueItems: true
items:
type: string
stringDefaultSet:
type: array
uniqueItems: true
default:
- A
- B
items:
type: string
stringEmptyDefaultSet:
type: array
uniqueItems: true
default: []
items:
type: string