mirror of
https://github.com/OpenAPITools/openapi-generator.git
synced 2026-03-17 14:19:16 +00:00
New generator - Scala Play Framework (#2421)
* Added new generator for Scala + Play Framework (WIP) * scala-play-framework: default values reintroduced (mostly); datatype -> dataType * reintroduced missing EOF newline * Support single/collection params for header/query params * Rename apiFutures > supportAsync, implStubs > skipStubs (opt-out instead of opt-in) * Deleted license and small fixes * Generate extraction of form parameters from request body * Added missing call to executeApi for unit methods when supportAsync=false * Polished some stuff and added routes, application.conf, logback.xml, better default responses * Disabled generation of Json.format for models with files * Added README * Multiple additions and improvements. - Fix Indentation using mustache lambdas - Option to set routes file name (default: routes) - allows uninterrupted manual maintenance of main routes file, which may include a subroute to the generated routes file - Move supporting file classes to a package and update application.conf generation accordingly - Option to generate custom exceptions (default: true) which are used in the controller to differentiate between API call exceptions and validation exceptions - Generate error handler with basic exception mapping - Option to generate API docs under /api route - Reorder routes file so parameter-less paths are given priority over parameterized paths. Prevents case like /v2/user/:username activating before /v2/user/login (thus shadowing the login route completely) as observed using v3 petstore.yaml - Option to set base package name (default: org.openapitools) to allow placing supporting files under a different package * Revert supportAsync default to false * Added binaries and default api/model packages * Added scala-play-framework sample * Add missing contextPath to README and controller comment
This commit is contained in:
committed by
William Cheng
parent
9e391efd1d
commit
28ae33cb13
@@ -0,0 +1,401 @@
|
||||
package org.openapitools.codegen.languages;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.samskivert.mustache.Mustache;
|
||||
import io.swagger.v3.oas.models.media.ArraySchema;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import org.openapitools.codegen.*;
|
||||
import org.openapitools.codegen.mustache.*;
|
||||
import org.openapitools.codegen.utils.ModelUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.rightPad;
|
||||
import static org.openapitools.codegen.utils.StringUtils.camelize;
|
||||
|
||||
public class ScalaPlayFrameworkServerCodegen extends AbstractScalaCodegen implements CodegenConfig {
|
||||
public static final String TITLE = "title";
|
||||
public static final String SKIP_STUBS = "skipStubs";
|
||||
public static final String SUPPORT_ASYNC = "supportAsync";
|
||||
public static final String GENERATE_CUSTOM_EXCEPTIONS = "generateCustomExceptions";
|
||||
public static final String USE_SWAGGER_UI = "useSwaggerUI";
|
||||
public static final String ROUTES_FILE_NAME = "routesFileName";
|
||||
public static final String BASE_PACKAGE = "basePackage";
|
||||
|
||||
static Logger LOGGER = LoggerFactory.getLogger(ScalaPlayFrameworkServerCodegen.class);
|
||||
|
||||
protected boolean skipStubs = false;
|
||||
protected boolean supportAsync = false;
|
||||
protected boolean generateCustomExceptions = true;
|
||||
protected boolean useSwaggerUI = true;
|
||||
protected String routesFileName = "routes";
|
||||
protected String basePackage = "org.openapitools";
|
||||
|
||||
public ScalaPlayFrameworkServerCodegen() {
|
||||
super();
|
||||
outputFolder = "generated-code" + File.separator + "scala-play-framework";
|
||||
modelTemplateFiles.put("model.mustache", ".scala");
|
||||
apiTemplateFiles.put("api.mustache", ".scala");
|
||||
embeddedTemplateDir = templateDir = "scala-play-framework";
|
||||
hideGenerationTimestamp = false;
|
||||
sourceFolder = "app";
|
||||
apiPackage = "api";
|
||||
modelPackage = "model";
|
||||
|
||||
instantiationTypes.put("map", "Map");
|
||||
instantiationTypes.put("array", "List");
|
||||
|
||||
typeMapping.put("DateTime", "OffsetDateTime");
|
||||
typeMapping.put("Date", "LocalDate");
|
||||
typeMapping.put("Integer", "Int");
|
||||
typeMapping.put("binary", "Array[Byte]");
|
||||
typeMapping.put("ByteArray", "Array[Byte]");
|
||||
typeMapping.put("object", "JsObject");
|
||||
typeMapping.put("file", "TemporaryFile");
|
||||
|
||||
importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
|
||||
importMapping.put("LocalDate", "java.time.LocalDate");
|
||||
importMapping.remove("BigDecimal");
|
||||
importMapping.put("TemporaryFile", "play.api.libs.Files.TemporaryFile");
|
||||
|
||||
cliOptions.add(new CliOption(ROUTES_FILE_NAME, "Name of the routes file to generate.").defaultValue(routesFileName));
|
||||
cliOptions.add(new CliOption(ROUTES_FILE_NAME, "Base package in which supporting classes are generated.").defaultValue(basePackage));
|
||||
|
||||
addCliOptionWithDefault(SKIP_STUBS, "If set, skips generation of stub classes.", skipStubs);
|
||||
addCliOptionWithDefault(SUPPORT_ASYNC, "If set, wraps API return types with Futures and generates async actions.", supportAsync);
|
||||
addCliOptionWithDefault(GENERATE_CUSTOM_EXCEPTIONS, "If set, generates custom exception types.", generateCustomExceptions);
|
||||
addCliOptionWithDefault(USE_SWAGGER_UI, "Add a route to /api which show your documentation in swagger-ui. Will also import needed dependencies", useSwaggerUI);
|
||||
}
|
||||
|
||||
public CodegenType getTag() {
|
||||
return CodegenType.SERVER;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "scala-play-framework";
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
return "Generates a Scala server application with Play Framework.";
|
||||
}
|
||||
|
||||
public void setSupportAsync(boolean supportAsync) {
|
||||
this.supportAsync = supportAsync;
|
||||
}
|
||||
|
||||
public void setSkipStubs(boolean skipStubs) {
|
||||
this.skipStubs = skipStubs;
|
||||
}
|
||||
|
||||
public void setGenerateCustomExceptions(boolean generateCustomExceptions) {
|
||||
this.generateCustomExceptions = generateCustomExceptions;
|
||||
}
|
||||
|
||||
public void setRoutesFileName(String routesFileName) {
|
||||
this.routesFileName = routesFileName;
|
||||
}
|
||||
|
||||
public void setBasePackage(String basePackage) {
|
||||
this.basePackage = basePackage;
|
||||
}
|
||||
|
||||
public void setUseSwaggerUI(boolean useSwaggerUI) {
|
||||
this.useSwaggerUI = useSwaggerUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processOpts() {
|
||||
super.processOpts();
|
||||
|
||||
if (additionalProperties.containsKey(SKIP_STUBS)) {
|
||||
this.setSkipStubs(convertPropertyToBoolean(SKIP_STUBS));
|
||||
}
|
||||
writePropertyBack(SKIP_STUBS, skipStubs);
|
||||
|
||||
if (additionalProperties.containsKey(SUPPORT_ASYNC)) {
|
||||
this.setSupportAsync(convertPropertyToBoolean(SUPPORT_ASYNC));
|
||||
}
|
||||
writePropertyBack(SUPPORT_ASYNC, supportAsync);
|
||||
|
||||
if (additionalProperties.containsKey(GENERATE_CUSTOM_EXCEPTIONS)) {
|
||||
this.setGenerateCustomExceptions(convertPropertyToBoolean(GENERATE_CUSTOM_EXCEPTIONS));
|
||||
}
|
||||
writePropertyBack(GENERATE_CUSTOM_EXCEPTIONS, generateCustomExceptions);
|
||||
|
||||
if (additionalProperties.containsKey(USE_SWAGGER_UI)) {
|
||||
this.setUseSwaggerUI(convertPropertyToBoolean(USE_SWAGGER_UI));
|
||||
}
|
||||
writePropertyBack(USE_SWAGGER_UI, useSwaggerUI);
|
||||
|
||||
if (additionalProperties.containsKey(ROUTES_FILE_NAME)) {
|
||||
this.setRoutesFileName((String)additionalProperties.get(ROUTES_FILE_NAME));
|
||||
} else {
|
||||
additionalProperties.put(ROUTES_FILE_NAME, routesFileName);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(BASE_PACKAGE)) {
|
||||
this.setBasePackage((String)additionalProperties.get(BASE_PACKAGE));
|
||||
} else {
|
||||
additionalProperties.put(BASE_PACKAGE, basePackage);
|
||||
}
|
||||
|
||||
apiTemplateFiles.remove("api.mustache");
|
||||
|
||||
if (!skipStubs) {
|
||||
apiTemplateFiles.put("app/apiImplStubs.scala.mustache", "Impl.scala");
|
||||
}
|
||||
|
||||
apiTemplateFiles.put("app/apiTrait.scala.mustache", ".scala");
|
||||
apiTemplateFiles.put("app/apiController.scala.mustache", "Controller.scala");
|
||||
|
||||
supportingFiles.add(new SupportingFile("README.md.mustache", "", "README.md"));
|
||||
supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt"));
|
||||
supportingFiles.add(new SupportingFile("conf/application.conf.mustache", "conf", "application.conf"));
|
||||
supportingFiles.add(new SupportingFile("conf/logback.xml.mustache", "conf", "logback.xml"));
|
||||
supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties"));
|
||||
supportingFiles.add(new SupportingFile("project/plugins.sbt.mustache", "project", "plugins.sbt"));
|
||||
supportingFiles.add(new SupportingFile("conf/routes.mustache", "conf", routesFileName));
|
||||
supportingFiles.add(new SupportingFile("app/module.scala.mustache", getBasePackagePath(), "Module.scala"));
|
||||
supportingFiles.add(new SupportingFile("app/errorHandler.scala.mustache", getBasePackagePath(), "ErrorHandler.scala"));
|
||||
|
||||
if (generateCustomExceptions) {
|
||||
supportingFiles.add(new SupportingFile("app/exceptions.scala.mustache", getBasePackagePath(), "OpenApiExceptions.scala"));
|
||||
}
|
||||
|
||||
if (this.useSwaggerUI) {
|
||||
//App/Controllers
|
||||
supportingFiles.add(new SupportingFile("public/openapi.json.mustache", "public", "openapi.json"));
|
||||
supportingFiles.add(new SupportingFile("app/apiDocController.scala.mustache", String.format(Locale.ROOT, "app/%s", apiPackage.replace(".", File.separator)), "ApiDocController.scala"));
|
||||
}
|
||||
addMustacheLambdas(additionalProperties);
|
||||
}
|
||||
|
||||
private void addMustacheLambdas(Map<String, Object> objs) {
|
||||
Map<String, Mustache.Lambda> lambdas = new ImmutableMap.Builder<String, Mustache.Lambda>()
|
||||
.put("indented_4", new IndentedLambda(4, " "))
|
||||
.put("indented_8", new IndentedLambda(8, " "))
|
||||
.build();
|
||||
objs.put("lambda", lambdas);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
|
||||
Map<String, CodegenModel> models = new HashMap<>();
|
||||
|
||||
for (Object _mo : allModels) {
|
||||
CodegenModel model = (CodegenModel)((Map<String, Object>)_mo).get("model");
|
||||
models.put(model.classname, model);
|
||||
}
|
||||
|
||||
Map<String, Object> operations = (Map<String, Object>)objs.get("operations");
|
||||
if (operations != null) {
|
||||
List<CodegenOperation> ops = (List<CodegenOperation>)operations.get("operation");
|
||||
for (CodegenOperation operation : ops) {
|
||||
Pattern pathVariableMatcher = Pattern.compile("\\{([^}]+)}");
|
||||
Matcher match = pathVariableMatcher.matcher(operation.path);
|
||||
while (match.find()) {
|
||||
String completeMatch = match.group();
|
||||
String replacement = ":" + camelize(match.group(1), true);
|
||||
operation.path = operation.path.replace(completeMatch, replacement);
|
||||
}
|
||||
|
||||
if ("null".equals(operation.defaultResponse) && models.containsKey(operation.returnType)) {
|
||||
operation.defaultResponse = models.get(operation.returnType).defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objs;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
|
||||
objs = super.postProcessAllModels(objs);
|
||||
Map<String, CodegenModel> modelsByClassName = new HashMap<>();
|
||||
|
||||
for (Object _outer : objs.values()) {
|
||||
Map<String, Object> outer = (Map<String, Object>)_outer;
|
||||
List<Map<String, Object>> models = (List<Map<String, Object>>)outer.get("models");
|
||||
|
||||
for (Map<String, Object> mo : models) {
|
||||
CodegenModel cm = (CodegenModel)mo.get("model");
|
||||
postProcessModelsEnum(outer);
|
||||
cm.classVarName = camelize(cm.classVarName, true);
|
||||
modelsByClassName.put(cm.classname, cm);
|
||||
boolean hasFiles = cm.vars.stream().anyMatch(var -> var.isFile);
|
||||
cm.vendorExtensions.put("hasFiles", hasFiles);
|
||||
}
|
||||
}
|
||||
|
||||
for (CodegenModel model : modelsByClassName.values()) {
|
||||
model.defaultValue = generateModelDefaultValue(model, modelsByClassName);
|
||||
}
|
||||
|
||||
return objs;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
|
||||
objs = super.postProcessSupportingFileData(objs);
|
||||
generateJSONSpecFile(objs);
|
||||
|
||||
// Prettify routes file
|
||||
Map<String, Object> apiInfo = (Map<String, Object>)objs.get("apiInfo");
|
||||
List<Map<String, Object>> apis = (List<Map<String, Object>>)apiInfo.get("apis");
|
||||
List<CodegenOperation> ops = apis.stream()
|
||||
.map(api -> (Map<String, Object>)api.get("operations"))
|
||||
.flatMap(operations -> ((List<CodegenOperation>)operations.get("operation")).stream())
|
||||
.collect(Collectors.toList());
|
||||
int maxPathLength = ops.stream()
|
||||
.mapToInt(op -> op.httpMethod.length() + op.path.length())
|
||||
.reduce(0, Integer::max);
|
||||
ops.forEach(op -> op.vendorExtensions.put("paddedPath", rightPad(op.path, maxPathLength - op.httpMethod.length())));
|
||||
ops.forEach(op -> op.vendorExtensions.put("hasPathParams", op.getHasPathParams()));
|
||||
|
||||
return objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSchemaType(Schema p) {
|
||||
String openAPIType = super.getSchemaType(p);
|
||||
openAPIType = getAlias(openAPIType);
|
||||
|
||||
// don't apply renaming on types from the typeMapping
|
||||
if (typeMapping.containsKey(openAPIType)) {
|
||||
return typeMapping.get(openAPIType);
|
||||
}
|
||||
|
||||
if (null == openAPIType) {
|
||||
LOGGER.error("No Type defined for Schema " + p);
|
||||
}
|
||||
return toModelName(openAPIType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toDefaultValue(Schema p) {
|
||||
if (p.getRequired() != null && p.getRequired().contains(p.getName())) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
if (p.getDefault() != null) {
|
||||
return p.getDefault().toString();
|
||||
}
|
||||
|
||||
if (ModelUtils.isBooleanSchema(p)) {
|
||||
return "false";
|
||||
}
|
||||
|
||||
if (ModelUtils.isDateSchema(p)) {
|
||||
return "LocalDate.now";
|
||||
}
|
||||
|
||||
if (ModelUtils.isDateTimeSchema(p)) {
|
||||
return "OffsetDateTime.now";
|
||||
}
|
||||
|
||||
if (ModelUtils.isDoubleSchema(p)) {
|
||||
return "0.0";
|
||||
}
|
||||
|
||||
if (ModelUtils.isFloatSchema(p)) {
|
||||
return "0.0F";
|
||||
}
|
||||
|
||||
if (ModelUtils.isIntegerSchema(p)) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
if (ModelUtils.isLongSchema(p)) {
|
||||
return "0L";
|
||||
}
|
||||
|
||||
if (ModelUtils.isStringSchema(p)) {
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
if (ModelUtils.isMapSchema(p)) {
|
||||
Schema ap = ModelUtils.getAdditionalProperties(p);
|
||||
String inner = getSchemaType(ap);
|
||||
return "Map.empty[String, " + inner + "]";
|
||||
}
|
||||
|
||||
if (ModelUtils.isArraySchema(p)) {
|
||||
Schema items = ((ArraySchema)p).getItems();
|
||||
String inner = getSchemaType(items);
|
||||
return "List.empty[" + inner + "]";
|
||||
}
|
||||
|
||||
return "null";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toEnumName(CodegenProperty property) {
|
||||
return camelize(property.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toEnumVarName(String value, String datatype) {
|
||||
if (value.length() == 0) {
|
||||
return "EMPTY";
|
||||
}
|
||||
|
||||
String var = camelize(value.replaceAll("\\W+", "_"));
|
||||
if (var.matches("\\d.*")) {
|
||||
return "_" + var;
|
||||
} else {
|
||||
return var;
|
||||
}
|
||||
}
|
||||
|
||||
private void addCliOptionWithDefault(String name, String description, boolean defaultValue) {
|
||||
cliOptions.add(CliOption.newBoolean(name, description).defaultValue(Boolean.toString(defaultValue)));
|
||||
}
|
||||
|
||||
private String getBasePackagePath() {
|
||||
return String.format(Locale.ROOT, "%s/%s", sourceFolder, basePackage.replace(".", File.separator));
|
||||
}
|
||||
|
||||
private String generateModelDefaultValue(CodegenModel cm, Map<String, CodegenModel> models) {
|
||||
StringBuilder defaultValue = new StringBuilder();
|
||||
defaultValue.append(cm.classname).append('(');
|
||||
|
||||
for (CodegenProperty var : cm.vars) {
|
||||
if (!var.required) {
|
||||
defaultValue.append("None");
|
||||
} else if (models.containsKey(var.dataType)) {
|
||||
defaultValue.append(generateModelDefaultValue(models.get(var.dataType), models));
|
||||
} else if (var.defaultValue != null) {
|
||||
defaultValue.append(var.defaultValue);
|
||||
} else if (var.isEnum) {
|
||||
defaultValue.append(cm.classname).append('.').append(var.enumName).append(".values.head");
|
||||
} else {
|
||||
LOGGER.warn("Unknown default value for var {0} in class {1}", var.name, cm.classname);
|
||||
defaultValue.append("null");
|
||||
}
|
||||
|
||||
if (var.hasMore) {
|
||||
defaultValue.append(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (cm.isMapModel) {
|
||||
defaultValue.append(", Map.empty");
|
||||
}
|
||||
|
||||
defaultValue.append(')');
|
||||
|
||||
return defaultValue.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user