Skip to content

Commit

Permalink
Elixir: Switch Poison to Jason (#16061)
Browse files Browse the repository at this point in the history
* Switch Poison to Jason

* generate-samples.sh

* Finalize Poison -> Jason switch

* parse date-time values to Elixir DateTime
* improve formatting in various places, so there's less changes by `mix
  format` later
* fix Java version in flake.nix

* Use List.delete/2 instead of Enum.reject/2 for performance reasons

* mix format test/*

* Install dialyxir and fix reported issues

* Fix RequestBuilder.decode/2 hardcoded module name

* Update docs

* Revert changes to API spec (HTTP -> HTTPS)

* Revert uneeded change to Elixir code generator

* Use HTTP in Elixir tests

HTTPS doesn't work for folks who setup petstore.swagger.io as described
in docs/faq-contributing.md.

---------

Co-authored-by: Wojciech Piekutowski <[email protected]>
  • Loading branch information
barttenbrinke and wpiekutowski committed Jul 20, 2023
1 parent ddc2b3e commit 4ece8e9
Show file tree
Hide file tree
Showing 71 changed files with 632 additions and 358 deletions.
1 change: 0 additions & 1 deletion docs/generators/elixir.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl
<li>AnyType</li>
<li>Atom</li>
<li>Boolean</li>
<li>DateTime</li>
<li>Decimal</li>
<li>Float</li>
<li>Integer</li>
Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
devShells.default = pkgs.mkShell
{
buildInputs = with pkgs;[
jdk8
jdk11
maven
];
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ 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\"}",
"{:ex_doc, \"~> 0.28\", only: :dev, runtime: false}"
"{:tesla, \"~> 1.7\"}",
"{:jason, \"~> 1.4\"}",
"{:ex_doc, \"~> 0.30\", only: :dev, runtime: false}",
"{:dialyxir, \"~> 1.3\", only: [:dev, :test], runtime: false}"
);

public ElixirClientCodegen() {
Expand Down Expand Up @@ -194,7 +195,6 @@ public ElixirClientCodegen() {
"AnyType",
"Tuple",
"PID",
"DateTime",
"map()", // This is a workaround, since the DefaultCodeGen uses our elixir TypeSpec datetype to evaluate the primitive
"any()"
)
Expand All @@ -210,7 +210,7 @@ public ElixirClientCodegen() {
typeMapping.put("string", "String");
typeMapping.put("byte", "Integer");
typeMapping.put("boolean", "Boolean");
typeMapping.put("Date", "DateTime");
typeMapping.put("Date", "Date");
typeMapping.put("DateTime", "DateTime");
typeMapping.put("file", "String");
typeMapping.put("map", "Map");
Expand Down Expand Up @@ -575,7 +575,12 @@ public String getTypeDeclaration(Schema p) {
} else if (ModelUtils.isBooleanSchema(p)) {
return "boolean()";
} else if (!StringUtils.isEmpty(p.get$ref())) {
return this.moduleName + ".Model." + super.getTypeDeclaration(p) + ".t";
switch (super.getTypeDeclaration(p)) {
case "String":
return "String.t";
default:
return this.moduleName + ".Model." + super.getTypeDeclaration(p) + ".t";
}
} else if (ModelUtils.isFileSchema(p)) {
return "String.t";
} else if (ModelUtils.isStringSchema(p)) {
Expand Down Expand Up @@ -662,28 +667,23 @@ public String codeMappingKey() {
}

public String decodedStruct() {
// Let Poison decode the entire response into a generic blob
// Let Jason decode the entire response into a generic blob
if (isMap) {
return "%{}";
}

// Primitive return type, don't even try to decode
if (baseType == null || (containerType == null && primitiveType)) {
return "false";
} else if (isArray && languageSpecificPrimitives().contains(baseType)) {
return "[]";
}

StringBuilder sb = new StringBuilder();
if (isArray) {
sb.append("[");
}
sb.append("%");
sb.append(moduleName);
sb.append(".Model.");
sb.append(baseType);
sb.append("{}");
if (isArray) {
sb.append("]");
}

return sb.toString();
}

Expand Down Expand Up @@ -768,6 +768,24 @@ public void setReplacedPathName(String replacedPathName) {
this.replacedPathName = replacedPathName;
}

private void translateBaseType(StringBuilder returnEntry, String baseType) {
switch (baseType) {
case "AnyType":
returnEntry.append("any()");
break;
case "Boolean":
returnEntry.append("boolean()");
break;
case "Float":
returnEntry.append("float()");
break;
default:
returnEntry.append(baseType);
returnEntry.append(".t");
break;
}
}

public String typespec() {
StringBuilder sb = new StringBuilder("@spec ");
sb.append(underscore(operationId));
Expand All @@ -793,12 +811,7 @@ public String typespec() {
returnEntry.append(".Model.");
}

if (exResponse.baseType.equals("AnyType")) {
returnEntry.append("any()");
}else {
returnEntry.append(exResponse.baseType);
returnEntry.append(".t");
}
translateBaseType(returnEntry, exResponse.baseType);
} else {
if (exResponse.containerType.equals("array") ||
exResponse.containerType.equals("set")) {
Expand All @@ -808,12 +821,8 @@ public String typespec() {
returnEntry.append(".Model.");
}

if (exResponse.baseType.equals("AnyType")) {
returnEntry.append("any())");
}else {
returnEntry.append(exResponse.baseType);
returnEntry.append(".t)");
}
translateBaseType(returnEntry, exResponse.baseType);
returnEntry.append(")");
} else if (exResponse.containerType.equals("map")) {
returnEntry.append("map()");
}
Expand Down
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 @@ -4,37 +4,81 @@ defmodule {{moduleName}}.Deserializer do
Helper functions for deserializing responses into models
"""

@jason_decode_opts [keys: :strings]

def jason_decode(json) do
Jason.decode(json, @jason_decode_opts)
end

def jason_decode(json, module) do
json
|> jason_decode()
|> case do
{:ok, decoded} -> {:ok, to_struct(decoded, module)}
{:error, _} = error -> error
end
end

@doc """
Update the provided model with a deserialization of a nested value
"""
@spec deserialize(struct(), :atom, :atom, struct(), keyword()) :: struct()
def deserialize(model, field, :list, mod, options) do
@spec deserialize(struct(), atom(), :date | :datetime | :list | :map | :struct, module()) ::
struct()
def deserialize(model, field, :list, module) do
model
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: [struct(mod)]]))))
|> Map.update!(field, fn
nil ->
nil

list ->
Enum.map(list, &to_struct(&1, module))
end)
end

def deserialize(model, field, :struct, mod, options) do
def deserialize(model, field, :struct, module) do
model
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: struct(mod)]))))
|> Map.update!(field, fn
nil ->
nil

value ->
to_struct(value, module)
end)
end

def deserialize(model, field, :map, mod, options) do
def deserialize(model, field, :map, module) do
maybe_transform_map = fn
nil ->
nil

existing_value ->
Map.new(existing_value, fn
{key, val} ->
{key, Poison.Decode.decode(val, Keyword.merge(options, as: struct(mod)))}
{key, value} ->
{key, to_struct(value, module)}
end)
end

Map.update!(model, field, maybe_transform_map)
end

def deserialize(model, field, :date, _, _options) do
def deserialize(model, field, :date, _) do
value = Map.get(model, field)

case is_binary(value) do
true ->
case Date.from_iso8601(value) do
{:ok, date} -> Map.put(model, field, date)
_ -> model
end

false ->
model
end
end

def deserialize(model, field, :datetime, _) do
value = Map.get(model, field)

case is_binary(value) do
true ->
case DateTime.from_iso8601(value) do
Expand All @@ -46,4 +90,23 @@ defmodule {{moduleName}}.Deserializer do
model
end
end

defp to_struct(map_or_list, module)
defp to_struct(nil, _), do: nil

defp to_struct(list, module) when is_list(list) and is_atom(module) do
Enum.map(list, &to_struct(&1, module))
end

defp to_struct(map, module) when is_map(map) and is_atom(module) do
model = struct(module)

model
|> Map.keys()
|> List.delete(:__struct__)
|> Enum.reduce(model, fn field, acc ->
Map.replace(acc, field, Map.get(map, Atom.to_string(field)))
end)
|> module.decode()
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ defmodule {{moduleName}}.Mixfile do
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
package: package(),
description: "{{appDescription}}",
description: """
{{appDescription}}
""",
deps: deps()
]
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{{&description}}
"""

@derive [Poison.Encoder]
@derive Jason.Encoder
defstruct [
{{#vars}}{{#atom}}{{&baseName}}{{/atom}}{{^-last}},
{{/-last}}{{/vars}}
Expand All @@ -14,22 +14,21 @@
{{#vars}}{{#atom}}{{&baseName}}{{/atom}} => {{{datatype}}}{{#isNullable}} | nil{{/isNullable}}{{^isNullable}}{{^required}} | nil{{/required}}{{/isNullable}}{{^-last}},
{{/-last}}{{/vars}}
}
end

defimpl Poison.Decoder, for: {{&moduleName}}.Model.{{&classname}} do
{{#hasComplexVars}}
import {{&moduleName}}.Deserializer
def decode(value, options) do
alias {{&moduleName}}.Deserializer

def decode(value) 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}} |> Deserializer.deserialize({{#atom}}{{&baseName}}{{/atom}}, {{#isArray}}:list, {{&moduleName}}.Model.{{{items.baseType}}}{{/isArray}}{{#isMap}}:map, {{&moduleName}}.Model.{{{items.baseType}}}{{/isMap}}{{#isDate}}:date, nil{{/isDate}}{{#isDateTime}}:datetime, nil{{/isDateTime}}{{^isDate}}{{^isDateTime}}{{^isMap}}{{^isArray}}:struct, {{moduleName}}.Model.{{baseType}}{{/isArray}}{{/isMap}}{{/isDateTime}}{{/isDate}})
{{/baseType}}
{{/isPrimitiveType}}
{{/vars}}
{{/hasComplexVars}}
{{^hasComplexVars}}
def decode(value, _options) do
def decode(value) do
value
{{/hasComplexVars}}
end
Expand Down
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 @@ -146,8 +146,8 @@ defmodule {{moduleName}}.RequestBuilder do
Map.put_new(request, :body, "")
end

@type status_code :: 100..599
@type response_mapping :: [{status_code, struct() | false}]
@type status_code :: :default | 100..599
@type response_mapping :: [{status_code, false | %{} | module()}]

@doc """
Evaluate the response from a Tesla request.
Expand Down Expand Up @@ -185,5 +185,11 @@ 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}, %{}) do
{{moduleName}}.Deserializer.jason_decode(body)
end

defp decode(%Tesla.Env{body: body}, module) do
{{moduleName}}.Deserializer.jason_decode(body, module)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ defmodule OpenapiPetstore.Api.AnotherFake do
connection
|> Connection.request(request)
|> evaluate_response([
{200, %OpenapiPetstore.Model.Client{}}
{200, OpenapiPetstore.Model.Client}
])
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ defmodule OpenapiPetstore.Api.Default do
connection
|> Connection.request(request)
|> evaluate_response([
{:default, %OpenapiPetstore.Model.FooGetDefaultResponse{}}
{:default, OpenapiPetstore.Model.FooGetDefaultResponse}
])
end
end
Loading

0 comments on commit 4ece8e9

Please sign in to comment.