[Java] fix JsonCreator with JsonNullable (#12813)

This commit is contained in:
Oleh Kurpiak
2022-07-12 05:41:55 +03:00
committed by GitHub
parent 073a800464
commit 4bbfa01ba8
12 changed files with 231 additions and 7 deletions

View File

@@ -2053,6 +2053,22 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
}
}
/**
* Search for property by {@link CodegenProperty#name}
* @param name - name to search for
* @param properties - list of properties
* @return either found property or {@link Optional#empty()} if nothing has been found
*/
protected Optional<CodegenProperty> findByName(String name, List<CodegenProperty> properties) {
if (properties == null || properties.isEmpty()) {
return Optional.empty();
}
return properties.stream()
.filter(p -> p.name.equals(name))
.findFirst();
}
/**
* This method removes all implicit header parameters from the list of parameters
*

View File

@@ -159,6 +159,8 @@ public class JavaCXFClientCodegen extends AbstractJavaCodegen
if (openApiNullable) {
if (Boolean.FALSE.equals(property.required) && Boolean.TRUE.equals(property.isNullable)) {
property.getVendorExtensions().put("x-is-jackson-optional-nullable", true);
findByName(property.name, model.readOnlyVars)
.ifPresent(p -> p.getVendorExtensions().put("x-is-jackson-optional-nullable", true));
model.imports.add("JsonNullable");
model.imports.add("JsonIgnore");
}

View File

@@ -926,6 +926,8 @@ public class JavaClientCodegen extends AbstractJavaCodegen
// only add JsonNullable and related imports to optional and nullable values
addImports |= isOptionalNullable;
var.getVendorExtensions().put("x-is-jackson-optional-nullable", isOptionalNullable);
findByName(var.name, cm.readOnlyVars)
.ifPresent(p -> p.getVendorExtensions().put("x-is-jackson-optional-nullable", isOptionalNullable));
}
if (Boolean.TRUE.equals(var.getVendorExtensions().get("x-enum-as-string"))) {

View File

@@ -100,7 +100,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
) {
this();
{{#readOnlyVars}}
this.{{name}} = {{name}};
this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};
{{/readOnlyVars}}
}{{/jackson}}{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}}
{{#vars}}

View File

@@ -100,7 +100,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
) {
this();
{{#readOnlyVars}}
this.{{name}} = {{name}};
this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};
{{/readOnlyVars}}
}{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}}
{{#vars}}

View File

@@ -93,12 +93,12 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
{{#jsonb}}@JsonbCreator{{/jsonb}}{{#jackson}}@JsonCreator{{/jackson}}
public {{classname}}(
{{#readOnlyVars}}
{{#jsonb}}@JsonbProperty("{{baseName}}"){{/jsonb}}{{#jackson}}@JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}){{/jackson}} {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}
{{#jsonb}}@JsonbProperty(value = "{{baseName}}"{{^required}}, nillable = true{{/required}}){{/jsonb}}{{#jackson}}@JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}){{/jackson}} {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}
{{/readOnlyVars}}
) {
this();
{{#readOnlyVars}}
this.{{name}} = {{name}};
this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};
{{/readOnlyVars}}
}{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}}
{{#vars}}

View File

@@ -1534,10 +1534,42 @@ public class JavaClientCodegenTest {
.collect(Collectors.toMap(File::getName, Function.identity()));
JavaFileAssert.assertThat(files.get("Foo.java"))
.assertConstructor("String", "Integer")
.hasParameter("b")
.assertParameterAnnotations()
.containsWithNameAndAttributes("JsonbProperty", ImmutableMap.of("value", "\"b\"", "nillable", "true"))
.toParameter().toConstructor()
.hasParameter("c")
.assertParameterAnnotations()
.containsWithNameAndAttributes("JsonbProperty", ImmutableMap.of("value", "\"c\""));
}
@Test
public void testWebClientJsonCreatorWithNullable_issue12790() throws Exception {
Map<String, Object> properties = new HashMap<>();
properties.put(AbstractJavaCodegen.OPENAPI_NULLABLE, "true");
File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();
final CodegenConfigurator configurator = new CodegenConfigurator()
.setAdditionalProperties(properties)
.setGeneratorName("java")
.setLibrary(JavaClientCodegen.WEBCLIENT)
.setInputSpec("src/test/resources/bugs/issue_12790.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
final ClientOptInput clientOptInput = configurator.toClientOptInput();
DefaultGenerator generator = new DefaultGenerator();
Map<String, File> files = generator.opts(clientOptInput).generate().stream()
.collect(Collectors.toMap(File::getName, Function.identity()));
JavaFileAssert.assertThat(files.get("TestObject.java"))
.printFileContent()
.fileContains(
"@JsonbProperty(value = \"b\", nillable = true) String b",
"@JsonbProperty(value = \"c\") Integer c"
.assertConstructor("String", "String")
.bodyContainsLines(
"this.nullableProperty = nullableProperty == null ? JsonNullable.<String>undefined() : JsonNullable.of(nullableProperty);",
"this.notNullableProperty = notNullableProperty;"
);
}

View File

@@ -0,0 +1,97 @@
package org.openapitools.codegen.java.assertions;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.nodeTypes.NodeWithName;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.util.CanIgnoreReturnValue;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
@CanIgnoreReturnValue
public class ConstructorAssert extends AbstractAssert<ConstructorAssert, ConstructorDeclaration> {
private final JavaFileAssert fileAssert;
private final String signature;
ConstructorAssert(final JavaFileAssert fileAssert, final ConstructorDeclaration constructorDeclaration) {
super(constructorDeclaration, ConstructorAssert.class);
this.fileAssert = fileAssert;
this.signature = constructorDeclaration.getDeclarationAsString();
}
public JavaFileAssert toFileAssert() {
return fileAssert;
}
public MethodAnnotationAssert assertConstructorAnnotations() {
return new MethodAnnotationAssert(this, actual.getAnnotations());
}
public ParameterAssert hasParameter(final String paramName) {
final Optional<Parameter> parameter = actual.getParameterByName(paramName);
Assertions.assertThat(parameter)
.withFailMessage("Constructor %s should have parameter %s, but it doesn't", signature, paramName)
.isPresent();
return new ParameterAssert(this, parameter.get());
}
public ConstructorAssert doesNotHaveParameter(final String paramName) {
Assertions.assertThat(actual.getParameterByName(paramName))
.withFailMessage("Constructor %s shouldn't have parameter %s, but it does", signature, paramName)
.isEmpty();
return this;
}
public ConstructorAssert bodyContainsLines(final String... lines) {
final String actualBody = actual.getTokenRange()
.orElseThrow(() -> new IllegalStateException("Can't get constructor body"))
.toString();
Assertions.assertThat(actualBody)
.withFailMessage(
"Constructor's %s body should contains lines\n====\n%s\n====\nbut actually was\n====\n%s\n====",
signature, Arrays.stream(lines).collect(Collectors.joining(System.lineSeparator())), actualBody
)
.contains(lines);
return this;
}
public ConstructorAssert doesNotHaveComment() {
Assertions.assertThat(actual.getJavadocComment())
.withFailMessage("Constructor %s shouldn't contains comment, but it does", signature)
.isEmpty();
return this;
}
public ConstructorAssert commentContainsLines(final String... lines) {
Assertions.assertThat(actual.getJavadocComment())
.withFailMessage("Constructor %s should contains comment, but it doesn't", signature)
.isPresent();
final String actualComment = actual.getJavadocComment().get().getContent();
Assertions.assertThat(actualComment)
.withFailMessage(
"Constructor's %s comment should contains lines\n====\n%s\n====\nbut actually was\n====%s\n====",
signature, Arrays.stream(lines).collect(Collectors.joining(System.lineSeparator())), actualComment
)
.contains(lines);
return this;
}
public ConstructorAssert noneOfParameterHasAnnotation(final String annotationName) {
actual.getParameters()
.forEach(
param -> Assertions.assertThat(param.getAnnotations())
.withFailMessage("Parameter %s contains annotation %s while it shouldn't", param.getNameAsString(), annotationName)
.extracting(NodeWithName::getNameAsString)
.doesNotContain(annotationName)
);
return this;
}
}

View File

@@ -8,6 +8,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.util.CanIgnoreReturnValue;
@@ -59,6 +60,15 @@ public class JavaFileAssert extends AbstractAssert<JavaFileAssert, CompilationUn
return new MethodAssert(this, methods.get(0));
}
public ConstructorAssert assertConstructor(final String... paramTypes) {
Optional<ConstructorDeclaration> constructorDeclaration = actual.getType(0).getConstructorByParameterTypes(paramTypes);
Assertions.assertThat(constructorDeclaration)
.withFailMessage("No constructor with parameter(s) %s", Arrays.toString(paramTypes))
.isPresent();
return new ConstructorAssert(this, constructorDeclaration.get());
}
public PropertyAssert hasProperty(final String propertyName) {
Optional<FieldDeclaration> fieldOptional = actual.getType(0).getMembers().stream()
.filter(FieldDeclaration.class::isInstance)

View File

@@ -10,13 +10,32 @@ import com.github.javaparser.ast.expr.AnnotationExpr;
public class MethodAnnotationAssert extends AbstractAnnotationAssert<MethodAnnotationAssert> {
private final MethodAssert methodAssert;
private final ConstructorAssert constructorAssert;
protected MethodAnnotationAssert(final MethodAssert methodAssert, final List<AnnotationExpr> annotationExpr) {
super(annotationExpr);
this.methodAssert = methodAssert;
this.constructorAssert = null;
}
protected MethodAnnotationAssert(final ConstructorAssert constructorAssert, final List<AnnotationExpr> annotationExpr) {
super(annotationExpr);
this.constructorAssert = constructorAssert;
this.methodAssert = null;
}
public MethodAssert toMethod() {
if (methodAssert == null) {
throw new IllegalArgumentException("No method assert for constructor's annotations");
}
return methodAssert;
}
public ConstructorAssert toConstructor() {
if (constructorAssert == null) {
throw new IllegalArgumentException("No constructor assert for method's annotations");
}
return constructorAssert;
}
}

View File

@@ -10,16 +10,34 @@ import com.github.javaparser.ast.body.Parameter;
public class ParameterAssert extends ObjectAssert<Parameter> {
private final MethodAssert methodAssert;
private final ConstructorAssert constructorAssert;
protected ParameterAssert(final MethodAssert methodAssert, final Parameter parameter) {
super(parameter);
this.methodAssert = methodAssert;
this.constructorAssert = null;
}
protected ParameterAssert(final ConstructorAssert constructorAssert, final Parameter parameter) {
super(parameter);
this.constructorAssert = constructorAssert;
this.methodAssert = null;
}
public MethodAssert toMethod() {
if (methodAssert == null) {
throw new IllegalArgumentException("No method assert for constructor's parameter");
}
return methodAssert;
}
public ConstructorAssert toConstructor() {
if (constructorAssert == null) {
throw new IllegalArgumentException("No constructor assert for method's parameter");
}
return constructorAssert;
}
public ParameterAssert withType(final String expectedType) {
Assertions.assertThat(actual.getTypeAsString())
.withFailMessage("Expected parameter to have type %s, but was %s", expectedType, actual.getTypeAsString())

View File

@@ -0,0 +1,28 @@
openapi: 3.0.3
info:
title: Title
version: "1"
paths:
/test:
get:
responses:
200:
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/TestObject'
components:
schemas:
TestObject:
type: object
properties:
nullableProperty:
type: string
nullable: true
readOnly: true
notNullableProperty:
type: string
readOnly: true
notNullablePropertyNotRO:
type: integer