From 1c22ec363ba84ab8015ee68d7f1d0856c562dae1 Mon Sep 17 00:00:00 2001 From: wing328 Date: Mon, 26 Mar 2018 23:56:56 +0800 Subject: [PATCH] add python generator and tests --- .../languages/PythonClientCodegen.java | 674 ++++++++++++++++++ .../python/PythonClientOptionsTest.java | 37 + .../codegen/python/PythonTest.java | 275 +++++++ .../options/PythonClientOptionsProvider.java | 38 + 4 files changed, 1024 insertions(+) create mode 100755 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientOptionsTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/options/PythonClientOptionsProvider.java diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java new file mode 100755 index 00000000000..b37aae3c608 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java @@ -0,0 +1,674 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.CliOption; +import org.openapitools.codegen.CodegenConfig; +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.CodegenType; +import org.openapitools.codegen.DefaultCodegen; +import org.openapitools.codegen.SupportingFile; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.*; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +public class PythonClientCodegen extends DefaultCodegen implements CodegenConfig { + public static final String PACKAGE_URL = "packageUrl"; + public static final String DEFAULT_LIBRARY = "urllib3"; + + protected String packageName; // e.g. petstore_api + protected String packageVersion; + protected String projectName; // for setup.py, e.g. petstore-api + protected String packageUrl; + protected String apiDocPath = "docs/"; + protected String modelDocPath = "docs/"; + + protected Map regexModifiers; + + private String testFolder; + + public PythonClientCodegen() { + super(); + + // clear import mapping (from default generator) as python does not use it + // at the moment + importMapping.clear(); + + supportsInheritance = true; + modelPackage = "models"; + apiPackage = "api"; + outputFolder = "generated-code" + File.separatorChar + "python"; + + modelTemplateFiles.put("model.mustache", ".py"); + apiTemplateFiles.put("api.mustache", ".py"); + + modelTestTemplateFiles.put("model_test.mustache", ".py"); + apiTestTemplateFiles.put("api_test.mustache", ".py"); + + embeddedTemplateDir = templateDir = "python"; + + modelDocTemplateFiles.put("model_doc.mustache", ".md"); + apiDocTemplateFiles.put("api_doc.mustache", ".md"); + + testFolder = "test"; + + languageSpecificPrimitives.clear(); + languageSpecificPrimitives.add("int"); + languageSpecificPrimitives.add("float"); + languageSpecificPrimitives.add("list"); + languageSpecificPrimitives.add("dict"); + languageSpecificPrimitives.add("bool"); + languageSpecificPrimitives.add("str"); + languageSpecificPrimitives.add("datetime"); + languageSpecificPrimitives.add("date"); + languageSpecificPrimitives.add("object"); + + typeMapping.clear(); + typeMapping.put("integer", "int"); + typeMapping.put("float", "float"); + typeMapping.put("number", "float"); + typeMapping.put("long", "int"); + typeMapping.put("double", "float"); + typeMapping.put("array", "list"); + typeMapping.put("map", "dict"); + typeMapping.put("boolean", "bool"); + typeMapping.put("string", "str"); + typeMapping.put("date", "date"); + typeMapping.put("DateTime", "datetime"); + typeMapping.put("object", "object"); + typeMapping.put("file", "file"); + // TODO binary should be mapped to byte array + // mapped to String as a workaround + typeMapping.put("binary", "str"); + typeMapping.put("ByteArray", "str"); + // map uuid to string for the time being + typeMapping.put("UUID", "str"); + + // from https://docs.python.org/3/reference/lexical_analysis.html#keywords + setReservedWordsLowerCase( + Arrays.asList( + // local variable name used in API methods (endpoints) + "all_params", "resource_path", "path_params", "query_params", + "header_params", "form_params", "local_var_files", "body_params", "auth_settings", + // @property + "property", + // python reserved words + "and", "del", "from", "not", "while", "as", "elif", "global", "or", "with", + "assert", "else", "if", "pass", "yield", "break", "except", "import", + "print", "class", "exec", "in", "raise", "continue", "finally", "is", + "return", "def", "for", "lambda", "try", "self", "nonlocal", "None", "True", "False")); + + 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.clear(); + cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "python package name (convention: snake_case).") + .defaultValue("swagger_client")); + cliOptions.add(new CliOption(CodegenConstants.PROJECT_NAME, "python project name in setup.py (e.g. petstore-api).")); + cliOptions.add(new CliOption(CodegenConstants.PACKAGE_VERSION, "python package version.") + .defaultValue("1.0.0")); + cliOptions.add(new CliOption(PACKAGE_URL, "python package URL.")); + cliOptions.add(CliOption.newBoolean(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, + CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG_DESC).defaultValue(Boolean.TRUE.toString())); + cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, "hides the timestamp when files were generated") + .defaultValue(Boolean.TRUE.toString())); + + supportedLibraries.put("urllib3", "urllib3-based client"); + supportedLibraries.put("asyncio", "Asyncio-based client (python 3.5+)"); + supportedLibraries.put("tornado", "tornado-based client"); + CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use"); + libraryOption.setDefault(DEFAULT_LIBRARY); + cliOptions.add(libraryOption); + setLibrary(DEFAULT_LIBRARY); + } + + @Override + public void processOpts() { + super.processOpts(); + Boolean excludeTests = false; + + if(additionalProperties.containsKey(CodegenConstants.EXCLUDE_TESTS)) { + excludeTests = Boolean.valueOf(additionalProperties.get(CodegenConstants.EXCLUDE_TESTS).toString()); + } + + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) { + setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME)); + } + else { + setPackageName("swagger_client"); + } + + if (additionalProperties.containsKey(CodegenConstants.PROJECT_NAME)) { + setProjectName((String) additionalProperties.get(CodegenConstants.PROJECT_NAME)); + } + else { + // default: set project based on package name + // e.g. petstore_api (package name) => petstore-api (project name) + setProjectName(packageName.replaceAll("_", "-")); + } + + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) { + setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION)); + } + else { + setPackageVersion("1.0.0"); + } + + // default HIDE_GENERATION_TIMESTAMP to true + if (!additionalProperties.containsKey(CodegenConstants.HIDE_GENERATION_TIMESTAMP)) { + additionalProperties.put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, Boolean.TRUE.toString()); + } else { + additionalProperties.put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, + Boolean.valueOf(additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP).toString())); + } + + additionalProperties.put(CodegenConstants.PROJECT_NAME, projectName); + additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName); + additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion); + + // make api and model doc path available in mustache template + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + + if (additionalProperties.containsKey(PACKAGE_URL)) { + setPackageUrl((String) additionalProperties.get(PACKAGE_URL)); + } + + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + + supportingFiles.add(new SupportingFile("tox.mustache", "", "tox.ini")); + supportingFiles.add(new SupportingFile("test-requirements.mustache", "", "test-requirements.txt")); + supportingFiles.add(new SupportingFile("requirements.mustache", "", "requirements.txt")); + + supportingFiles.add(new SupportingFile("configuration.mustache", packageName, "configuration.py")); + supportingFiles.add(new SupportingFile("__init__package.mustache", packageName, "__init__.py")); + supportingFiles.add(new SupportingFile("__init__model.mustache", packageName + File.separatorChar + modelPackage, "__init__.py")); + supportingFiles.add(new SupportingFile("__init__api.mustache", packageName + File.separatorChar + apiPackage, "__init__.py")); + + if(Boolean.FALSE.equals(excludeTests)) { + supportingFiles.add(new SupportingFile("__init__test.mustache", testFolder, "__init__.py")); + } + supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh")); + supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); + supportingFiles.add(new SupportingFile("travis.mustache", "", ".travis.yml")); + supportingFiles.add(new SupportingFile("setup.mustache", "", "setup.py")); + supportingFiles.add(new SupportingFile("api_client.mustache", packageName, "api_client.py")); + + if ("asyncio".equals(getLibrary())) { + supportingFiles.add(new SupportingFile("asyncio/rest.mustache", packageName, "rest.py")); + additionalProperties.put("asyncio", "true"); + } else if ("tornado".equals(getLibrary())) { + supportingFiles.add(new SupportingFile("tornado/rest.mustache", packageName, "rest.py")); + additionalProperties.put("tornado", "true"); + } else { + supportingFiles.add(new SupportingFile("rest.mustache", packageName, "rest.py")); + } + + modelPackage = packageName + "." + modelPackage; + apiPackage = packageName + "." + apiPackage; + + } + + private static String dropDots(String str) { + return str.replaceAll("\\.", "_"); + } + + @Override + public String toModelImport(String name) { + String modelImport; + if (StringUtils.startsWithAny(name,"import", "from")) { + modelImport = name; + } else { + modelImport = "from "; + if (!"".equals(modelPackage())) { + modelImport += modelPackage() + "."; + } + modelImport += toModelFilename(name)+ " import " + name; + } + return modelImport; + } + + @Override + public Map postProcessModels(Map objs) { + // process enum in models + return postProcessModelsEnum(objs); + } + + @Override + public void postProcessParameter(CodegenParameter parameter){ + postProcessPattern(parameter.pattern, parameter.vendorExtensions); + } + + @Override + public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { + postProcessPattern(property.pattern, property.vendorExtensions); + } + + /* + * The swagger pattern spec follows the Perl convention and style of modifiers. Python + * does not support this in as natural a way so it needs to convert it. See + * https://docs.python.org/2/howto/regex.html#compilation-flags for details. + */ + public void postProcessPattern(String pattern, Map vendorExtensions){ + if(pattern != null) { + int i = pattern.lastIndexOf('/'); + + //Must follow Perl /pattern/modifiers convention + if(pattern.charAt(0) != '/' || i < 2) { + throw new IllegalArgumentException("Pattern must follow the Perl " + + "/pattern/modifiers convention. "+pattern+" is not valid."); + } + + String regex = pattern.substring(1, i).replace("'", "\\'"); + List modifiers = new ArrayList(); + + for(char c : pattern.substring(i).toCharArray()) { + if(regexModifiers.containsKey(c)) { + String modifier = regexModifiers.get(c); + modifiers.add(modifier); + } + } + + vendorExtensions.put("x-regex", regex); + vendorExtensions.put("x-modifiers", modifiers); + } + } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String getName() { + return "python"; + } + + @Override + public String getHelp() { + return "Generates a Python client library."; + } + + @Override + public String escapeReservedWord(String name) { + if(this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "_" + name; + } + + @Override + public String apiDocFileFolder() { + return (outputFolder + "/" + apiDocPath); + } + + @Override + public String modelDocFileFolder() { + return (outputFolder + "/" + modelDocPath); + } + + @Override + public String toModelDocFilename(String name) { + return toModelName(name); + } + + @Override + public String toApiDocFilename(String name) { + return toApiName(name); + } + + + @Override + public String apiFileFolder() { + return outputFolder + File.separatorChar + apiPackage().replace('.', File.separatorChar); + } + + @Override + public String modelFileFolder() { + return outputFolder + File.separatorChar + modelPackage().replace('.', File.separatorChar); + } + + @Override + public String apiTestFileFolder() { + return outputFolder + File.separatorChar + testFolder; + } + + @Override + public String modelTestFileFolder() { + return outputFolder + File.separatorChar + testFolder; + } + + @Override + public String getTypeDeclaration(Schema p) { + if (p instanceof ArraySchema) { + ArraySchema ap = (ArraySchema) p; + Schema inner = ap.getItems(); + return getSchemaType(p) + "[" + getTypeDeclaration(inner) + "]"; + } else if (p instanceof MapSchema) { + MapSchema mp = (MapSchema) p; + Schema inner = (Schema) mp.getAdditionalProperties(); + + return getSchemaType(p) + "(str, " + getTypeDeclaration(inner) + ")"; + } + return super.getTypeDeclaration(p); + } + + @Override + public String getSchemaType(Schema p) { + String swaggerType = super.getSchemaType(p); + String type = null; + if (typeMapping.containsKey(swaggerType)) { + type = typeMapping.get(swaggerType); + if (languageSpecificPrimitives.contains(type)) { + return type; + } + } else { + type = toModelName(swaggerType); + } + return type; + } + + @Override + public String toVarName(String name) { + // sanitize name + name = sanitizeName(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'. + + // remove dollar sign + name = name.replaceAll("$", ""); + + // if it's all uppper case, convert to lower case + if (name.matches("^[A-Z_]*$")) { + name = name.toLowerCase(); + } + + // underscore the variable name + // petId => pet_id + name = underscore(name); + + // remove leading underscore + name = name.replaceAll("^_*", ""); + + // for reserved word or word starting with number, append _ + if (isReservedWord(name) || name.matches("^\\d.*")) { + name = escapeReservedWord(name); + } + + return name; + } + + @Override + public String toParamName(String name) { + // to avoid conflicts with 'callback' parameter for async call + if ("callback".equals(name)) { + return "param_callback"; + } + + // should be the same as variable name + return toVarName(name); + } + + @Override + public String toModelName(String name) { + name = sanitizeName(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'. + // remove dollar sign + name = name.replaceAll("$", ""); + + // model name cannot use reserved keyword, e.g. return + if (isReservedWord(name)) { + LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + camelize("model_" + name)); + name = "model_" + name; // e.g. return => ModelReturn (after camelize) + } + + // model name starts with number + if (name.matches("^\\d.*")) { + LOGGER.warn(name + " (model name starts with number) cannot be used as model name. Renamed to " + camelize("model_" + name)); + name = "model_" + name; // e.g. 200Response => Model200Response (after camelize) + } + + if (!StringUtils.isEmpty(modelNamePrefix)) { + name = modelNamePrefix + "_" + name; + } + + if (!StringUtils.isEmpty(modelNameSuffix)) { + name = name + "_" + modelNameSuffix; + } + + // camelize the model name + // phone_number => PhoneNumber + return camelize(name); + } + + @Override + public String toModelFilename(String name) { + // underscore the model file name + // PhoneNumber => phone_number + return underscore(dropDots(toModelName(name))); + } + + @Override + public String toModelTestFilename(String name) { + return "test_" + toModelFilename(name); + } + + @Override + public String toApiFilename(String name) { + // replace - with _ e.g. created-at => created_at + name = name.replaceAll("-", "_"); + + // e.g. PhoneNumberApi.py => phone_number_api.py + return underscore(name) + "_api"; + } + + @Override + public String toApiTestFilename(String name) { + return "test_" + toApiFilename(name); + } + + @Override + public String toApiName(String name) { + if (name.length() == 0) { + return "DefaultApi"; + } + // e.g. phone_number_api => PhoneNumberApi + return camelize(name) + "Api"; + } + + @Override + public String toApiVarName(String name) { + if (name.length() == 0) { + return "default_api"; + } + return underscore(name) + "_api"; + } + + @Override + public String toOperationId(String operationId) { + // throw exception if method name is empty (should not occur as an auto-generated method name will be used) + if (StringUtils.isEmpty(operationId)) { + throw new RuntimeException("Empty method name (operationId) not allowed"); + } + + // method name cannot use reserved keyword, e.g. return + if (isReservedWord(operationId)) { + LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + underscore(sanitizeName("call_" + operationId))); + operationId = "call_" + operationId; + } + + return underscore(sanitizeName(operationId)); + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public void setProjectName(String projectName) { + this.projectName= projectName; + } + + public void setPackageVersion(String packageVersion) { + this.packageVersion = packageVersion; + } + + public void setPackageUrl(String packageUrl) { + this.packageUrl = packageUrl; + } + + /** + * Generate Python package name from String `packageName` + * + * (PEP 0008) Python packages should also have short, all-lowercase names, + * although the use of underscores is discouraged. + * + * @param packageName Package name + * @return Python package name that conforms to PEP 0008 + */ + @SuppressWarnings("static-method") + public String generatePackageName(String packageName) { + return underscore(packageName.replaceAll("[^\\w]+", "")); + } + + /** + * Return the default value of the property + * + * @param p Swagger property object + * @return string presentation of the default value of the property + */ + @Override + public String toDefaultValue(Schema p) { + if (p instanceof StringSchema) { + StringSchema dp = (StringSchema) p; + if (dp.getDefault() != null) { + if (Pattern.compile("\r\n|\r|\n").matcher(dp.getDefault()).find()) + return "'''" + dp.getDefault() + "'''"; + else + return "'" + dp.getDefault() + "'"; + } + } else if (p instanceof BooleanSchema) { + BooleanSchema dp = (BooleanSchema) p; + if (dp.getDefault() != null) { + if (dp.getDefault().toString().equalsIgnoreCase("false")) + return "False"; + else + return "True"; + } + } else if (p instanceof DateSchema) { + // TODO + } else if (p instanceof DateTimeSchema) { + // TODO + } else if (p instanceof NumberSchema) { + NumberSchema dp = (NumberSchema) p; + if (dp.getDefault() != null) { + return dp.getDefault().toString(); + } + } else if (p instanceof IntegerSchema) { + IntegerSchema dp = (IntegerSchema) p; + if (dp.getDefault() != null) { + return dp.getDefault().toString(); + } + } + + return null; + } + + @Override + public void setParameterExampleValue(CodegenParameter p) { + String example; + + if (p.defaultValue == null) { + example = p.example; + } else { + example = p.defaultValue; + } + + String type = p.baseType; + if (type == null) { + type = p.dataType; + } + + if ("String".equalsIgnoreCase(type) || "str".equalsIgnoreCase(type)) { + if (example == null) { + example = p.paramName + "_example"; + } + example = "'" + escapeText(example) + "'"; + } else if ("Integer".equals(type) || "int".equals(type)) { + if (example == null) { + example = "56"; + } + } else if ("Float".equalsIgnoreCase(type) || "Double".equalsIgnoreCase(type)) { + if (example == null) { + example = "3.4"; + } + } else if ("BOOLEAN".equalsIgnoreCase(type) || "bool".equalsIgnoreCase(type)) { + if (example == null) { + example = "True"; + } + } else if ("file".equalsIgnoreCase(type)) { + if (example == null) { + example = "/path/to/file"; + } + example = "'" + escapeText(example) + "'"; + } else if ("Date".equalsIgnoreCase(type)) { + if (example == null) { + example = "2013-10-20"; + } + example = "'" + escapeText(example) + "'"; + } else if ("DateTime".equalsIgnoreCase(type)) { + if (example == null) { + example = "2013-10-20T19:20:30+01:00"; + } + example = "'" + escapeText(example) + "'"; + } else if (!languageSpecificPrimitives.contains(type)) { + // type is a model class, e.g. User + example = this.packageName + "." + type + "()"; + } else { + LOGGER.warn("Type " + type + " not handled properly in setParameterExampleValue"); + } + + if (example == null) { + example = "NULL"; + } else if (Boolean.TRUE.equals(p.isListContainer)) { + example = "[" + example + "]"; + } else if (Boolean.TRUE.equals(p.isMapContainer)) { + example = "{'key': " + example + "}"; + } + + p.example = example; + } + + @Override + public String sanitizeTag(String tag) { + return sanitizeName(tag); + } + + @Override + public String escapeQuotationMark(String input) { + // remove ' to avoid code injection + return input.replace("'", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + // remove multiline comment + return input.replace("'''", "'_'_'"); + } + +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientOptionsTest.java new file mode 100644 index 00000000000..f686ad68c10 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientOptionsTest.java @@ -0,0 +1,37 @@ +package org.openapitools.codegen.python; + +import org.openapitools.codegen.AbstractOptionsTest; +import org.openapitools.codegen.CodegenConfig; +import org.openapitools.codegen.languages.PythonClientCodegen; +import org.openapitools.codegen.options.PythonClientOptionsProvider; + +import mockit.Expectations; +import mockit.Tested; + +public class PythonClientOptionsTest extends AbstractOptionsTest { + + @Tested + private PythonClientCodegen clientCodegen; + + public PythonClientOptionsTest() { + super(new PythonClientOptionsProvider()); + } + + @Override + protected CodegenConfig getCodegenConfig() { + return clientCodegen; + } + + @SuppressWarnings("unused") + @Override + protected void setExpectations() { + new Expectations(clientCodegen) {{ + clientCodegen.setPackageName(PythonClientOptionsProvider.PACKAGE_NAME_VALUE); + clientCodegen.setProjectName(PythonClientOptionsProvider.PROJECT_NAME_VALUE); + clientCodegen.setPackageVersion(PythonClientOptionsProvider.PACKAGE_VERSION_VALUE); + clientCodegen.setPackageUrl(PythonClientOptionsProvider.PACKAGE_URL_VALUE); + // clientCodegen.setLibrary(PythonClientCodegen.DEFAULT_LIBRARY); + times = 1; + }}; + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonTest.java new file mode 100644 index 00000000000..d504e85e389 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonTest.java @@ -0,0 +1,275 @@ +package org.openapitools.codegen.python; + +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.DefaultCodegen; +import org.openapitools.codegen.languages.PythonClientCodegen; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.util.SchemaTypeUtil; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.testng.annotations.ITestAnnotation; + +import com.google.common.collect.Sets; +import java.util.Map; + +@SuppressWarnings("static-method") +public class PythonTest { + + @Test(description = "convert a python model with dots") + public void modelTest() { + final OpenAPI openAPI= new OpenAPIV3Parser().read("src/test/resources/2_0/v1beta3.json"); + final DefaultCodegen codegen = new PythonClientCodegen(); + + final CodegenModel simpleName = codegen.fromModel("v1beta3.Binding", openAPI.getComponents().getSchemas().get("v1beta3.Binding")); + Assert.assertEquals(simpleName.name, "v1beta3.Binding"); + Assert.assertEquals(simpleName.classname, "V1beta3Binding"); + Assert.assertEquals(simpleName.classVarName, "v1beta3_binding"); + + final CodegenModel compoundName = codegen.fromModel("v1beta3.ComponentStatus", openAPI.getComponents().getSchemas().get("v1beta3.ComponentStatus")); + Assert.assertEquals(compoundName.name, "v1beta3.ComponentStatus"); + Assert.assertEquals(compoundName.classname, "V1beta3ComponentStatus"); + Assert.assertEquals(compoundName.classVarName, "v1beta3_component_status"); + + final String path = "/api/v1beta3/namespaces/{namespaces}/bindings"; + final Operation operation = openAPI.getPaths().get(path).getPost(); + final CodegenOperation codegenOperation = codegen.fromOperation(path, "get", operation, openAPI.getComponents().getSchemas()); + Assert.assertEquals(codegenOperation.returnType, "V1beta3Binding"); + Assert.assertEquals(codegenOperation.returnBaseType, "V1beta3Binding"); + } + + @Test(description = "convert a simple java model") + public void simpleModelTest() { + final Schema schema = new Schema() + .description("a sample model") + .addProperties("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT)) + .addProperties("name", new StringSchema()) + .addProperties("createdAt", new DateTimeSchema()) + .addRequiredItem("id") + .addRequiredItem("name"); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", schema); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 3); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "id"); + Assert.assertEquals(property1.datatype, "int"); + Assert.assertEquals(property1.name, "id"); + Assert.assertNull(property1.defaultValue); + Assert.assertEquals(property1.baseType, "int"); + Assert.assertTrue(property1.hasMore); + Assert.assertTrue(property1.required); + Assert.assertTrue(property1.isPrimitiveType); + Assert.assertTrue(property1.isNotContainer); + + final CodegenProperty property2 = cm.vars.get(1); + Assert.assertEquals(property2.baseName, "name"); + Assert.assertEquals(property2.datatype, "str"); + Assert.assertEquals(property2.name, "name"); + Assert.assertNull(property2.defaultValue); + Assert.assertEquals(property2.baseType, "str"); + Assert.assertTrue(property2.hasMore); + Assert.assertTrue(property2.required); + Assert.assertTrue(property2.isPrimitiveType); + Assert.assertTrue(property2.isNotContainer); + + final CodegenProperty property3 = cm.vars.get(2); + Assert.assertEquals(property3.baseName, "createdAt"); + Assert.assertEquals(property3.datatype, "datetime"); + Assert.assertEquals(property3.name, "created_at"); + Assert.assertNull(property3.defaultValue); + Assert.assertEquals(property3.baseType, "datetime"); + Assert.assertFalse(property3.hasMore); + Assert.assertFalse(property3.required); + Assert.assertTrue(property3.isNotContainer); + } + + @Test(description = "convert a model with list property") + public void listPropertyTest() { + final Schema model = new Schema() + .description("a sample model") + .addProperties("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT)) + .addProperties("urls", new ArraySchema() + .items(new StringSchema())) + .addRequiredItem("id"); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 2); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "id"); + Assert.assertEquals(property1.datatype, "int"); + Assert.assertEquals(property1.name, "id"); + Assert.assertNull(property1.defaultValue); + Assert.assertEquals(property1.baseType, "int"); + Assert.assertTrue(property1.hasMore); + Assert.assertTrue(property1.required); + Assert.assertTrue(property1.isPrimitiveType); + Assert.assertTrue(property1.isNotContainer); + + final CodegenProperty property2 = cm.vars.get(1); + Assert.assertEquals(property2.baseName, "urls"); + Assert.assertEquals(property2.datatype, "list[str]"); + Assert.assertEquals(property2.name, "urls"); + Assert.assertNull(property2.defaultValue); + Assert.assertEquals(property2.baseType, "list"); + Assert.assertFalse(property2.hasMore); + Assert.assertEquals(property2.containerType, "array"); + Assert.assertFalse(property2.required); + Assert.assertTrue(property2.isPrimitiveType); + Assert.assertTrue(property2.isContainer); + } + + @Test(description = "convert a model with a map property") + public void mapPropertyTest() { + final Schema model = new Schema() + .description("a sample model") + .addProperties("translations", new MapSchema() + .additionalProperties(new StringSchema())) + .addRequiredItem("id"); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 1); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "translations"); + Assert.assertEquals(property1.datatype, "dict(str, str)"); + Assert.assertEquals(property1.name, "translations"); + Assert.assertEquals(property1.baseType, "dict"); + Assert.assertEquals(property1.containerType, "map"); + Assert.assertFalse(property1.required); + Assert.assertTrue(property1.isContainer); + Assert.assertTrue(property1.isPrimitiveType); + } + + @Test(description = "convert a model with complex property") + public void complexPropertyTest() { + final Schema model = new Schema() + .description("a sample model") + .addProperties("children", new Schema().$ref("#/definitions/Children")); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 1); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "children"); + Assert.assertEquals(property1.datatype, "Children"); + Assert.assertEquals(property1.name, "children"); + Assert.assertEquals(property1.baseType, "Children"); + Assert.assertFalse(property1.required); + Assert.assertTrue(property1.isNotContainer); + } + + @Test(description = "convert a model with complex list property") + public void complexListPropertyTest() { + final Schema model = new Schema() + .description("a sample model") + .addProperties("children", new ArraySchema() + .items(new Schema().$ref("#/definitions/Children"))); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 1); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "children"); + Assert.assertEquals(property1.complexType, "Children"); + Assert.assertEquals(property1.datatype, "list[Children]"); + Assert.assertEquals(property1.name, "children"); + Assert.assertEquals(property1.baseType, "list"); + Assert.assertEquals(property1.containerType, "array"); + Assert.assertFalse(property1.required); + Assert.assertTrue(property1.isContainer); + } + + @Test(description = "convert a model with complex map property") + public void complexMapPropertyTest() { + final Schema model = new Schema() + .description("a sample model") + .addProperties("children", new MapSchema() + .additionalProperties(new Schema().$ref("#/definitions/Children"))); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a sample model"); + Assert.assertEquals(cm.vars.size(), 1); + Assert.assertEquals(Sets.intersection(cm.imports, Sets.newHashSet("Children")).size(), 1); + + final CodegenProperty property1 = cm.vars.get(0); + Assert.assertEquals(property1.baseName, "children"); + Assert.assertEquals(property1.complexType, "Children"); + Assert.assertEquals(property1.datatype, "dict(str, Children)"); + Assert.assertEquals(property1.name, "children"); + Assert.assertEquals(property1.baseType, "dict"); + Assert.assertEquals(property1.containerType, "map"); + Assert.assertFalse(property1.required); + Assert.assertTrue(property1.isContainer); + Assert.assertFalse(property1.isNotContainer); + } + + + // should not start with 'null'. need help from the community to investigate further + @Test(enabled = false, description = "convert an array model") + public void arrayModelTest() { + final Schema model = new ArraySchema() + //.description() + .items(new Schema().$ref("#/definitions/Children")) + .description("an array model"); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "an array model"); + Assert.assertEquals(cm.vars.size(), 0); + Assert.assertEquals(cm.parent, "null"); + Assert.assertEquals(cm.imports.size(), 1); + Assert.assertEquals(Sets.intersection(cm.imports, Sets.newHashSet("Children")).size(), 1); + } + + // should not start with 'null'. need help from the community to investigate further + @Test(enabled = false, description = "convert an map model") + public void mapModelTest() { + final Schema model = new Schema() + .description("a map model") + .additionalProperties(new Schema().$ref("#/definitions/Children")); + final DefaultCodegen codegen = new PythonClientCodegen(); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "a map model"); + Assert.assertEquals(cm.vars.size(), 0); + Assert.assertEquals(cm.parent, "null"); + Assert.assertEquals(cm.imports.size(), 2); // TODO: need to verify + Assert.assertEquals(Sets.intersection(cm.imports, Sets.newHashSet("Children")).size(), 1); + } + +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/options/PythonClientOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/options/PythonClientOptionsProvider.java new file mode 100644 index 00000000000..0e55549e376 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/options/PythonClientOptionsProvider.java @@ -0,0 +1,38 @@ +package org.openapitools.codegen.options; + +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.languages.PythonClientCodegen; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public class PythonClientOptionsProvider implements OptionsProvider { + public static final String PACKAGE_NAME_VALUE = "swagger_client_python"; + public static final String PROJECT_NAME_VALUE = "swagger-client-python"; + public static final String PACKAGE_VERSION_VALUE = "1.0.0-SNAPSHOT"; + public static final String PACKAGE_URL_VALUE = ""; + + @Override + public String getLanguage() { + return "python"; + } + + @Override + public Map createOptions() { + ImmutableMap.Builder builder = new ImmutableMap.Builder(); + return builder.put(PythonClientCodegen.PACKAGE_URL, PACKAGE_URL_VALUE) + .put(CodegenConstants.PACKAGE_NAME, PACKAGE_NAME_VALUE) + .put(CodegenConstants.PROJECT_NAME, PROJECT_NAME_VALUE) + .put(CodegenConstants.PACKAGE_VERSION, PACKAGE_VERSION_VALUE) + .put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, "true") + .put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, "true") + .put(CodegenConstants.LIBRARY, "urllib3") + .build(); + } + + @Override + public boolean isServer() { + return false; + } +}