Skip to content

Commit

Permalink
Improved Elixir Code Generation (#12751)
Browse files Browse the repository at this point in the history
* Bump the minimum version of Elixir supported

The previous minimum version of Elixir is several years EOL.

The current minimum version of Elixir is also EOL, but is the minimum
version required to support some upcoming changes to the config
templates.

* Bump the minimum version fo Tesla

Keep the dependencies up to date

* Add a default .formatter.exs

* Add two Elixir-specific mustache lambdas

- The `atom` lambda results in the proper quoting of an atom depending
  on the safe contents of the atom text, per the Elixir language
  specification. That is, `{{#atom}}foo{{/atom}}` will be turned into
  `:foo` and `{{#atom}foo.bar{{/atom}}` will be turned into
  `:"foo.bar"`.

- The `env_var` lambda results in the treatment of the identifier
  provided being capitalized as an environment variable would be.
  `{{#env_var}}apiVersion{{/env_var}}` would become `ENV_VAR`.

* Use modern Elixir configuration

- This includes runtime configuration
- It depends on the `env_var` lambda.

* Fix a Language Server Warning

This change is *optional*, but removes a LS warning that was raised.

* Regenerated openapi_petstore for Elixir

* Add ex_doc as a default dependency

Fixes #12484

* Refine the regular expression for atoms

The original regex incorrectly matched `123Number` (unquoted atoms
cannot begin with numbers) and would incorrectly quote atoms ending in
`?` or `!`. Through testing with `iex`, it also turns out that the atom
`:-` is legal.

The following atoms will now not be quoted that would have been
incorrectly quoted:

- `:-`
- `:declawed?`
- `:neutered!`

The following atoms will be quoted that were incorrectly unquoted:

- `:"123Number"`

* Improve regex (again), remove files not generated

- The previous commit resulted in a number of warnings that were still
  present and so I played with the regular expression. This did not
  solve the problem, but the resulting regular expression is *much*
  better than the previous one, so I'm keeping it.

- The problem was that the configuration (`bin/configs/elixir.yaml`) is
  generated using a 3.0 input spec:

  ```yaml
  inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml
  ```

  Which means that there were 16 files committed which were no longer
  being generated. When I tested with the 2.0 input spec:

  ```yaml
  inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore-with-fake-endpoints-models-for-testing.yaml
  ```

  The files were generated again. I *believe* that the correct change
  here is to switch back to the 2.0 input spec, as it tests more code
  generation, but I wanted to check in before I did this.

  The following files are deleted:

  - `elixir/lib/openapi_petstore/model/additional_properties_any_type.ex`
  - `elixir/lib/openapi_petstore/model/additional_properties_array.ex`
  - `elixir/lib/openapi_petstore/model/additional_properties_boolean.ex`
  - `elixir/lib/openapi_petstore/model/additional_properties_integer.ex`
  - `elixir/lib/openapi_petstore/model/additional_properties_number.ex`
  - `elixir/lib/openapi_petstore/model/additional_properties_object.ex`
  - `elixir/lib/openapi_petstore/model/additional_properties_string.ex`
  - `elixir/lib/openapi_petstore/model/big_cat.ex`
  - `elixir/lib/openapi_petstore/model/big_cat_all_of.ex`
  - `elixir/lib/openapi_petstore/model/inline_response_default.ex`
  - `elixir/lib/openapi_petstore/model/special_model_name.ex`
  - `elixir/lib/openapi_petstore/model/type_holder_default.ex`
  - `elixir/lib/openapi_petstore/model/type_holder_example.ex`
  - `elixir/lib/openapi_petstore/model/xml_item.ex`
  - `elixir/pom.xml`
  - `elixir/test/pet_test.exs`

  In the interim, I have removed those files from the commit.
  • Loading branch information
halostatue authored Jul 3, 2022
1 parent f1dd44d commit 18a07ea
Show file tree
Hide file tree
Showing 71 changed files with 427 additions and 950 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,24 @@
import static org.openapitools.codegen.utils.StringUtils.camelize;
import static org.openapitools.codegen.utils.StringUtils.underscore;

public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig {
public class ElixirClientCodegen extends DefaultCodegen {
private final Logger LOGGER = LoggerFactory.getLogger(ElixirClientCodegen.class);

private final Pattern simpleAtomPattern = Pattern.compile("\\A(?:(?:[_@\\p{Alpha}][_@\\p{Alnum}]*[?!]?)|-)\\z");

protected String apiVersion = "1.0.0";
protected String moduleName;
protected static final String defaultModuleName = "OpenAPI.Client";

// This is the name of elixir project name;
protected static final String defaultPackageName = "openapi_client";

String supportedElixirVersion = "1.6";
String supportedElixirVersion = "1.10";
List<String> extraApplications = Arrays.asList(":logger");
List<String> deps = Arrays.asList(
"{:tesla, \"~> 1.2\"}",
"{:poison, \"~> 3.0\"}"
"{:tesla, \"~> 1.4\"}",
"{:poison, \"~> 3.0\"}",
"{:ex_doc, \"~> 0.28\", only: :dev, runtime: false}"
);

public ElixirClientCodegen() {
Expand Down Expand Up @@ -150,10 +153,18 @@ public ElixirClientCodegen() {
"config",
"config.exs")
);
supportingFiles.add(new SupportingFile("runtime.exs.mustache",
"config",
"runtime.exs")
);
supportingFiles.add(new SupportingFile("mix.exs.mustache",
"",
"mix.exs")
);
supportingFiles.add(new SupportingFile("formatter.exs",
"",
".formatter.exs")
);
supportingFiles.add(new SupportingFile("test_helper.exs.mustache",
"test",
"test_helper.exs")
Expand Down Expand Up @@ -262,6 +273,19 @@ public void execute(Template.Fragment fragment, Writer writer) throws IOExceptio
writer.write(modulized(fragment.execute()));
}
});
additionalProperties.put("atom", new Mustache.Lambda() {
@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
writer.write(atomized(fragment.execute()));
}
});
additionalProperties.put("env_var", new Mustache.Lambda() {
@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
String text = underscored(fragment.execute());
writer.write(text.toUpperCase(Locale.ROOT));
}
});

