Skip to content

Commit 4ece8e9

Browse files
Elixir: Switch Poison to Jason (OpenAPITools#16061)
* 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]>
1 parent ddc2b3e commit 4ece8e9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+632
-358
lines changed

docs/generators/elixir.md

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl
4848
<li>AnyType</li>
4949
<li>Atom</li>
5050
<li>Boolean</li>
51-
<li>DateTime</li>
5251
<li>Decimal</li>
5352
<li>Float</li>
5453
<li>Integer</li>

flake.nix

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
devShells.default = pkgs.mkShell
1414
{
1515
buildInputs = with pkgs;[
16-
jdk8
16+
jdk11
1717
maven
1818
];
1919
};

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElixirClientCodegen.java

+36-27
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ public class ElixirClientCodegen extends DefaultCodegen {
5959
String supportedElixirVersion = "1.10";
6060
List<String> extraApplications = Arrays.asList(":logger");
6161
List<String> deps = Arrays.asList(
62-
"{:tesla, \"~> 1.4\"}",
63-
"{:poison, \"~> 3.0\"}",
64-
"{:ex_doc, \"~> 0.28\", only: :dev, runtime: false}"
62+
"{:tesla, \"~> 1.7\"}",
63+
"{:jason, \"~> 1.4\"}",
64+
"{:ex_doc, \"~> 0.30\", only: :dev, runtime: false}",
65+
"{:dialyxir, \"~> 1.3\", only: [:dev, :test], runtime: false}"
6566
);
6667

6768
public ElixirClientCodegen() {
@@ -194,7 +195,6 @@ public ElixirClientCodegen() {
194195
"AnyType",
195196
"Tuple",
196197
"PID",
197-
"DateTime",
198198
"map()", // This is a workaround, since the DefaultCodeGen uses our elixir TypeSpec datetype to evaluate the primitive
199199
"any()"
200200
)
@@ -210,7 +210,7 @@ public ElixirClientCodegen() {
210210
typeMapping.put("string", "String");
211211
typeMapping.put("byte", "Integer");
212212
typeMapping.put("boolean", "Boolean");
213-
typeMapping.put("Date", "DateTime");
213+
typeMapping.put("Date", "Date");
214214
typeMapping.put("DateTime", "DateTime");
215215
typeMapping.put("file", "String");
216216
typeMapping.put("map", "Map");
@@ -575,7 +575,12 @@ public String getTypeDeclaration(Schema p) {
575575
} else if (ModelUtils.isBooleanSchema(p)) {
576576
return "boolean()";
577577
} else if (!StringUtils.isEmpty(p.get$ref())) {
578-
return this.moduleName + ".Model." + super.getTypeDeclaration(p) + ".t";
578+
switch (super.getTypeDeclaration(p)) {
579+
case "String":
580+
return "String.t";
581+
default:
582+
return this.moduleName + ".Model." + super.getTypeDeclaration(p) + ".t";
583+
}
579584
} else if (ModelUtils.isFileSchema(p)) {
580585
return "String.t";
581586
} else if (ModelUtils.isStringSchema(p)) {
@@ -662,28 +667,23 @@ public String codeMappingKey() {
662667
}
663668

664669
public String decodedStruct() {
665-
// Let Poison decode the entire response into a generic blob
670+
// Let Jason decode the entire response into a generic blob
666671
if (isMap) {
667672
return "%{}";
668673
}
674+
669675
// Primitive return type, don't even try to decode
670676
if (baseType == null || (containerType == null && primitiveType)) {
671677
return "false";
672678
} else if (isArray && languageSpecificPrimitives().contains(baseType)) {
673679
return "[]";
674680
}
681+
675682
StringBuilder sb = new StringBuilder();
676-
if (isArray) {
677-
sb.append("[");
678-
}
679-
sb.append("%");
680683
sb.append(moduleName);
681684
sb.append(".Model.");
682685
sb.append(baseType);
683-
sb.append("{}");
684-
if (isArray) {
685-
sb.append("]");
686-
}
686+
687687
return sb.toString();
688688
}
689689

@@ -768,6 +768,24 @@ public void setReplacedPathName(String replacedPathName) {
768768
this.replacedPathName = replacedPathName;
769769
}
770770

771+
private void translateBaseType(StringBuilder returnEntry, String baseType) {
772+
switch (baseType) {
773+
case "AnyType":
774+
returnEntry.append("any()");
775+
break;
776+
case "Boolean":
777+
returnEntry.append("boolean()");
778+
break;
779+
case "Float":
780+
returnEntry.append("float()");
781+
break;
782+
default:
783+
returnEntry.append(baseType);
784+
returnEntry.append(".t");
785+
break;
786+
}
787+
}
788+
771789
public String typespec() {
772790
StringBuilder sb = new StringBuilder("@spec ");
773791
sb.append(underscore(operationId));
@@ -793,12 +811,7 @@ public String typespec() {
793811
returnEntry.append(".Model.");
794812
}
795813

796-
if (exResponse.baseType.equals("AnyType")) {
797-
returnEntry.append("any()");
798-
}else {
799-
returnEntry.append(exResponse.baseType);
800-
returnEntry.append(".t");
801-
}
814+
translateBaseType(returnEntry, exResponse.baseType);
802815
} else {
803816
if (exResponse.containerType.equals("array") ||
804817
exResponse.containerType.equals("set")) {
@@ -808,12 +821,8 @@ public String typespec() {
808821
returnEntry.append(".Model.");
809822
}
810823

811-
if (exResponse.baseType.equals("AnyType")) {
812-
returnEntry.append("any())");
813-
}else {
814-
returnEntry.append(exResponse.baseType);
815-
returnEntry.append(".t)");
816-
}
824+
translateBaseType(returnEntry, exResponse.baseType);
825+
returnEntry.append(")");
817826
} else if (exResponse.containerType.equals("map")) {
818827
returnEntry.append("map()");
819828
}

modules/openapi-generator/src/main/resources/elixir/connection.ex.mustache

+1-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ defmodule {{moduleName}}.Connection do
216216

217217
tesla_options = Application.get_env(:tesla, __MODULE__, [])
218218
middleware = Keyword.get(tesla_options, :middleware, [])
219-
json_engine = Keyword.get(tesla_options, :json, Poison)
219+
json_engine = Keyword.get(tesla_options, :json, Jason)
220220

221221
user_agent =
222222
Keyword.get(

modules/openapi-generator/src/main/resources/elixir/deserializer.ex.mustache

+72-9
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,81 @@ defmodule {{moduleName}}.Deserializer do
44
Helper functions for deserializing responses into models
55
"""
66

7+
@jason_decode_opts [keys: :strings]
8+
9+
def jason_decode(json) do
10+
Jason.decode(json, @jason_decode_opts)
11+
end
12+
13+
def jason_decode(json, module) do
14+
json
15+
|> jason_decode()
16+
|> case do
17+
{:ok, decoded} -> {:ok, to_struct(decoded, module)}
18+
{:error, _} = error -> error
19+
end
20+
end
21+
722
@doc """
823
Update the provided model with a deserialization of a nested value
924
"""
10-
@spec deserialize(struct(), :atom, :atom, struct(), keyword()) :: struct()
11-
def deserialize(model, field, :list, mod, options) do
25+
@spec deserialize(struct(), atom(), :date | :datetime | :list | :map | :struct, module()) ::
26+
struct()
27+
def deserialize(model, field, :list, module) do
1228
model
13-
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: [struct(mod)]]))))
29+
|> Map.update!(field, fn
30+
nil ->
31+
nil
32+
33+
list ->
34+
Enum.map(list, &to_struct(&1, module))
35+
end)
1436
end
1537

16-
def deserialize(model, field, :struct, mod, options) do
38+
def deserialize(model, field, :struct, module) do
1739
model
18-
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: struct(mod)]))))
40+
|> Map.update!(field, fn
41+
nil ->
42+
nil
43+
44+
value ->
45+
to_struct(value, module)
46+
end)
1947
end
2048

21-
def deserialize(model, field, :map, mod, options) do
49+
def deserialize(model, field, :map, module) do
2250
maybe_transform_map = fn
2351
nil ->
2452
nil
2553

2654
existing_value ->
2755
Map.new(existing_value, fn
28-
{key, val} ->
29-
{key, Poison.Decode.decode(val, Keyword.merge(options, as: struct(mod)))}
56+
{key, value} ->
57+
{key, to_struct(value, module)}
3058
end)
3159
end
3260

3361
Map.update!(model, field, maybe_transform_map)
3462
end
3563

36-
def deserialize(model, field, :date, _, _options) do
64+
def deserialize(model, field, :date, _) do
65+
value = Map.get(model, field)
66+
67+
case is_binary(value) do
68+
true ->
69+
case Date.from_iso8601(value) do
70+
{:ok, date} -> Map.put(model, field, date)
71+
_ -> model
72+
end
73+
74+
false ->
75+
model
76+
end
77+
end
78+
79+
def deserialize(model, field, :datetime, _) do
3780
value = Map.get(model, field)
81+
3882
case is_binary(value) do
3983
true ->
4084
case DateTime.from_iso8601(value) do
@@ -46,4 +90,23 @@ defmodule {{moduleName}}.Deserializer do
4690
model
4791
end
4892
end
93+
94+
defp to_struct(map_or_list, module)
95+
defp to_struct(nil, _), do: nil
96+
97+
defp to_struct(list, module) when is_list(list) and is_atom(module) do
98+
Enum.map(list, &to_struct(&1, module))
99+
end
100+
101+
defp to_struct(map, module) when is_map(map) and is_atom(module) do
102+
model = struct(module)
103+
104+
model
105+
|> Map.keys()
106+
|> List.delete(:__struct__)
107+
|> Enum.reduce(model, fn field, acc ->
108+
Map.replace(acc, field, Map.get(map, Atom.to_string(field)))
109+
end)
110+
|> module.decode()
111+
end
49112
end

modules/openapi-generator/src/main/resources/elixir/mix.exs.mustache

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ defmodule {{moduleName}}.Mixfile do
99
build_embedded: Mix.env() == :prod,
1010
start_permanent: Mix.env() == :prod,
1111
package: package(),
12-
description: "{{appDescription}}",
12+
description: """
13+
{{appDescription}}
14+
""",
1315
deps: deps()
1416
]
1517
end

modules/openapi-generator/src/main/resources/elixir/model.mustache

+6-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{{&description}}
55
"""
66

7-
@derive [Poison.Encoder]
7+
@derive Jason.Encoder
88
defstruct [
99
{{#vars}}{{#atom}}{{&baseName}}{{/atom}}{{^-last}},
1010
{{/-last}}{{/vars}}
@@ -14,22 +14,21 @@
1414
{{#vars}}{{#atom}}{{&baseName}}{{/atom}} => {{{datatype}}}{{#isNullable}} | nil{{/isNullable}}{{^isNullable}}{{^required}} | nil{{/required}}{{/isNullable}}{{^-last}},
1515
{{/-last}}{{/vars}}
1616
}
17-
end
1817

19-
defimpl Poison.Decoder, for: {{&moduleName}}.Model.{{&classname}} do
2018
{{#hasComplexVars}}
21-
import {{&moduleName}}.Deserializer
22-
def decode(value, options) do
19+
alias {{&moduleName}}.Deserializer
20+
21+
def decode(value) do
2322
value
2423
{{#vars}}
2524
{{^isPrimitiveType}}
26-
{{#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)
25+
{{#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}})
2726
{{/baseType}}
2827
{{/isPrimitiveType}}
2928
{{/vars}}
3029
{{/hasComplexVars}}
3130
{{^hasComplexVars}}
32-
def decode(value, _options) do
31+
def decode(value) do
3332
value
3433
{{/hasComplexVars}}
3534
end

modules/openapi-generator/src/main/resources/elixir/request_builder.ex.mustache

+10-4
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ defmodule {{moduleName}}.RequestBuilder do
9494
Tesla.Multipart.add_field(
9595
multipart,
9696
key,
97-
Poison.encode!(value),
97+
Jason.encode!(value),
9898
headers: [{:"Content-Type", "application/json"}]
9999
)
100100
end)
@@ -146,8 +146,8 @@ defmodule {{moduleName}}.RequestBuilder do
146146
Map.put_new(request, :body, "")
147147
end
148148

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

152152
@doc """
153153
Evaluate the response from a Tesla request.
@@ -185,5 +185,11 @@ defmodule {{moduleName}}.RequestBuilder do
185185

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

188-
defp decode(%Tesla.Env{body: body}, struct), do: Poison.decode(body, as: struct)
188+
defp decode(%Tesla.Env{body: body}, %{}) do
189+
{{moduleName}}.Deserializer.jason_decode(body)
190+
end
191+
192+
defp decode(%Tesla.Env{body: body}, module) do
193+
{{moduleName}}.Deserializer.jason_decode(body, module)
194+
end
189195
end

samples/client/petstore/elixir/lib/openapi_petstore/api/another_fake.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ defmodule OpenapiPetstore.Api.AnotherFake do
3636
connection
3737
|> Connection.request(request)
3838
|> evaluate_response([
39-
{200, %OpenapiPetstore.Model.Client{}}
39+
{200, OpenapiPetstore.Model.Client}
4040
])
4141
end
4242
end

samples/client/petstore/elixir/lib/openapi_petstore/api/default.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ defmodule OpenapiPetstore.Api.Default do
3232
connection
3333
|> Connection.request(request)
3434
|> evaluate_response([
35-
{:default, %OpenapiPetstore.Model.FooGetDefaultResponse{}}
35+
{:default, OpenapiPetstore.Model.FooGetDefaultResponse}
3636
])
3737
end
3838
end

0 commit comments

Comments
 (0)