Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and update Elixir generator #15746

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.openapitools.codegen.languages;

import com.google.common.base.Strings;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import io.swagger.v3.oas.models.OpenAPI;
Expand All @@ -38,11 +39,11 @@
import java.io.IOException;
import java.io.Writer;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.openapitools.codegen.utils.StringUtils.camelize;
import static org.openapitools.codegen.utils.StringUtils.underscore;
import static org.openapitools.codegen.utils.StringUtils.*;

public class ElixirClientCodegen extends DefaultCodegen {
private final Logger LOGGER = LoggerFactory.getLogger(ElixirClientCodegen.class);
Expand All @@ -59,11 +60,16 @@ public class ElixirClientCodegen extends DefaultCodegen {
String supportedElixirVersion = "1.10";
List<String> extraApplications = Arrays.asList(":logger");
List<String> deps = Arrays.asList(
"{:tesla, \"~> 1.4\"}",
"{:poison, \"~> 3.0\"}",
"{:tesla, \"~> 1.7\"}",
"{:jason, \"~> 1.4\"}",
"{:ex_doc, \"~> 0.28\", only: :dev, runtime: false}"
);

protected List<String> charactersToAllow = Collections.singletonList("_");

protected Set<String> keywordsThatDoNotSupportRawIdentifiers = new HashSet<>(
Arrays.asList());

public ElixirClientCodegen() {
super();

Expand Down Expand Up @@ -982,4 +988,92 @@ public void setModuleName(String moduleName) {

@Override
public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.ELIXIR; }

public enum CasingType {CAMEL_CASE, SNAKE_CASE};

/**
* General purpose sanitizing function for Elixir identifiers (fields, variables, structs, parameters, etc.).<br>
* @param name The input string
* @param casingType Which casing type to apply
* @param escapePrefix Prefix to escape words beginning with numbers or reserved words
* @param type The type of identifier (used for logging)
* @param allowRawIdentifiers Raw identifiers can't always be used, because of filename vs import mismatch.
* @return Sanitized string
*/
public String sanitizeIdentifier(String name, ElixirClientCodegen.CasingType casingType, String escapePrefix, String type, boolean allowRawIdentifiers) {
String originalName = name;

Function<String, String> casingFunction;
switch (casingType) {
case CAMEL_CASE:
// This probably seems odd, but it is necessary for two reasons
// Compatibility with rust-server, such that MyIDList => my_id_list => MyIdList
// Conversion from SCREAMING_SNAKE_CASE to ScreamingSnakeCase
casingFunction = (input) -> camelize(underscore(input));
break;
case SNAKE_CASE:
casingFunction = org.openapitools.codegen.utils.StringUtils::underscore;
break;
default:
throw new IllegalArgumentException("Unknown CasingType");
}

// Replace hyphens with underscores
name = name.replaceAll("-", "_");

// Apply special character escapes, e.g. "@type" => "At_type"
// Remove the trailing underscore if necessary
if (!Strings.isNullOrEmpty(name)) {
boolean endedWithUnderscore = name.endsWith("_");
name = escape(name, specialCharReplacements, charactersToAllow, "_");
if (!endedWithUnderscore && name.endsWith("_")) {
name = org.apache.commons.lang3.StringUtils.chop(name);
}
}

// Sanitize any other special characters that weren't replaced
name = sanitizeName(name);

// Keep track of modifications prior to casing
boolean nameWasModified = !originalName.equals(name);

// Convert casing
name = casingFunction.apply(name);

// If word starts with number add a prefix
// Note: this must be done after casing since CamelCase will strip leading underscores
if (name.matches("^\\d.*")) {
nameWasModified = true;
name = casingFunction.apply(escapePrefix + '_' + name);
}

// Escape reserved words - this is case-sensitive so must be done after casing
// There is currently a bug in Rust where this doesn't work for a few reserved words :(
// https://internals.rust-lang.org/t/raw-identifiers-dont-work-for-all-identifiers/9094
if (isReservedWord(name)) {
nameWasModified = true;
if (this.keywordsThatDoNotSupportRawIdentifiers.contains(name) || !allowRawIdentifiers) {
name = casingFunction.apply(escapePrefix + '_' + name);
} else {
name = "r#" + name;
};
}

// If the name had to be modified (not just because of casing), log the change
if (nameWasModified) {
LOGGER.warn("{} cannot be used as a {} name. Renamed to {}", casingFunction.apply(originalName), type, name);
}

return name;
}

@Override
public String toVarName(String name) {
return sanitizeIdentifier(name, CasingType.SNAKE_CASE, "param", "field/variable", true);
}

@Override
public String toParamName(String name) {
return sanitizeIdentifier(name, CasingType.SNAKE_CASE, "param", "parameter", true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ defmodule {{moduleName}}.Connection do

tesla_options = Application.get_env(:tesla, __MODULE__, [])
middleware = Keyword.get(tesla_options, :middleware, [])
json_engine = Keyword.get(tesla_options, :json, Poison)
json_engine = Keyword.get(tesla_options, :json, Jason)

user_agent =
Keyword.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ defmodule {{moduleName}}.Deserializer do
@spec deserialize(struct(), :atom, :atom, struct(), keyword()) :: struct()
def deserialize(model, field, :list, mod, options) do
model
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: [struct(mod)]]))))
|> Map.update!(field, &struct(mod,(Jason.decode!(&1, Keyword.merge(options, keys: :atoms!)))))
end

