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

Elixir: Switch Poison to Jason #16061

Merged
merged 11 commits into from
Jul 20, 2023
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
Copy link
Contributor

Choose a reason for hiding this comment

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

@wing328 the run-in-docker.sh also uses jdk11, so this change looks good to me, any veto against this change in the common area?

Copy link
Contributor

Choose a reason for hiding this comment

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

Exactly. Recent maven versions require JDK >= 11.

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
Loading