diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PerlClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PerlClientCodegen.java new file mode 100644 index 00000000000..2150e110578 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PerlClientCodegen.java @@ -0,0 +1,415 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.CodegenConfig; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenType; +import org.openapitools.codegen.DefaultCodegen; +import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.CliOption; + +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.Arrays; +import java.util.HashSet; +import java.util.regex.Matcher; + +import org.apache.commons.lang3.StringUtils; + +public class PerlClientCodegen extends DefaultCodegen implements CodegenConfig { + public static final String MODULE_NAME = "moduleName"; + public static final String MODULE_VERSION = "moduleVersion"; + protected String moduleName = "WWW::SwaggerClient"; + protected String modulePathPart = moduleName.replaceAll("::", Matcher.quoteReplacement(File.separator)); + protected String moduleVersion = "1.0.0"; + protected String apiDocPath = "docs/"; + protected String modelDocPath = "docs/"; + + protected static int emptyFunctionNameCounter = 0; + + public PerlClientCodegen() { + super(); + + // clear import mapping (from default generator) as perl does not use it + // at the moment + importMapping.clear(); + + modelPackage = File.separatorChar + "Object"; + outputFolder = "generated-code" + File.separatorChar + "perl"; + modelTemplateFiles.put("object.mustache", ".pm"); + apiTemplateFiles.put("api.mustache", ".pm"); + modelTestTemplateFiles.put("object_test.mustache", ".t"); + apiTestTemplateFiles.put("api_test.mustache", ".t"); + modelDocTemplateFiles.put("object_doc.mustache", ".md"); + apiDocTemplateFiles.put("api_doc.mustache", ".md"); + embeddedTemplateDir = templateDir = "perl"; + + + setReservedWordsLowerCase( + Arrays.asList( + "else", "lock", "qw", + "__END__", "elsif", "lt", "qx", + "__FILE__", "eq", "m", "s", + "__LINE__", "exp", "ne", "sub", + "__PACKAGE__", "for", "no", "tr", + "and", "foreach", "or", "unless", + "cmp", "ge", "package", "until", + "continue", "gt", "q", "while", + "CORE", "if", "qq", "xor", + "do", "le", "qr", "y", + "return" + ) + ); + + languageSpecificPrimitives.clear(); + languageSpecificPrimitives.add("int"); + languageSpecificPrimitives.add("double"); + languageSpecificPrimitives.add("string"); + languageSpecificPrimitives.add("boolean"); + languageSpecificPrimitives.add("DateTime"); + languageSpecificPrimitives.add("ARRAY"); + languageSpecificPrimitives.add("HASH"); + languageSpecificPrimitives.add("object"); + + typeMapping.clear(); + typeMapping.put("integer", "int"); + typeMapping.put("long", "int"); + typeMapping.put("float", "double"); + typeMapping.put("double", "double"); + typeMapping.put("boolean", "boolean"); + typeMapping.put("string", "string"); + typeMapping.put("date", "DateTime"); + typeMapping.put("DateTime", "DateTime"); + typeMapping.put("password", "string"); + typeMapping.put("array", "ARRAY"); + typeMapping.put("map", "HASH"); + typeMapping.put("object", "object"); + //TODO binary should be mapped to byte array + // mapped to String as a workaround + typeMapping.put("binary", "string"); + typeMapping.put("ByteArray", "string"); + + cliOptions.clear(); + cliOptions.add(new CliOption(MODULE_NAME, "Perl module name (convention: CamelCase or Long::Module).").defaultValue("SwaggerClient")); + cliOptions.add(new CliOption(MODULE_VERSION, "Perl module version.").defaultValue("1.0.0")); + cliOptions.add(CliOption.newBoolean(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, + CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG_DESC).defaultValue(Boolean.TRUE.toString())); + cliOptions.add(CliOption.newBoolean(CodegenConstants.ENSURE_UNIQUE_PARAMS, CodegenConstants + .ENSURE_UNIQUE_PARAMS_DESC).defaultValue(Boolean.TRUE.toString())); + cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC) + .defaultValue(Boolean.TRUE.toString())); + } + + + @Override + public void processOpts() { + super.processOpts(); + + if (additionalProperties.containsKey(MODULE_VERSION)) { + setModuleVersion((String) additionalProperties.get(MODULE_VERSION)); + } else { + additionalProperties.put(MODULE_VERSION, moduleVersion); + } + + if (additionalProperties.containsKey(MODULE_NAME)) { + setModuleName((String) additionalProperties.get(MODULE_NAME)); + setModulePathPart(moduleName.replaceAll("::", Matcher.quoteReplacement(File.separator))); + } else { + additionalProperties.put(MODULE_NAME, moduleName); + } + + // make api and model doc path available in mustache template + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + + // 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())); + } + + supportingFiles.add(new SupportingFile("ApiClient.mustache", ("lib/" + modulePathPart).replace('/', File.separatorChar), "ApiClient.pm")); + supportingFiles.add(new SupportingFile("Configuration.mustache", ("lib/" + modulePathPart).replace('/', File.separatorChar), "Configuration.pm")); + supportingFiles.add(new SupportingFile("ApiFactory.mustache", ("lib/" + modulePathPart).replace('/', File.separatorChar), "ApiFactory.pm")); + supportingFiles.add(new SupportingFile("Role.mustache", ("lib/" + modulePathPart).replace('/', File.separatorChar), "Role.pm")); + supportingFiles.add(new SupportingFile("AutoDoc.mustache", ("lib/" + modulePathPart + "/Role").replace('/', File.separatorChar), "AutoDoc.pm")); + supportingFiles.add(new SupportingFile("autodoc.script.mustache", "bin", "autodoc")); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); + supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh")); + } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String getName() { + return "perl"; + } + + @Override + public String getHelp() { + return "Generates a Perl client library."; + } + + @Override + public String escapeReservedWord(String name) { + if(this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "_" + name; + } + + @Override + public String apiFileFolder() { + return (outputFolder + "/lib/" + modulePathPart + apiPackage()).replace('/', File.separatorChar); + } + + @Override + public String modelFileFolder() { + return (outputFolder + "/lib/" + modulePathPart + modelPackage()).replace('/', File.separatorChar); + } + + @Override + public String apiTestFileFolder() { + return (outputFolder + "/t").replace('/', File.separatorChar); + } + + @Override + public String modelTestFileFolder() { + return (outputFolder + "/t").replace('/', File.separatorChar); + } + + @Override + public String apiDocFileFolder() { + return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); + } + + @Override + public String modelDocFileFolder() { + return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar); + } + + @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) + "[string," + 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 = swaggerType; + } + if (type == null) { + return null; + } + return toModelName(type); + } + + @Override + public String toDefaultValue(Schema p) { + if (p instanceof StringSchema) { + StringSchema dp = (StringSchema) p; + if (dp.getDefault() != null) { + return "'" + dp.getDefault() + "'"; + } + } else if (p instanceof BooleanSchema) { + BooleanSchema dp = (BooleanSchema) p; + if (dp.getDefault() != null) { + return dp.getDefault().toString(); + } + } 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 String toVarName(String name) { + // return the name in underscore style + // PhoneNumber => phone_number + name = underscore(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'. + + // parameter name starting with number won't compile + // need to escape it by appending _ at the beginning + if (name.matches("^\\d.*")) { + name = "_" + name; + } + return name; + } + + @Override + public String toParamName(String name) { + // 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'. + + // model name cannot use reserved keyword + if (isReservedWord(name)) { + LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + camelize("model_" + name)); + name = "model_" + name; + } + + // 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) + } + + // add prefix/suffic to model name + 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) { + // should be the same as the model name + return toModelName(name); + } + + @Override + public String toModelTestFilename(String name) { + return toModelFilename(name) + "Test"; + } + + @Override + public String toModelDocFilename(String name) { + return toModelFilename(name); + } + + @Override + public String toApiTestFilename(String name) { + return toApiFilename(name) + "Test"; + } + + @Override + public String toApiDocFilename(String name) { + return toApiFilename(name); + } + + @Override + public String toApiFilename(String name) { + // replace - with _ e.g. created-at => created_at + name = name.replaceAll("-", "_"); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'. + + // e.g. phone_number_api.rb => PhoneNumberApi.rb + return camelize(name) + "Api"; + } + + @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 toOperationId(String operationId) { + //rename to empty_function_name_1 (e.g.) if method name is empty + if (StringUtils.isEmpty(operationId)) { + operationId = underscore("empty_function_name_" + emptyFunctionNameCounter++); + LOGGER.warn("Empty method name (operationId) found. Renamed to " + operationId); + return operationId; + } + + // 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("call_" + operationId)); + return underscore("call_" + operationId); + } + + //return underscore(operationId).replaceAll("[^A-Za-z0-9_]", ""); + return underscore(sanitizeName(operationId)); + } + + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + + public void setModulePathPart(String modulePathPart) { + this.modulePathPart = modulePathPart; + } + + public void setModuleVersion(String moduleVersion) { + this.moduleVersion = moduleVersion; + } + + @Override + public void setParameterExampleValue(CodegenParameter p) { + if (Boolean.TRUE.equals(p.isString) || Boolean.TRUE.equals(p.isBinary) || + Boolean.TRUE.equals(p.isByteArray) || Boolean.TRUE.equals(p.isFile)) { + p.example = "'" + p.example + "'"; + } else if (Boolean.TRUE.equals(p.isBoolean)) { + if (Boolean.parseBoolean(p.example)) + p.example = "1"; + else + p.example = "0"; + } else if (Boolean.TRUE.equals(p.isDateTime) || Boolean.TRUE.equals(p.isDate)) { + p.example = "DateTime->from_epoch(epoch => str2time('" + p.example + "'))"; + } + + } + + @Override + public String escapeQuotationMark(String input) { + return input.replace("'", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + // remove =end, =cut to avoid code injection + return input.replace("=begin", "=_begin").replace("=end", "=_end").replace("=cut", "=_cut").replace("=pod", "=_pod"); + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index ec883bb6578..865151f8011 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -13,6 +13,7 @@ org.openapitools.codegen.languages.HaskellServantCodegen org.openapitools.codegen.languages.LuaClientCodegen org.openapitools.codegen.languages.LumenServerCodegen org.openapitools.codegen.languages.ObjcClientCodegen +org.openapitools.codegen.languages.PerlClientCodegen org.openapitools.codegen.languages.PhpClientCodegen org.openapitools.codegen.languages.PowerShellClientCodegen org.openapitools.codegen.languages.PythonClientCodegen diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/perl/PerlClientOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/perl/PerlClientOptionsTest.java new file mode 100644 index 00000000000..a6c7c05d168 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/perl/PerlClientOptionsTest.java @@ -0,0 +1,37 @@ +package org.openapitools.codegen.perl; + +import org.openapitools.codegen.AbstractOptionsTest; +import org.openapitools.codegen.CodegenConfig; +import org.openapitools.codegen.languages.PerlClientCodegen; +import org.openapitools.codegen.options.PerlClientOptionsProvider; + +import mockit.Expectations; +import mockit.Tested; + +public class PerlClientOptionsTest extends AbstractOptionsTest { + + @Tested + private PerlClientCodegen clientCodegen; + + public PerlClientOptionsTest() { + super(new PerlClientOptionsProvider()); + } + + @Override + protected CodegenConfig getCodegenConfig() { + return clientCodegen; + } + + @SuppressWarnings("unused") + @Override + protected void setExpectations() { + new Expectations(clientCodegen) {{ + clientCodegen.setModuleName(PerlClientOptionsProvider.MODULE_NAME_VALUE); + times = 1; + clientCodegen.setModuleVersion(PerlClientOptionsProvider.MODULE_VERSION_VALUE); + times = 1; + clientCodegen.setPrependFormOrBodyParameters(Boolean.valueOf(PerlClientOptionsProvider.PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)); + times = 1; + }}; + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/options/PerlClientOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/options/PerlClientOptionsProvider.java new file mode 100644 index 00000000000..09283b99259 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/options/PerlClientOptionsProvider.java @@ -0,0 +1,36 @@ +package org.openapitools.codegen.options; + +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.languages.PerlClientCodegen; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public class PerlClientOptionsProvider implements OptionsProvider { + public static final String MODULE_NAME_VALUE = ""; + public static final String MODULE_VERSION_VALUE = ""; + public static final String PREPEND_FORM_OR_BODY_PARAMETERS_VALUE = "true"; + + @Override + public String getLanguage() { + return "perl"; + } + + @Override + public Map createOptions() { + ImmutableMap.Builder builder = new ImmutableMap.Builder(); + return builder.put(PerlClientCodegen.MODULE_NAME, MODULE_NAME_VALUE) + .put(PerlClientCodegen.MODULE_VERSION, MODULE_VERSION_VALUE) + .put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, "true") + .put(CodegenConstants.ENSURE_UNIQUE_PARAMS, "true") + .put("hideGenerationTimestamp", "true") + .put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE) + .build(); + } + + @Override + public boolean isServer() { + return false; + } +}