def deserialize(model, field, :struct, mod, options) do
model
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: struct(mod)]))))
|> Map.update!(field, &struct(mod,(Jason.decode!(&1, Keyword.merge(options, keys: :atoms!)))))
end

def deserialize(model, field, :map, mod, options) do
Expand All @@ -26,7 +26,7 @@ defmodule {{moduleName}}.Deserializer do
existing_value ->
Map.new(existing_value, fn
{key, val} ->
{key, Poison.Decode.decode(val, Keyword.merge(options, as: struct(mod)))}
{key, struct(mod, Jason.decode!(val, Keyword.merge(options, keys: :atoms!)))}
end)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule {{moduleName}}.Mixfile do
def project do
[
app: {{#atom}}{{#underscored}}{{packageName}}{{/underscored}}{{/atom}},
version: "{{appVersion}}",
version: "{{packageVersion}}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Samples will fail, due to packageVersion being undefined.

** (Mix) Expected :version to be a valid Version, got: "" (see the Version module for more information)

elixir: "~> {{supportedElixirVersion}}",
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
Expand Down
25 changes: 3 additions & 22 deletions modules/openapi-generator/src/main/resources/elixir/model.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,15 @@
{{&description}}
"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
{{#vars}}{{#atom}}{{&baseName}}{{/atom}}{{^-last}},
{{#vars}}{{#atom}}{{name}}{{/atom}}{{^-last}},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the name and writing of the payload, which results in breaking the spec due to diverging of the property name.

see generated examples.

{{/-last}}{{/vars}}
]

@type t :: %__MODULE__{
{{#vars}}{{#atom}}{{&baseName}}{{/atom}} => {{{datatype}}}{{#isNullable}} | nil{{/isNullable}}{{^isNullable}}{{^required}} | nil{{/required}}{{/isNullable}}{{^-last}},
{{#vars}}{{#atom}}{{name}}{{/atom}} => {{{datatype}}}{{#isNullable}} | nil{{/isNullable}}{{^isNullable}}{{^required}} | nil{{/required}}{{/isNullable}}{{^-last}},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, changing the properiename (or cases) breaks the openapi spec

{{/-last}}{{/vars}}
}
end

defimpl Poison.Decoder, for: {{&moduleName}}.Model.{{&classname}} do
{{#hasComplexVars}}
import {{&moduleName}}.Deserializer
def decode(value, options) do
value
{{#vars}}
{{^isPrimitiveType}}
{{#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}}
{{/hasComplexVars}}
{{^hasComplexVars}}
def decode(value, _options) do
value
{{/hasComplexVars}}
end
end
{{/model}}{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ defmodule {{moduleName}}.RequestBuilder do
Tesla.Multipart.add_field(
multipart,
key,
Poison.encode!(value),
Jason.encode(value),
headers: [{:"Content-Type", "application/json"}]
)
end)
Expand Down Expand Up @@ -185,5 +185,5 @@ defmodule {{moduleName}}.RequestBuilder do

defp decode(%Tesla.Env{} = env, false), do: {:ok, env}

defp decode(%Tesla.Env{body: body}, struct), do: Poison.decode(body, as: struct)
defp decode(%Tesla.Env{body: body}, struct), do: struct(struct, Jason.decode!(body, keys: :atoms!))
end
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ defmodule OpenapiPetstore.Connection do

tesla_options = Application.get_env(:tesla, __MODULE__, [])
middleware = Keyword.get(tesla_options, :middleware, [])
json_engine = Keyword.get(tesla_options, :json, Poison)
json_engine = Keyword.get(tesla_options, :json, Jason)

user_agent =
Keyword.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ defmodule OpenapiPetstore.Deserializer do
@spec deserialize(struct(), :atom, :atom, struct(), keyword()) :: struct()
def deserialize(model, field, :list, mod, options) do
model
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: [struct(mod)]]))))
|> Map.update!(field, &struct(mod,(Jason.decode!(&1, Keyword.merge(options, keys: :atoms!)))))
end

def deserialize(model, field, :struct, mod, options) do
model
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: struct(mod)]))))
|> Map.update!(field, &struct(mod,(Jason.decode!(&1, Keyword.merge(options, keys: :atoms!)))))
end

def deserialize(model, field, :map, mod, options) do
Expand All @@ -28,7 +28,7 @@ defmodule OpenapiPetstore.Deserializer do
existing_value ->
Map.new(existing_value, fn
{key, val} ->
{key, Poison.Decode.decode(val, Keyword.merge(options, as: struct(mod)))}
{key, struct(mod, Jason.decode!(val, Keyword.merge(options, keys: :atoms!)))}
end)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule OpenapiPetstore.Model.FooGetDefaultResponse do

"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
:string
]
Expand All @@ -16,11 +16,3 @@ defmodule OpenapiPetstore.Model.FooGetDefaultResponse do
}
end

defimpl Poison.Decoder, for: OpenapiPetstore.Model.FooGetDefaultResponse do
import OpenapiPetstore.Deserializer
def decode(value, options) do
value
|> deserialize(:string, :struct, OpenapiPetstore.Model.Foo, options)
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@ defmodule OpenapiPetstore.Model.SpecialModelName do

"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
:"$special[property.name]"
:dollar_special_left_square_bracket_property_period_name_right_square_bracket
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$special[property.name] is the expected payload name

]

@type t :: %__MODULE__{
:"$special[property.name]" => integer() | nil
:dollar_special_left_square_bracket_property_period_name_right_square_bracket => integer() | nil
}
end

defimpl Poison.Decoder, for: OpenapiPetstore.Model.SpecialModelName do
def decode(value, _options) do
value
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule OpenapiPetstore.Model.AdditionalPropertiesClass do

"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
:map_property,
:map_of_map_property
Expand All @@ -18,9 +18,3 @@ defmodule OpenapiPetstore.Model.AdditionalPropertiesClass do
}
end

defimpl Poison.Decoder, for: OpenapiPetstore.Model.AdditionalPropertiesClass do
def decode(value, _options) do
value
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,15 @@ defmodule OpenapiPetstore.Model.AllOfWithSingleRef do

"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
:username,
:SingleRefType
:single_ref_type
]

@type t :: %__MODULE__{
:username => String.t | nil,
:SingleRefType => OpenapiPetstore.Model.SingleRefType.t | nil
:single_ref_type => OpenapiPetstore.Model.SingleRefType.t | nil
}
end

defimpl Poison.Decoder, for: OpenapiPetstore.Model.AllOfWithSingleRef do
import OpenapiPetstore.Deserializer
def decode(value, options) do
value
|> deserialize(:SingleRefType, :struct, OpenapiPetstore.Model.SingleRefType, options)
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,15 @@ defmodule OpenapiPetstore.Model.Animal do

"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
:className,
:class_name,
:color
]

@type t :: %__MODULE__{
:className => String.t,
:class_name => String.t,
:color => String.t | nil
}
end

defimpl Poison.Decoder, for: OpenapiPetstore.Model.Animal do
def decode(value, _options) do
value
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule OpenapiPetstore.Model.ApiResponse do

"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
:code,
:type,
Expand All @@ -20,9 +20,3 @@ defmodule OpenapiPetstore.Model.ApiResponse do
}
end

defimpl Poison.Decoder, for: OpenapiPetstore.Model.ApiResponse do
def decode(value, _options) do
value
end
end

Loading
Loading