if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) {
setModuleName((String) additionalProperties.get(CodegenConstants.INVOKER_PACKAGE));
Expand Down Expand Up @@ -377,6 +401,26 @@ private String modulized(String words) {
return join("", modulizedWords);
}

private String atomized(String text) {
StringBuilder atom = new StringBuilder();
Matcher m = simpleAtomPattern.matcher(text);

atom.append(":");

if (!m.matches()) {
atom.append("\"");
}

atom.append(text);

if (!m.matches()) {
atom.append("\"");
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ defmodule {{moduleName}}.Api.{{classname}} do
:body => :body
{{/isBodyParam}}
{{^isBodyParam}}
:"{{baseName}}" => {{#isFormParam}}:form{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}{{^-last}},{{/-last}}
{{#atom}}{{baseName}}{{/atom}} => {{#isFormParam}}:form{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}{{^-last}},{{/-last}}
{{/isBodyParam}}
{{#-last}}
}
Expand All @@ -59,7 +59,7 @@ defmodule {{moduleName}}.Api.{{classname}} do
|> url("{{replacedPathName}}")
{{#requiredParams}}
{{^isPathParam}}
|> add_param({{#isBodyParam}}:body{{/isBodyParam}}{{#isFormParam}}{{#isMultipart}}{{#isFile}}:file{{/isFile}}{{^isFile}}:form{{/isFile}}{{/isMultipart}}{{^isMultipart}}:form{{/isMultipart}}{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}, {{#isBodyParam}}:body, {{/isBodyParam}}{{^isBodyParam}}:"{{baseName}}", {{/isBodyParam}}{{#underscored}}{{paramName}}{{/underscored}})
|> add_param({{#isBodyParam}}:body{{/isBodyParam}}{{#isFormParam}}{{#isMultipart}}{{#isFile}}:file{{/isFile}}{{^isFile}}:form{{/isFile}}{{/isMultipart}}{{^isMultipart}}:form{{/isMultipart}}{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}, {{#isBodyParam}}:body, {{/isBodyParam}}{{^isBodyParam}}{{#atom}}{{baseName}}{{/atom}}, {{/isBodyParam}}{{#underscored}}{{paramName}}{{/underscored}})
{{/isPathParam}}
{{/requiredParams}}
{{#optionalParams}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :{{#underscored}}{{appName}}{{/underscored}}, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:{{#underscored}}{{appName}}{{/underscored}}, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
import Config

config :{{#underscored}}{{appName}}{{/underscored}}, base_url: "{{{basePath}}}"

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
#
# import_config "#{Mix.env}.exs"
# import_config "#{config_env()}.exs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule {{moduleName}}.Mixfile do
defp package() do
[
name: "{{#underscored}}{{packageName}}{{/underscored}}",
files: ~w(lib mix.exs README* LICENSE*),
files: ~w(.formatter.exs config lib mix.exs README* LICENSE*),
licenses: ["{{licenseId}}"]
]
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

@derive [Poison.Encoder]
defstruct [
{{#vars}}:"{{&baseName}}"{{^-last}},
{{#vars}}{{#atom}}{{&baseName}}{{/atom}}{{^-last}},
{{/-last}}{{/vars}}
]

@type t :: %__MODULE__{
{{#vars}}:"{{&baseName}}" => {{{datatype}}}{{#isNullable}} | nil{{/isNullable}}{{^isNullable}}{{^required}} | nil{{/required}}{{/isNullable}}{{^-last}},
{{#vars}}{{#atom}}{{&baseName}}{{/atom}} => {{{datatype}}}{{#isNullable}} | nil{{/isNullable}}{{^isNullable}}{{^required}} | nil{{/required}}{{/isNullable}}{{^-last}},
{{/-last}}{{/vars}}
}
end
Expand All @@ -23,7 +23,7 @@ defimpl Poison.Decoder, for: {{&moduleName}}.Model.{{&classname}} do
value
{{#vars}}
{{^isPrimitiveType}}
{{#baseType}}|> deserialize(:"{{&baseName}}", {{#isArray}}:list, {{&moduleName}}.Model.{{{items.baseType}}}{{/isArray}}{{#isMap}}:map, {{&moduleName}}.Model.{{{items.baseType}}}{{/isMap}}{{#isDate}}:date, nil{{/isDate}}{{#isDateTime}}:date, nil{{/isDateTime}}{{^isDate}}{{^isDateTime}}{{^isMap}}{{^isArray}}:struct, {{moduleName}}.Model.{{baseType}}{{/isArray}}{{/isMap}}{{/isDateTime}}{{/isDate}}, options)
{{#baseType}}|> deserialize({{#atom}}{{&baseName}}{{/atom}}, {{#isArray}}:list, {{&moduleName}}.Model.{{{items.baseType}}}{{/isArray}}{{#isMap}}:map, {{&moduleName}}.Model.{{{items.baseType}}}{{/isMap}}{{#isDate}}:date, nil{{/isDate}}{{#isDateTime}}:date, nil{{/isDateTime}}{{^isDate}}{{^isDateTime}}{{^isMap}}{{^isArray}}:struct, {{moduleName}}.Model.{{baseType}}{{/isArray}}{{/isMap}}{{/isDateTime}}{{/isDate}}, options)
{{/baseType}}
{{/isPrimitiveType}}
{{/vars}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Config

# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration
# and secrets from environment variables or elsewhere. Do not define
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.

if env = System.get_env("{{#env_var}}{{appName}}{{/env_var}}_BASE_URI") do
config :{{#underscored}}{{appName}}{{/underscored}}, base_url: env
end
3 changes: 3 additions & 0 deletions samples/client/petstore/elixir/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
3 changes: 3 additions & 0 deletions samples/client/petstore/elixir/.openapi-generator/FILES
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.formatter.exs
.gitignore
.openapi-generator-ignore
README.md
config/config.exs
config/runtime.exs
lib/openapi_petstore/api/another_fake.ex
lib/openapi_petstore/api/default.ex
lib/openapi_petstore/api/fake.ex
Expand Down
35 changes: 9 additions & 26 deletions samples/client/petstore/elixir/config/config.exs
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :open_api_petstore, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:open_api_petstore, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
import Config

config :open_api_petstore, base_url: "http://petstore.swagger.io:80/v2"

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
#
# import_config "#{Mix.env}.exs"
# import_config "#{config_env()}.exs"
12 changes: 12 additions & 0 deletions samples/client/petstore/elixir/config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Config

# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration
# and secrets from environment variables or elsewhere. Do not define
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.

if env = System.get_env("OPEN_API_PETSTORE_BASE_URI") do
config :open_api_petstore, base_url: env
end
Loading

0 comments on commit 18a07ea

Please sign in to comment.