[Elixir] Fix generation issues and compilation warnings in Elixir generator (#18788)

* Format Elixir generator

* Update Elixir reserved words

* Update Elixir generator docs

* Improve typespec generation to avoid double ".t" issues

* Fix compilation warnings by changing reserved words to use suffix instead of underscore prefix

* Include additional reserved words and handle words with leading underscores

* Update samples and docs

* Uses dataType instead of baseType for non-struct types

* Generate elixir samples

* Fixes issue with AnyType in a list

* Generate elixir samples

* Removes normalizeTypeName for arrays as they correct by getTypeDeclaration

* CodeStyle

---------

Co-authored-by: Michael Ramstein <michael@ramste.in>
This commit is contained in:
Nate Todd 2024-06-10 08:06:16 -04:00 committed by GitHub
parent 80bb3dde0b
commit 00f2cd573c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 142 additions and 123 deletions

View File

@ -68,9 +68,22 @@ These options may be applied as additional-properties (cli) or configOptions (pl
<li>__ENV__</li>
<li>__FILE__</li>
<li>__MODULE__</li>
<li>__struct__</li>
<li>after</li>
<li>and</li>
<li>catch</li>
<li>do</li>
<li>else</li>
<li>end</li>
<li>false</li>
<li>fn</li>
<li>in</li>
<li>nil</li>
<li>not</li>
<li>or</li>
<li>rescue</li>
<li>true</li>
<li>when</li>
</ul>
## FEATURE SET

View File

@ -63,8 +63,7 @@ public class ElixirClientCodegen extends DefaultCodegen {
"{:tesla, \"~> 1.7\"}",
"{:jason, \"~> 1.4\"}",
"{:ex_doc, \"~> 0.30\", only: :dev, runtime: false}",
"{:dialyxir, \"~> 1.3\", only: [:dev, :test], runtime: false}"
);
"{:dialyxir, \"~> 1.3\", only: [:dev, :test], runtime: false}");
public ElixirClientCodegen() {
super();
@ -73,114 +72,117 @@ public class ElixirClientCodegen extends DefaultCodegen {
.includeDocumentationFeatures(DocumentationFeature.Readme)
.securityFeatures(EnumSet.of(
SecurityFeature.OAuth2_Implicit,
SecurityFeature.BasicAuth
))
SecurityFeature.BasicAuth))
.excludeGlobalFeatures(
GlobalFeature.XMLStructureDefinitions,
GlobalFeature.Callbacks,
GlobalFeature.LinkObjects,
GlobalFeature.ParameterStyling
)
GlobalFeature.ParameterStyling)
.excludeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism
)
SchemaSupportFeature.Polymorphism)
.excludeParameterFeatures(
ParameterFeature.Cookie
)
ParameterFeature.Cookie)
.includeClientModificationFeatures(
ClientModificationFeature.BasePath
)
ClientModificationFeature.BasePath)
.includeDataTypeFeatures(
DataTypeFeature.AnyType
)
);
DataTypeFeature.AnyType));
// set the output folder here
outputFolder = "generated-code/elixir";
/*
* Models. You can write model files using the modelTemplateFiles map.
* Models. You can write model files using the modelTemplateFiles map.
* if you want to create one template for file, you can do so here.
* for multiple files for model, just put another entry in the `modelTemplateFiles` with
* for multiple files for model, just put another entry in the
* `modelTemplateFiles` with
* a different extension
*/
modelTemplateFiles.put(
"model.mustache", // the template to use
".ex"); // the extension for each file to write
".ex"); // the extension for each file to write
/**
* Api classes. You can write classes for each Api file with the apiTemplateFiles map.
* as with models, add multiple entries with different extensions for multiple files per
* Api classes. You can write classes for each Api file with the
* apiTemplateFiles map.
* as with models, add multiple entries with different extensions for multiple
* files per
* class
*/
apiTemplateFiles.put(
"api.mustache", // the template to use
".ex"); // the extension for each file to write
"api.mustache", // the template to use
".ex"); // the extension for each file to write
/**
* Template Location. This is the location which templates will be read from. The generator
* 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.
*/
templateDir = "elixir";
/**
* Reserved words. Override this with reserved words specific to your language
* Ref: https://github.com/itsgreggreg/elixir_quick_reference#reserved-words
* Reserved words. Override this with reserved words specific to your language
* Ref: https://hexdocs.pm/elixir/1.16.3/syntax-reference.html#reserved-words
*/
reservedWords = new HashSet<>(
Arrays.asList(
"nil",
"true",
"false",
"nil",
"when",
"and",
"or",
"not",
"in",
"fn",
"do",
"end",
"catch",
"rescue",
"after",
"else",
"__struct__",
"__MODULE__",
"__FILE__",
"__DIR__",
"__ENV__",
"__CALLER__")
);
"__CALLER__"));
/**
* Additional Properties. These values can be passed to the templates and
* Additional Properties. These values can be passed to the templates and
* are available in models, apis, and supporting files
*/
additionalProperties.put("apiVersion", apiVersion);
/**
* 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
* 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.md.mustache", // the input template or file
"", // the destination folder, relative `outputFolder`
"README.md") // the output file
supportingFiles.add(new SupportingFile("README.md.mustache", // the input template or file
"", // the destination folder, relative `outputFolder`
"README.md") // the output file
);
supportingFiles.add(new SupportingFile("config.exs.mustache",
"config",
"config.exs")
);
"config.exs"));
supportingFiles.add(new SupportingFile("runtime.exs.mustache",
"config",
"runtime.exs")
);
"runtime.exs"));
supportingFiles.add(new SupportingFile("mix.exs.mustache",
"",
"mix.exs")
);
"mix.exs"));
supportingFiles.add(new SupportingFile("formatter.exs",
"",
".formatter.exs")
);
".formatter.exs"));
supportingFiles.add(new SupportingFile("test_helper.exs.mustache",
"test",
"test_helper.exs")
);
"test_helper.exs"));
supportingFiles.add(new SupportingFile("gitignore.mustache",
"",
".gitignore")
);
".gitignore"));
/**
* Language Specific Primitives. These types will not trigger imports by
* Language Specific Primitives. These types will not trigger imports by
* the client generator
*/
languageSpecificPrimitives = new HashSet<>(
@ -196,12 +198,13 @@ public class ElixirClientCodegen extends DefaultCodegen {
"AnyType",
"Tuple",
"PID",
"map()", // This is a workaround, since the DefaultCodeGen uses our elixir TypeSpec datetype to evaluate the primitive
"any()"
)
);
// This is a workaround, since the DefaultCodeGen uses our elixir TypeSpec
// datetype to evaluate the primitive
"map()",
"any()"));
// ref: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types
// ref:
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types
typeMapping = new HashMap<>();
typeMapping.put("integer", "Integer");
typeMapping.put("long", "Integer");
@ -223,7 +226,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
typeMapping.put("UUID", "String");
typeMapping.put("URI", "String");
cliOptions.add(new CliOption(CodegenConstants.INVOKER_PACKAGE, "The main namespace to use for all classes. e.g. Yay.Pets"));
cliOptions.add(new CliOption(CodegenConstants.INVOKER_PACKAGE,
"The main namespace to use for all classes. e.g. Yay.Pets"));
cliOptions.add(new CliOption("licenseHeader", "The license header to prepend to the top of all source files."));
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Elixir package name (convention: lowercase)."));
}
@ -240,7 +244,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
/**
* Configures a friendly name for the generator. This will be used by the generator
* Configures a friendly name for the generator. This will be used by the
* generator
* to select the library with the -g flag.
*
* @return the friendly name for the generator
@ -251,7 +256,7 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
/**
* Returns human-friendly help for the generator. Provide the consumer with help
* Returns human-friendly help for the generator. Provide the consumer with help
* tips, parameters here
*
* @return A string value for the help message
@ -323,7 +328,6 @@ public class ElixirClientCodegen extends DefaultCodegen {
sourceFolder(),
"request_builder.ex"));
supportingFiles.add(new SupportingFile("deserializer.ex.mustache",
sourceFolder(),
"deserializer.ex"));
@ -408,34 +412,39 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
private String atomized(String text) {
StringBuilder atom = new StringBuilder();
Matcher m = simpleAtomPattern.matcher(text);
StringBuilder atom = new StringBuilder();
Matcher m = simpleAtomPattern.matcher(text);
atom.append(":");
atom.append(":");
if (!m.matches()) {
atom.append("\"");
}
if (!m.matches()) {
atom.append("\"");
}
atom.append(text);
atom.append(text);
if (!m.matches()) {
atom.append("\"");
}
if (!m.matches()) {
atom.append("\"");
}
return atom.toString();
return atom.toString();
}
/**
* Escapes a reserved word as defined in the `reservedWords` array. Handle escaping
* those terms here. This logic is only called if a variable matches the reserved words
* Escapes a reserved word as defined in the `reservedWords` array. Handle
* escaping
* those terms here. This logic is only called if a variable matches the
* reserved words
*
* @return the escaped term
*/
@Override
public String escapeReservedWord(String name) {
return "_" + name; // add an underscore to the name
String escapedName = name + "_var";
// Trim leading underscores in the event the name is already underscored
escapedName = escapedName.replaceAll("^_+", "");
return escapedName;
}
private String sourceFolder() {
@ -447,7 +456,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
/**
* Location to write model files. You can use the modelPackage() as defined when the class is
* Location to write model files. You can use the modelPackage() as defined when
* the class is
* instantiated
*/
@Override
@ -456,7 +466,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
/**
* Location to write api files. You can use the apiPackage() as defined when the class is
* Location to write api files. You can use the apiPackage() as defined when the
* class is
* instantiated
*/
@Override
@ -529,20 +540,23 @@ public class ElixirClientCodegen extends DefaultCodegen {
@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)
// 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("{} (reserved word) cannot be used as method name. Renamed to {}", operationId, underscore(sanitizeName("call_" + operationId)));
LOGGER.warn("{} (reserved word) cannot be used as method name. Renamed to {}", operationId,
underscore(sanitizeName("call_" + operationId)));
return underscore(sanitizeName("call_" + operationId));
}
// operationId starts with a number
if (operationId.matches("^\\d.*")) {
LOGGER.warn("{} (starting with a number) cannot be used as method name. Renamed to {}", operationId, underscore(sanitizeName("call_" + operationId)));
LOGGER.warn("{} (starting with a number) cannot be used as method name. Renamed to {}", operationId,
underscore(sanitizeName("call_" + operationId)));
operationId = "call_" + operationId;
}
@ -550,10 +564,12 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
/**
* Optional - type declaration. This is a String which is used by the templates to instantiate your
* types. There is typically special handling for different property types
* Optional - type declaration. This is a String which is used by the templates
* to instantiate your
* types. There is typically special handling for different property types
*
* @return a string value used as the `dataType` field for model templates, `returnType` for api templates
* @return a string value used as the `dataType` field for model templates,
* `returnType` for api templates
*/
@Override
public String getTypeDeclaration(Schema p) {
@ -603,8 +619,10 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
/**
* Optional - OpenAPI type conversion. This is used to map OpenAPI types in a `Schema` into
* either language specific types via `typeMapping` or into complex models if there is not a mapping.
* Optional - OpenAPI type conversion. This is used to map OpenAPI types in a
* `Schema` into
* either language specific types via `typeMapping` or into complex models if
* there is not a mapping.
*
* @return a string value of the type or complex model for this property
*/
@ -801,23 +819,10 @@ public class ElixirClientCodegen extends DefaultCodegen {
if (exResponse.baseType == null) {
returnEntry.append("nil");
} else if (exResponse.containerType == null) { // not container (array, map, set)
if (!exResponse.primitiveType) {
returnEntry.append(moduleName);
returnEntry.append(".Model.");
}
translateBaseType(returnEntry, exResponse.baseType);
returnEntry.append(normalizeTypeName(exResponse.dataType, exResponse.primitiveType));
} else {
if (exResponse.containerType.equals("array") ||
exResponse.containerType.equals("set")) {
returnEntry.append("list(");
if (!exResponse.primitiveType) {
returnEntry.append(moduleName);
returnEntry.append(".Model.");
}
translateBaseType(returnEntry, exResponse.baseType);
returnEntry.append(")");
if (exResponse.containerType.equals("array") || exResponse.containerType.equals("set")) {
returnEntry.append(exResponse.dataType);
} else if (exResponse.containerType.equals("map")) {
returnEntry.append("map()");
}
@ -833,6 +838,22 @@ public class ElixirClientCodegen extends DefaultCodegen {
return sb.toString();
}
private String normalizeTypeName(String baseType, boolean isPrimitive) {
if (baseType == null) {
return "nil";
}
if (isPrimitive || "String.t".equals(baseType)) {
return baseType;
}
if (!baseType.startsWith(moduleName + ".Model.")) {
baseType = moduleName + ".Model." + baseType;
}
if (!baseType.endsWith(".t")) {
baseType += ".t";
}
return baseType;
}
private void buildTypespec(CodegenParameter param, StringBuilder sb) {
if (param.dataType == null) {
sb.append("nil");
@ -846,26 +867,15 @@ public class ElixirClientCodegen extends DefaultCodegen {
sb.append("%{optional(String.t) => ");
buildTypespec(param.items, sb);
sb.append("}");
} else if (param.isPrimitiveType) {
// like `integer()`, `String.t`
sb.append(param.dataType);
} else if (param.isFile || param.isBinary) {
sb.append("String.t");
} else if ("String.t".equals(param.dataType)) {
// uuid, password, etc
sb.append(param.dataType);
} else {
// <module>.Model.<type>.t
sb.append(moduleName);
sb.append(".Model.");
sb.append(param.dataType);
sb.append(".t");
sb.append(normalizeTypeName(param.dataType, param.isPrimitiveType || param.isFile || param.isBinary));
}
}
private void buildTypespec(CodegenProperty property, StringBuilder sb) {
if (property == null) {
LOGGER.error("CodegenProperty cannot be null. Please report the issue to https://github.com/openapitools/openapi-generator with the spec");
LOGGER.error(
"CodegenProperty cannot be null. Please report the issue to https://github.com/openapitools/openapi-generator with the spec");
} else if (property.isArray) {
sb.append("list(");
buildTypespec(property.items, sb);
@ -874,14 +884,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
sb.append("%{optional(String.t) => ");
buildTypespec(property.items, sb);
sb.append("}");
} else if (property.isPrimitiveType) {
sb.append(property.baseType);
sb.append(".t");
} else {
sb.append(moduleName);
sb.append(".Model.");
sb.append(property.baseType);
sb.append(".t");
sb.append(normalizeTypeName(property.dataType, property.isPrimitiveType));
}
}
@ -982,6 +986,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
}
@Override
public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.ELIXIR; }
public GeneratorLanguage generatorLanguage() {
return GeneratorLanguage.ELIXIR;
}
}

View File

@ -288,7 +288,7 @@ defmodule OpenapiPetstore.Api.Fake do
- `{:ok, nil}` on success
- `{:error, Tesla.Env.t}` on failure
"""
@spec test_additional_properties_reference(Tesla.Env.client, %{optional(String.t) => AnyType.t}, keyword()) :: {:ok, nil} | {:error, Tesla.Env.t}
@spec test_additional_properties_reference(Tesla.Env.client, %{optional(String.t) => any()}, keyword()) :: {:ok, nil} | {:error, Tesla.Env.t}
def test_additional_properties_reference(connection, request_body, _opts \\ []) do
request =
%{}

View File

@ -93,7 +93,7 @@ defmodule OpenapiPetstore.Api.Pet do
- `{:ok, [%Pet{}, ...]}` on success
- `{:error, Tesla.Env.t}` on failure
"""
@spec find_pets_by_status(Tesla.Env.client, list(String.t), keyword()) :: {:ok, nil} | {:ok, list(OpenapiPetstore.Model.Pet.t)} | {:error, Tesla.Env.t}
@spec find_pets_by_status(Tesla.Env.client, list(String.t), keyword()) :: {:ok, nil} | {:ok, [OpenapiPetstore.Model.Pet.t]} | {:error, Tesla.Env.t}
def find_pets_by_status(connection, status, _opts \\ []) do
request =
%{}
@ -125,7 +125,7 @@ defmodule OpenapiPetstore.Api.Pet do
- `{:ok, [%Pet{}, ...]}` on success
- `{:error, Tesla.Env.t}` on failure
"""
@spec find_pets_by_tags(Tesla.Env.client, list(String.t), keyword()) :: {:ok, nil} | {:ok, list(OpenapiPetstore.Model.Pet.t)} | {:error, Tesla.Env.t}
@spec find_pets_by_tags(Tesla.Env.client, list(String.t), keyword()) :: {:ok, nil} | {:ok, [OpenapiPetstore.Model.Pet.t]} | {:error, Tesla.Env.t}
def find_pets_by_tags(connection, tags, _opts \\ []) do
request =
%{}