[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>__ENV__</li>
<li>__FILE__</li> <li>__FILE__</li>
<li>__MODULE__</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>false</li>
<li>fn</li>
<li>in</li>
<li>nil</li> <li>nil</li>
<li>not</li>
<li>or</li>
<li>rescue</li>
<li>true</li> <li>true</li>
<li>when</li>
</ul> </ul>
## FEATURE SET ## FEATURE SET

View File

@ -63,8 +63,7 @@ public class ElixirClientCodegen extends DefaultCodegen {
"{:tesla, \"~> 1.7\"}", "{:tesla, \"~> 1.7\"}",
"{:jason, \"~> 1.4\"}", "{:jason, \"~> 1.4\"}",
"{:ex_doc, \"~> 0.30\", only: :dev, runtime: false}", "{: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() { public ElixirClientCodegen() {
super(); super();
@ -73,114 +72,117 @@ public class ElixirClientCodegen extends DefaultCodegen {
.includeDocumentationFeatures(DocumentationFeature.Readme) .includeDocumentationFeatures(DocumentationFeature.Readme)
.securityFeatures(EnumSet.of( .securityFeatures(EnumSet.of(
SecurityFeature.OAuth2_Implicit, SecurityFeature.OAuth2_Implicit,
SecurityFeature.BasicAuth SecurityFeature.BasicAuth))
))
.excludeGlobalFeatures( .excludeGlobalFeatures(
GlobalFeature.XMLStructureDefinitions, GlobalFeature.XMLStructureDefinitions,
GlobalFeature.Callbacks, GlobalFeature.Callbacks,
GlobalFeature.LinkObjects, GlobalFeature.LinkObjects,
GlobalFeature.ParameterStyling GlobalFeature.ParameterStyling)
)
.excludeSchemaSupportFeatures( .excludeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism SchemaSupportFeature.Polymorphism)
)
.excludeParameterFeatures( .excludeParameterFeatures(
ParameterFeature.Cookie ParameterFeature.Cookie)
)
.includeClientModificationFeatures( .includeClientModificationFeatures(
ClientModificationFeature.BasePath ClientModificationFeature.BasePath)
)
.includeDataTypeFeatures( .includeDataTypeFeatures(
DataTypeFeature.AnyType DataTypeFeature.AnyType));
)
);
// set the output folder here // set the output folder here
outputFolder = "generated-code/elixir"; 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. * 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 * a different extension
*/ */
modelTemplateFiles.put( modelTemplateFiles.put(
"model.mustache", // the template to use "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. * Api classes. You can write classes for each Api file with the
* as with models, add multiple entries with different extensions for multiple files per * apiTemplateFiles map.
* as with models, add multiple entries with different extensions for multiple
* files per
* class * class
*/ */
apiTemplateFiles.put( apiTemplateFiles.put(
"api.mustache", // the template to use "api.mustache", // the template to use
".ex"); // the extension for each file to write ".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. * will use the resource stream to attempt to read the templates.
*/ */
templateDir = "elixir"; templateDir = "elixir";
/** /**
* Reserved words. Override this with reserved words specific to your language * Reserved words. Override this with reserved words specific to your language
* Ref: https://github.com/itsgreggreg/elixir_quick_reference#reserved-words * Ref: https://hexdocs.pm/elixir/1.16.3/syntax-reference.html#reserved-words
*/ */
reservedWords = new HashSet<>( reservedWords = new HashSet<>(
Arrays.asList( Arrays.asList(
"nil",
"true", "true",
"false", "false",
"nil",
"when",
"and",
"or",
"not",
"in",
"fn",
"do",
"end",
"catch",
"rescue",
"after",
"else",
"__struct__",
"__MODULE__", "__MODULE__",
"__FILE__", "__FILE__",
"__DIR__", "__DIR__",
"__ENV__", "__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 * are available in models, apis, and supporting files
*/ */
additionalProperties.put("apiVersion", apiVersion); additionalProperties.put("apiVersion", apiVersion);
/** /**
* Supporting Files. You can write single files for the generator with the * 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 * 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 * 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 supportingFiles.add(new SupportingFile("README.md.mustache", // the input template or file
"", // the destination folder, relative `outputFolder` "", // the destination folder, relative `outputFolder`
"README.md") // the output file "README.md") // the output file
); );
supportingFiles.add(new SupportingFile("config.exs.mustache", supportingFiles.add(new SupportingFile("config.exs.mustache",
"config", "config",
"config.exs") "config.exs"));
);
supportingFiles.add(new SupportingFile("runtime.exs.mustache", supportingFiles.add(new SupportingFile("runtime.exs.mustache",
"config", "config",
"runtime.exs") "runtime.exs"));
);
supportingFiles.add(new SupportingFile("mix.exs.mustache", supportingFiles.add(new SupportingFile("mix.exs.mustache",
"", "",
"mix.exs") "mix.exs"));
);
supportingFiles.add(new SupportingFile("formatter.exs", supportingFiles.add(new SupportingFile("formatter.exs",
"", "",
".formatter.exs") ".formatter.exs"));
);
supportingFiles.add(new SupportingFile("test_helper.exs.mustache", supportingFiles.add(new SupportingFile("test_helper.exs.mustache",
"test", "test",
"test_helper.exs") "test_helper.exs"));
);
supportingFiles.add(new SupportingFile("gitignore.mustache", 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 * the client generator
*/ */
languageSpecificPrimitives = new HashSet<>( languageSpecificPrimitives = new HashSet<>(
@ -196,12 +198,13 @@ public class ElixirClientCodegen extends DefaultCodegen {
"AnyType", "AnyType",
"Tuple", "Tuple",
"PID", "PID",
"map()", // This is a workaround, since the DefaultCodeGen uses our elixir TypeSpec datetype to evaluate the primitive // This is a workaround, since the DefaultCodeGen uses our elixir TypeSpec
"any()" // 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 = new HashMap<>();
typeMapping.put("integer", "Integer"); typeMapping.put("integer", "Integer");
typeMapping.put("long", "Integer"); typeMapping.put("long", "Integer");
@ -223,7 +226,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
typeMapping.put("UUID", "String"); typeMapping.put("UUID", "String");
typeMapping.put("URI", "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("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).")); 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. * to select the library with the -g flag.
* *
* @return the friendly name for the generator * @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 * tips, parameters here
* *
* @return A string value for the help message * @return A string value for the help message
@ -323,7 +328,6 @@ public class ElixirClientCodegen extends DefaultCodegen {
sourceFolder(), sourceFolder(),
"request_builder.ex")); "request_builder.ex"));
supportingFiles.add(new SupportingFile("deserializer.ex.mustache", supportingFiles.add(new SupportingFile("deserializer.ex.mustache",
sourceFolder(), sourceFolder(),
"deserializer.ex")); "deserializer.ex"));
@ -408,34 +412,39 @@ public class ElixirClientCodegen extends DefaultCodegen {
} }
private String atomized(String text) { private String atomized(String text) {
StringBuilder atom = new StringBuilder(); StringBuilder atom = new StringBuilder();
Matcher m = simpleAtomPattern.matcher(text); Matcher m = simpleAtomPattern.matcher(text);
atom.append(":"); atom.append(":");
if (!m.matches()) { if (!m.matches()) {
atom.append("\""); atom.append("\"");
} }
atom.append(text); atom.append(text);
if (!m.matches()) { if (!m.matches()) {
atom.append("\""); atom.append("\"");
} }
return atom.toString(); return atom.toString();
} }
/** /**
* Escapes a reserved word as defined in the `reservedWords` array. Handle escaping * Escapes a reserved word as defined in the `reservedWords` array. Handle
* those terms here. This logic is only called if a variable matches the reserved words * escaping
* those terms here. This logic is only called if a variable matches the
* reserved words
* *
* @return the escaped term * @return the escaped term
*/ */
@Override @Override
public String escapeReservedWord(String name) { 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() { 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 * instantiated
*/ */
@Override @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 * instantiated
*/ */
@Override @Override
@ -529,20 +540,23 @@ public class ElixirClientCodegen extends DefaultCodegen {
@Override @Override
public String toOperationId(String operationId) { 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)) { if (StringUtils.isEmpty(operationId)) {
throw new RuntimeException("Empty method name (operationId) not allowed"); throw new RuntimeException("Empty method name (operationId) not allowed");
} }
// method name cannot use reserved keyword, e.g. return // method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) { 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)); return underscore(sanitizeName("call_" + operationId));
} }
// operationId starts with a number // operationId starts with a number
if (operationId.matches("^\\d.*")) { 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; 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 * Optional - type declaration. This is a String which is used by the templates
* types. There is typically special handling for different property types * 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 @Override
public String getTypeDeclaration(Schema p) { 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 * Optional - OpenAPI type conversion. This is used to map OpenAPI types in a
* either language specific types via `typeMapping` or into complex models if there is not a mapping. * `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 * @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) { if (exResponse.baseType == null) {
returnEntry.append("nil"); returnEntry.append("nil");
} else if (exResponse.containerType == null) { // not container (array, map, set) } else if (exResponse.containerType == null) { // not container (array, map, set)
if (!exResponse.primitiveType) { returnEntry.append(normalizeTypeName(exResponse.dataType, exResponse.primitiveType));
returnEntry.append(moduleName);
returnEntry.append(".Model.");
}
translateBaseType(returnEntry, exResponse.baseType);
} else { } else {
if (exResponse.containerType.equals("array") || if (exResponse.containerType.equals("array") || exResponse.containerType.equals("set")) {
exResponse.containerType.equals("set")) { returnEntry.append(exResponse.dataType);
returnEntry.append("list(");
if (!exResponse.primitiveType) {
returnEntry.append(moduleName);
returnEntry.append(".Model.");
}
translateBaseType(returnEntry, exResponse.baseType);
returnEntry.append(")");
} else if (exResponse.containerType.equals("map")) { } else if (exResponse.containerType.equals("map")) {
returnEntry.append("map()"); returnEntry.append("map()");
} }
@ -833,6 +838,22 @@ public class ElixirClientCodegen extends DefaultCodegen {
return sb.toString(); 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) { private void buildTypespec(CodegenParameter param, StringBuilder sb) {
if (param.dataType == null) { if (param.dataType == null) {
sb.append("nil"); sb.append("nil");
@ -846,26 +867,15 @@ public class ElixirClientCodegen extends DefaultCodegen {
sb.append("%{optional(String.t) => "); sb.append("%{optional(String.t) => ");
buildTypespec(param.items, sb); buildTypespec(param.items, sb);
sb.append("}"); 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 { } else {
// <module>.Model.<type>.t sb.append(normalizeTypeName(param.dataType, param.isPrimitiveType || param.isFile || param.isBinary));
sb.append(moduleName);
sb.append(".Model.");
sb.append(param.dataType);
sb.append(".t");
} }
} }
private void buildTypespec(CodegenProperty property, StringBuilder sb) { private void buildTypespec(CodegenProperty property, StringBuilder sb) {
if (property == null) { 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) { } else if (property.isArray) {
sb.append("list("); sb.append("list(");
buildTypespec(property.items, sb); buildTypespec(property.items, sb);
@ -874,14 +884,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
sb.append("%{optional(String.t) => "); sb.append("%{optional(String.t) => ");
buildTypespec(property.items, sb); buildTypespec(property.items, sb);
sb.append("}"); sb.append("}");
} else if (property.isPrimitiveType) {
sb.append(property.baseType);
sb.append(".t");
} else { } else {
sb.append(moduleName); sb.append(normalizeTypeName(property.dataType, property.isPrimitiveType));
sb.append(".Model.");
sb.append(property.baseType);
sb.append(".t");
} }
} }
@ -982,6 +986,8 @@ public class ElixirClientCodegen extends DefaultCodegen {
} }
@Override @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 - `{:ok, nil}` on success
- `{:error, Tesla.Env.t}` on failure - `{: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 def test_additional_properties_reference(connection, request_body, _opts \\ []) do
request = request =
%{} %{}

View File

@ -93,7 +93,7 @@ defmodule OpenapiPetstore.Api.Pet do
- `{:ok, [%Pet{}, ...]}` on success - `{:ok, [%Pet{}, ...]}` on success
- `{:error, Tesla.Env.t}` on failure - `{: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 def find_pets_by_status(connection, status, _opts \\ []) do
request = request =
%{} %{}
@ -125,7 +125,7 @@ defmodule OpenapiPetstore.Api.Pet do
- `{:ok, [%Pet{}, ...]}` on success - `{:ok, [%Pet{}, ...]}` on success
- `{:error, Tesla.Env.t}` on failure - `{: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 def find_pets_by_tags(connection, tags, _opts \\ []) do
request = request =
%{} %{}