Skip to content

Commit

Permalink
Add parameter and response body mapping (#1089)
Browse files Browse the repository at this point in the history
* Add parameter mappings functionality
  • Loading branch information
andriy-dmytruk authored Jun 30, 2023
1 parent a41cff4 commit 4ad0c31
Show file tree
Hide file tree
Showing 18 changed files with 922 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def openapiGenerate = tasks.register("generateOpenApi", OpenApiGeneratorTask) {
outputDirectory.convention(layout.buildDirectory.dir("generated/openapi"))
generatorKind.convention("client")
outputKinds.convention(["models", "apis", "supportingFiles", "modelTests", "apiTests"])
parameterMappings.convention([])
responseBodyMappings.convention([])
}

sourceSets {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* A task which simulates what the Gradle Micronaut plugin
Expand Down Expand Up @@ -70,6 +71,12 @@ public Provider<Directory> getGeneratedTestSourcesDirectory() {
@Input
public abstract ListProperty<String> getOutputKinds();

@Input
public abstract ListProperty<Map<String, String>> getParameterMappings();

@Input
public abstract ListProperty<Map<String, String>> getResponseBodyMappings();


@Inject
protected abstract ExecOperations getExecOperations();
Expand All @@ -80,6 +87,7 @@ public void execute() throws IOException {
var generatedTestSourcesDir = getGeneratedTestSourcesDirectory().get().getAsFile();
Files.createDirectories(generatedSourcesDir.toPath());
Files.createDirectories(generatedTestSourcesDir.toPath());
getProject().getLogger().info("json: " + getParameterMappings().get());
getExecOperations().javaexec(javaexec -> {
javaexec.setClasspath(getClasspath());
javaexec.getMainClass().set("io.micronaut.openapi.testsuite.GeneratorMain");
Expand All @@ -88,6 +96,8 @@ public void execute() throws IOException {
args.add(getOpenApiDefinition().get().getAsFile().toURI().toString());
args.add(getOutputDirectory().get().getAsFile().getAbsolutePath());
args.add(String.join(",", getOutputKinds().get()));
args.add(getParameterMappings().get().toString());
args.add(getResponseBodyMappings().get().toString());
javaexec.args(args);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.samskivert.mustache.Template;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConstants;
Expand All @@ -41,12 +42,17 @@
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static org.openapitools.codegen.CodegenConstants.INVOKER_PACKAGE;
Expand Down Expand Up @@ -94,6 +100,8 @@ public abstract class AbstractMicronautJavaCodegen<T extends GeneratorOptionsBui
protected String generateSwaggerAnnotations;
protected boolean generateOperationOnlyForFirstTag;
protected String serializationLibrary = SerializationLibraryKind.MICRONAUT_SERDE_JACKSON.name();
protected List<ParameterMapping> parameterMappings = new ArrayList<>();
protected List<ResponseBodyMapping> responseBodyMappings = new ArrayList<>();

protected AbstractMicronautJavaCodegen() {
super();
Expand Down Expand Up @@ -372,6 +380,14 @@ public void processOpts() {
additionalProperties.put("indent", new Formatting.IndentFormatter(4));
}

public void addParameterMappings(List<ParameterMapping> parameterMappings) {
this.parameterMappings.addAll(parameterMappings);
}

public void addResponseBodyMappings(List<ResponseBodyMapping> responseBodyMappings) {
this.responseBodyMappings.addAll(responseBodyMappings);
}

// CHECKSTYLE:OFF
private void maybeSetSwagger() {
if (additionalProperties.containsKey(OPT_GENERATE_SWAGGER_ANNOTATIONS)) {
Expand Down Expand Up @@ -571,6 +587,130 @@ public CodegenModel fromModel(String name, Schema model) {
return codegenModel;
}


@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation,
List<Server> servers) {
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);

op.vendorExtensions.put("originalParams", new ArrayList(op.allParams));
op.vendorExtensions.put("originReturnProperty", op.returnProperty);
processParametersWithAdditionalMappings(op.allParams, op.imports);
processWithResponseBodyMapping(op);

return op;
}

/**
* Method that maps parameters if a corresponding mapping is specified.
*
* @param params The parameters to modify.
* @param imports The operation imports.
*/
private void processParametersWithAdditionalMappings(List<CodegenParameter> params, Set<String> imports) {
Map<String, ParameterMapping> additionalMappings = new LinkedHashMap<>();
Iterator<CodegenParameter> iter = params.iterator();
while (iter.hasNext()) {
CodegenParameter param = iter.next();
boolean paramWasMapped = false;
for (ParameterMapping mapping : parameterMappings) {
if (mapping.doesMatch(param)) {
additionalMappings.put(mapping.mappedName(), mapping);
paramWasMapped = true;
}
}
if (paramWasMapped) {
iter.remove();
}
}

for (ParameterMapping mapping : additionalMappings.values()) {
if (mapping.mappedType() != null) {
CodegenParameter newParam = new CodegenParameter();
newParam.paramName = mapping.mappedName();
newParam.required = true;
newParam.isModel = mapping.isValidated;

String typeName = makeSureImported(mapping.mappedType(), imports);
newParam.dataType = typeName;

// Set the paramName if required
if (newParam.paramName == null) {
newParam.paramName = toParamName(typeName);
}

params.add(newParam);
}
}
}

/**
* Method that changes the return type if the corresponding header is specified.
*
* @param op The operation to modify.
*/
private void processWithResponseBodyMapping(CodegenOperation op) {
ResponseBodyMapping bodyMapping = null;

Iterator<CodegenProperty> iter = op.responseHeaders.iterator();
while (iter.hasNext()) {
CodegenProperty header = iter.next();
boolean headerWasMapped = false;
for (ResponseBodyMapping mapping : responseBodyMappings) {
if (mapping.doesMatch(header.baseName, op.isArray)) {
if (mapping.mappedBodyType() != null) {
bodyMapping = mapping;
}
headerWasMapped = true;
}
}
if (headerWasMapped) {
iter.remove();
}
}

if (bodyMapping != null) {
CodegenProperty newProperty = new CodegenProperty();
newProperty.required = true;
newProperty.isModel = bodyMapping.isValidated;

String typeName = makeSureImported(bodyMapping.mappedBodyType(), op.imports);

if (bodyMapping.isListWrapper) {
newProperty.dataType = typeName + '<' + op.returnBaseType + '>';
newProperty.items = op.returnProperty.items;
} else {
newProperty.dataType = typeName + '<' + op.returnType + '>';
newProperty.items = op.returnProperty;
}

op.returnType = newProperty.dataType;
op.returnContainer = null;
op.returnProperty = newProperty;
}
}

private String makeSureImported(String typeName, Set<String> imports) {
// Find the index of the first capital letter
int firstCapitalIndex = 0;
for (int i = 0; i < typeName.length(); i++) {
if (Character.isUpperCase(typeName.charAt(i))) {
firstCapitalIndex = i;
break;
}
}

// Add import if the name is fully-qualified
if (firstCapitalIndex != 0) {
// Add import if fully-qualified name is used
String dataType = typeName.substring(firstCapitalIndex);
importMapping.put(dataType, typeName);
typeName = dataType;
}
imports.add(typeName);
return typeName;
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);
Expand Down Expand Up @@ -746,6 +886,78 @@ private static class ReplaceDotsWithUnderscoreLambda implements Mustache.Lambda
public void execute(final Template.Fragment fragment, final Writer writer) throws IOException {
writer.write(fragment.execute().replace('.', '_'));
}
}

/**
* A record that can be used to specify parameter mapping.
* Parameter mapping would map a given parameter to a specific type and name.
*
* @param name The name of the parameter as described by the name field in specification.
* @param location The location of parameter. Path parameters cannot be mapped, as this
* behavior should not be used.
* @param mappedType The type to which the parameter should be mapped. If multiple parameters
* have the same mapping, only one parameter will be present. If set to null,
* the original parameter will be deleted.
* @param mappedName The unique name of the parameter to be used as method parameter name.
* @param isValidated Whether the mapped parameter requires validation.
*/
public record ParameterMapping (
String name,
ParameterLocation location,
String mappedType,
String mappedName,
boolean isValidated
) {
private boolean doesMatch(CodegenParameter parameter) {
if (name != null && !name.equals(parameter.baseName)) {
return false;
}
if (location == null) {
return true;
}
return switch (location) {
case HEADER -> parameter.isHeaderParam;
case QUERY -> parameter.isQueryParam;
case FORM -> parameter.isFormParam;
case COOKIE -> parameter.isCookieParam;
case BODY -> parameter.isBodyParam;
};
}

/**
* The location of the parameter to be mapped.
*/
public enum ParameterLocation {
HEADER,
QUERY,
FORM,
COOKIE,
BODY
}
}

/**
* A record that can be used to specify parameter mapping.
* Parameter mapping would map a given parameter to a specific type and name.
*
* @param headerName The response header name that triggers the change of response type.
* @param mappedBodyType The type in which will be used as the response type. The type must take
* a single type parameter, which will be the original body.
* @param isListWrapper Whether the mapped body type needs to be supplied list items
* as property.
* @param isValidated Whether the mapped response body type required validation.
*/
public record ResponseBodyMapping (
String headerName,
String mappedBodyType,
boolean isListWrapper,
boolean isValidated
) {
private boolean doesMatch(String header, boolean isBodyList) {
if (isListWrapper && !isBodyList) {
return false;
}
return Objects.equals(headerName, header);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.net.URI;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

Expand Down Expand Up @@ -118,6 +119,12 @@ private void configureOptions() {
if (options.artifactId != null) {
codeGenerator.setArtifactId(options.artifactId);
}
if (options.parameterMappings != null) {
codeGenerator.addParameterMappings(options.parameterMappings);
}
if (options.responseBodyMappings != null) {
codeGenerator.addResponseBodyMappings(options.responseBodyMappings);
}
codeGenerator.setReactive(options.reactive);
codeGenerator.setWrapInHttpResponse(options.wrapInHttpResponse);
codeGenerator.setUseOptional(options.optional);
Expand Down Expand Up @@ -288,6 +295,8 @@ private static class DefaultOptionsBuilder implements MicronautCodeGeneratorOpti
private boolean beanValidation = true;
private String invokerPackage;
private String modelPackage;
private List<AbstractMicronautJavaCodegen.ParameterMapping> parameterMappings;
private List<AbstractMicronautJavaCodegen.ResponseBodyMapping> responseBodyMappings;
private boolean optional = false;
private boolean reactive = true;
private boolean wrapInHttpResponse;
Expand Down Expand Up @@ -318,6 +327,18 @@ public MicronautCodeGeneratorOptionsBuilder withArtifactId(String artifactId) {
return this;
}

@Override
public MicronautCodeGeneratorOptionsBuilder withParameterMappings(List<AbstractMicronautJavaCodegen.ParameterMapping> parameterMappings) {
this.parameterMappings = parameterMappings;
return this;
}

@Override
public MicronautCodeGeneratorOptionsBuilder withResponseBodyMappings(List<AbstractMicronautJavaCodegen.ResponseBodyMapping> responseBodyMappings) {
this.responseBodyMappings = responseBodyMappings;
return this;
}

@Override
public MicronautCodeGeneratorOptionsBuilder withReactive(boolean reactive) {
this.reactive = reactive;
Expand Down Expand Up @@ -355,7 +376,7 @@ public MicronautCodeGeneratorOptionsBuilder withSerializationLibrary(Serializati
}

private Options build() {
return new Options(apiPackage, modelPackage, invokerPackage, artifactId, beanValidation, optional, reactive, wrapInHttpResponse, testFramework, serializationLibraryKind);
return new Options(apiPackage, modelPackage, invokerPackage, artifactId, parameterMappings, responseBodyMappings, beanValidation, optional, reactive, wrapInHttpResponse, testFramework, serializationLibraryKind);
}
}
}
Expand All @@ -380,6 +401,8 @@ private record Options(
String modelPackage,
String invokerPackage,
String artifactId,
List<AbstractMicronautJavaCodegen.ParameterMapping> parameterMappings,
List<AbstractMicronautJavaCodegen.ResponseBodyMapping> responseBodyMappings,
boolean beanValidation,
boolean optional,
boolean reactive,
Expand Down
Loading

0 comments on commit 4ad0c31

Please sign in to comment.