org.openapitools.codegen.languages.AbstractPythonConnexionServerCodegen Maven / Gradle / Ivy
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openapitools.codegen.languages;
import java.io.File;
import java.io.IOException;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.meta.features.DocumentationFeature;
import org.openapitools.codegen.model.ApiInfoMap;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
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.PathItem.HttpMethod;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.security.SecurityScheme;
import static org.openapitools.codegen.utils.StringUtils.*;
public abstract class AbstractPythonConnexionServerCodegen extends AbstractPythonCodegen implements CodegenConfig {
private static class PythonBooleanSerializer extends JsonSerializer {
@Override
public void serialize(Boolean value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeRawValue(value ? "True" : "False");
}
}
private final Logger LOGGER = LoggerFactory.getLogger(AbstractPythonConnexionServerCodegen.class);
public static final String CONTROLLER_PACKAGE = "controllerPackage";
public static final String DEFAULT_CONTROLLER = "defaultController";
public static final String FEATURE_CORS = "featureCORS";
// nose is a python testing framework, we use pytest if USE_NOSE is unset
public static final String USE_NOSE = "useNose";
public static final String PYTHON_SRC_ROOT = "pythonSrcRoot";
public static final String USE_PYTHON_SRC_ROOT_IN_IMPORTS = "usePythonSrcRootInImports";
public static final String MOVE_TESTS_UNDER_PYTHON_SRC_ROOT = "testsUsePythonSrcRoot";
static final String MEDIA_TYPE = "mediaType";
// An object mapper that is used to convert an example string to
// a "python-compliant" example string (boolean as True/False).
final ObjectMapper MAPPER = new ObjectMapper();
protected int serverPort = 8080;
protected String controllerPackage;
protected String defaultController;
protected Map regexModifiers;
protected boolean fixBodyName;
protected boolean featureCORS = Boolean.FALSE;
protected boolean useNose = Boolean.FALSE;
protected String pythonSrcRoot;
protected boolean usePythonSrcRootInImports = Boolean.FALSE;
protected boolean moveTestsUnderPythonSrcRoot = Boolean.FALSE;
public AbstractPythonConnexionServerCodegen(String templateDirectory, boolean fixBodyNameValue) {
super();
modifyFeatureSet(features -> features.includeDocumentationFeatures(DocumentationFeature.Readme));
fixBodyName = fixBodyNameValue;
modelPackage = "models";
testPackage = "test";
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Boolean.class, new PythonBooleanSerializer());
MAPPER.registerModule(simpleModule);
// TODO may remove these later to default to the setting in abstract python base class instead
languageSpecificPrimitives.add("List");
languageSpecificPrimitives.add("Dict");
typeMapping.put("array", "List");
typeMapping.put("map", "Dict");
// set the output folder here
outputFolder = "generated-code" + File.separatorChar + "connexion";
apiTemplateFiles.put("controller.mustache", ".py");
modelTemplateFiles.put("model.mustache", ".py");
apiTestTemplateFiles().put("controller_test.mustache", ".py");
/*
* Template Location. This is the location which templates will be read from. The generator
* will use the resource stream to attempt to read the templates.
*/
embeddedTemplateDir = templateDir = templateDirectory;
/*
* Additional Properties. These values can be passed to the templates and
* are available in models, apis, and supporting files
*/
additionalProperties.put("serverPort", serverPort);
/*
* Supporting Files. You can write single files for the generator with the
* entire object tree available. If the input file has a suffix of `.mustache
* it will be processed by the template engine. Otherwise, it will be copied
*/
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
supportingFiles.add(new SupportingFile("test-requirements.mustache", "", "test-requirements.txt"));
supportingFiles.add(new SupportingFile("requirements.mustache", "", "requirements.txt"));
regexModifiers = new HashMap();
regexModifiers.put('i', "IGNORECASE");
regexModifiers.put('l', "LOCALE");
regexModifiers.put('m', "MULTILINE");
regexModifiers.put('s', "DOTALL");
regexModifiers.put('u', "UNICODE");
regexModifiers.put('x', "VERBOSE");
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "python package name (convention: snake_case).")
.defaultValue("openapi_server"));
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_VERSION, "python package version.")
.defaultValue("1.0.0"));
cliOptions.add(new CliOption(CONTROLLER_PACKAGE, "controller package").
defaultValue("controllers"));
cliOptions.add(new CliOption(DEFAULT_CONTROLLER, "default controller").
defaultValue("default_controller"));
cliOptions.add(new CliOption("serverPort", "TCP port to listen to in app.run").
defaultValue("8080"));
cliOptions.add(CliOption.newBoolean(FEATURE_CORS, "use flask-cors for handling CORS requests").
defaultValue(Boolean.FALSE.toString()));
cliOptions.add(CliOption.newBoolean(USE_NOSE, "use the nose test framework").
defaultValue(Boolean.FALSE.toString()));
cliOptions.add(new CliOption(PYTHON_SRC_ROOT, "put python sources in this subdirectory of output folder (defaults to \"\" for). Use this for src/ layout.").
defaultValue(""));
cliOptions.add(new CliOption(USE_PYTHON_SRC_ROOT_IN_IMPORTS, "include pythonSrcRoot in import namespaces.").
defaultValue("false"));
cliOptions.add(new CliOption(MOVE_TESTS_UNDER_PYTHON_SRC_ROOT, "generates test under the pythonSrcRoot folder.")
.defaultValue("false"));
}
protected void addSupportingFiles() {
}
@Override
public void processOpts() {
super.processOpts();
//apiTemplateFiles.clear();
if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME));
} else {
setPackageName("openapi_server");
}
if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) {
setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION));
} else {
setPackageVersion("1.0.0");
additionalProperties.put(CodegenConstants.PACKAGE_VERSION, this.packageVersion);
}
if (additionalProperties.containsKey(CONTROLLER_PACKAGE)) {
this.controllerPackage = additionalProperties.get(CONTROLLER_PACKAGE).toString();
} else {
this.controllerPackage = "controllers";
additionalProperties.put(CONTROLLER_PACKAGE, this.controllerPackage);
}
if (additionalProperties.containsKey(DEFAULT_CONTROLLER)) {
this.defaultController = additionalProperties.get(DEFAULT_CONTROLLER).toString();
} else {
this.defaultController = "default_controller";
additionalProperties.put(DEFAULT_CONTROLLER, this.defaultController);
}
if (additionalProperties.containsKey(FEATURE_CORS)) {
setFeatureCORS(String.valueOf(additionalProperties.get(FEATURE_CORS)));
}
if (additionalProperties.containsKey(USE_NOSE)) {
setUseNose(String.valueOf(additionalProperties.get(USE_NOSE)));
}
if (additionalProperties.containsKey(USE_PYTHON_SRC_ROOT_IN_IMPORTS)) {
setUsePythonSrcRootInImports(String.valueOf(additionalProperties.get(USE_PYTHON_SRC_ROOT_IN_IMPORTS)));
}
if (additionalProperties.containsKey(MOVE_TESTS_UNDER_PYTHON_SRC_ROOT)) {
setMoveTestsUnderPythonSrcRoot(String.valueOf(additionalProperties.get(MOVE_TESTS_UNDER_PYTHON_SRC_ROOT)));
}
if (additionalProperties.containsKey(PYTHON_SRC_ROOT)) {
String pythonSrcRoot = (String) additionalProperties.get(PYTHON_SRC_ROOT);
if (moveTestsUnderPythonSrcRoot) {
testPackage = pythonSrcRoot + "." + testPackage;
}
if (usePythonSrcRootInImports) {
// if we prepend the package name with the pythonSrcRoot we get the desired effect.
// But, we also need to set pythonSrcRoot itself to "" to ensure all the paths are
// what we expect.
setPackageName(pythonSrcRoot + "." + packageName);
pythonSrcRoot = "";
}
setPythonSrcRoot(pythonSrcRoot);
} else {
setPythonSrcRoot("");
}
supportingFiles.add(new SupportingFile("__main__.mustache", packagePath(), "__main__.py"));
supportingFiles.add(new SupportingFile("util.mustache", packagePath(), "util.py"));
supportingFiles.add(new SupportingFile("typing_utils.mustache", packagePath(), "typing_utils.py"));
supportingFiles.add(new SupportingFile("__init__.mustache", packagePath() + File.separatorChar + packageToPath(controllerPackage), "__init__.py"));
supportingFiles.add(new SupportingFile("security_controller.mustache", packagePath() + File.separatorChar + packageToPath(controllerPackage), "security_controller.py"));
supportingFiles.add(new SupportingFile("__init__model.mustache", packagePath() + File.separatorChar + packageToPath(modelPackage), "__init__.py"));
supportingFiles.add(new SupportingFile("base_model.mustache", packagePath() + File.separatorChar + packageToPath(modelPackage), "base_model.py"));
supportingFiles.add(new SupportingFile("openapi.mustache", packagePath() + File.separatorChar + "openapi", "openapi.yaml"));
addSupportingFiles();
modelPackage = packageName + "." + modelPackage;
controllerPackage = packageName + "." + controllerPackage;
}
public void setFeatureCORS(String val) {
this.featureCORS = Boolean.parseBoolean(val);
}
public void setUseNose(String val) {
this.useNose = Boolean.parseBoolean(val);
}
public void setPythonSrcRoot(String val) {
String pySrcRoot;
if (val == null) {
pySrcRoot = "";
} else {
pySrcRoot = val.replaceAll("[/\\\\]+$", "");
}
if (pySrcRoot.isEmpty() || ".".equals(pySrcRoot)) {
this.pythonSrcRoot = "";
} else {
this.pythonSrcRoot = pySrcRoot + File.separator;
}
additionalProperties.put(PYTHON_SRC_ROOT, StringUtils.defaultIfBlank(this.pythonSrcRoot, null));
}
public void setUsePythonSrcRootInImports(String val) {
this.usePythonSrcRootInImports = Boolean.parseBoolean(val);
}
public void setMoveTestsUnderPythonSrcRoot(String val) {
this.moveTestsUnderPythonSrcRoot = Boolean.parseBoolean(val);
}
public String pythonSrcOutputFolder() {
return outputFolder + File.separator + pythonSrcRoot;
}
private static String packageToPath(String pkg) {
return pkg.replace(".", File.separator);
}
@Override
public String apiPackage() {
return controllerPackage;
}
/**
* Configures the type of generator.
*
* @return the CodegenType for this generator
* @see org.openapitools.codegen.CodegenType
*/
@Override
public CodegenType getTag() {
return CodegenType.SERVER;
}
/**
* Returns human-friendly help for the generator. Provide the consumer with help
* tips, parameters here
*
* @return A string value for the help message
*/
@Override
public String getHelp() {
return "Generates a Python server library using the Connexion project. By default, " +
"it will also generate service classes -- which you can disable with the `-Dnoservice` environment variable.";
}
@Override
public String toApiName(String name) {
if (name == null || name.length() == 0) {
return "DefaultController";
}
return camelize(name) + "Controller";
}
@Override
public String toApiTestFilename(String name) {
return "test_" + toApiFilename(name);
}
/**
* Location to write api files. You can use the apiPackage() as defined when the class is
* instantiated
*/
@Override
public String apiFileFolder() {
String pkgPath = apiPackage().replace('.', File.separatorChar);
return pythonSrcOutputFolder() + pkgPath;
}
@Override
public String modelFileFolder() {
String pkgPath = modelPackage().replace('.', File.separatorChar);
return pythonSrcOutputFolder() + pkgPath;
}
@Override
public String getTypeDeclaration(Schema p) {
if (ModelUtils.isArraySchema(p)) {
Schema inner = ModelUtils.getSchemaItems(p);
return getSchemaType(p) + "[" + getTypeDeclaration(inner) + "]";
} else if (ModelUtils.isMapSchema(p)) {
Schema inner = ModelUtils.getAdditionalProperties(p);
return getSchemaType(p) + "[str, " + getTypeDeclaration(inner) + "]";
}
return super.getTypeDeclaration(p);
}
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
// need vendor extensions for x-openapi-router-controller
Map paths = openAPI.getPaths();
if (paths != null) {
List pathnames = new ArrayList(paths.keySet());
for (String pathname : pathnames) {
PathItem path = paths.get(pathname);
// Fix path parameters to be in snake_case
if (pathname.contains("{")) {
String fixedPath = "";
for (String token : pathname.substring(1).split("/")) {
if (token.startsWith("{")) {
String snake_case_token = "{" + this.toParamName(token.substring(1, token.length() - 1)) + "}";
if (!token.equals(snake_case_token)) {
token = snake_case_token;
}
}
fixedPath += "/" + token;
}
if (!fixedPath.equals(pathname)) {
LOGGER.warn(
"Path '{}' is not consistent with Python variable names. It will be replaced by '{}'",
pathname, fixedPath);
paths.remove(pathname);
path.addExtension("x-python-connexion-openapi-name", pathname);
paths.put(fixedPath, path);
}
}
Map operationMap = path.readOperationsMap();
if (operationMap != null) {
for (Map.Entry operationMapEntry : operationMap.entrySet()) {
HttpMethod method = operationMapEntry.getKey();
Operation operation = operationMapEntry.getValue();
String tag = "default";
if (operation.getTags() != null && operation.getTags().size() > 0) {
tag = operation.getTags().get(0);
}
String operationId = getOrGenerateOperationId(operation, pathname, method.toString());
operation.setOperationId(toOperationId(operationId));
if (operation.getExtensions() == null || operation.getExtensions().get("x-openapi-router-controller") == null) {
operation.addExtension(
"x-openapi-router-controller",
controllerPackage + "." + toApiFilename(tag)
);
}
if (operation.getParameters() != null) {
for (Parameter parameter : operation.getParameters()) {
if (StringUtils.isNotEmpty(parameter.get$ref())) {
parameter = ModelUtils.getReferencedParameter(openAPI, parameter);
}
String swaggerParameterName = parameter.getName();
String pythonParameterName = this.toParamName(swaggerParameterName);
if (swaggerParameterName == null) {
throw new RuntimeException("Please report the issue as the parameter name cannot be null: " + parameter);
}
if (!swaggerParameterName.equals(pythonParameterName)) {
LOGGER.warn(
"Parameter name '{}' is not consistent with Python variable names. It will be replaced by '{}'",
swaggerParameterName, pythonParameterName);
parameter.addExtension("x-python-connexion-openapi-name", swaggerParameterName);
parameter.setName(pythonParameterName);
}
if (swaggerParameterName.isEmpty()) {
LOGGER.error("Missing parameter name in {}.{}", pathname, parameter.getIn());
}
}
}
RequestBody body = operation.getRequestBody();
if (fixBodyName && body != null) {
if (body.getExtensions() == null || !body.getExtensions().containsKey("x-body-name")) {
String bodyParameterName = "body";
if (operation.getExtensions() != null && operation.getExtensions().containsKey("x-codegen-request-body-name")) {
bodyParameterName = (String) operation.getExtensions().get("x-codegen-request-body-name");
} else {
// Used by code generator
operation.addExtension("x-codegen-request-body-name", bodyParameterName);
}
// Used by connexion
body.addExtension("x-body-name", bodyParameterName);
}
}
}
}
}
// Sort path names after variable name fix
List fixedPathnames = new ArrayList(paths.keySet());
Collections.sort(fixedPathnames);
for (String pathname : fixedPathnames) {
PathItem pathItem = paths.remove(pathname);
paths.put(pathname, pathItem);
}
}
addSecurityExtensions(openAPI);
}
private void addSecurityExtension(SecurityScheme securityScheme, String extensionName, String functionName) {
if (securityScheme.getExtensions() == null || !securityScheme.getExtensions().containsKey(extensionName)) {
securityScheme.addExtension(extensionName, functionName);
}
}
private void addSecurityExtensions(OpenAPI openAPI) {
Components components = openAPI.getComponents();
if (components != null && components.getSecuritySchemes() != null) {
Map securitySchemes = components.getSecuritySchemes();
for (Map.Entry securitySchemesEntry : securitySchemes.entrySet()) {
String securityName = securitySchemesEntry.getKey();
SecurityScheme securityScheme = securitySchemesEntry.getValue();
String baseFunctionName = controllerPackage + ".security_controller.";
switch (securityScheme.getType()) {
case APIKEY:
addSecurityExtension(securityScheme, "x-apikeyInfoFunc", baseFunctionName + "info_from_" + securityName);
break;
case HTTP:
if ("basic".equals(securityScheme.getScheme())) {
addSecurityExtension(securityScheme, "x-basicInfoFunc", baseFunctionName + "info_from_" + securityName);
} else if ("bearer".equals(securityScheme.getScheme())) {
addSecurityExtension(securityScheme, "x-bearerInfoFunc", baseFunctionName + "info_from_" + securityName);
}
break;
case OPENIDCONNECT:
LOGGER.warn("Security type {} is not supported by connexion yet", securityScheme.getType().toString());
case OAUTH2:
addSecurityExtension(securityScheme, "x-tokenInfoFunc", baseFunctionName + "info_from_" + securityName);
addSecurityExtension(securityScheme, "x-scopeValidateFunc", baseFunctionName + "validate_scope_" + securityName);
break;
default:
LOGGER.warn("Unknown security type {}", securityScheme.getType().toString());
}
}
}
}
private static List getOperations(Map objs) {
List result = new ArrayList<>();
ApiInfoMap apiInfo = (ApiInfoMap) objs.get("apiInfo");
for (OperationsMap api : apiInfo.getApis()) {
result.add(api.getOperations());
}
return result;
}
private static List
© 2015 - 2024 Weber Informatics LLC | Privacy Policy