Skip to content

Commit

Permalink
[Elixir] Deserialize responses based on status code (#2355)
Browse files Browse the repository at this point in the history
* Update Tesla dependency and replace Poison with Jason

* Use new Tesla method to set headers

* Fix jason dependency definition

* Use list for Headers instead of a map

* Rollback to Poison because Jason does not support 'as:' option to decode to arbitrary struct

* Use new return signature from Tesla 1.0 in decode function

* catch error when a struct is given as second parameter to RequestBuilder.decode

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

Co-Authored-By: yknx4 <[email protected]>

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

Co-Authored-By: yknx4 <[email protected]>

* Evaluate response based on status code

* Generate Petstore

* pin poison to ~> 3.0.0 since 4.0.0 does not work atm

* run ./bin/openapi3/elixir-petstore.sh
  • Loading branch information
mrmstn authored and wing328 committed Mar 20, 2019
1 parent 37c275b commit 49f3e9a
Show file tree
Hide file tree
Showing 70 changed files with 3,246 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.utils.ModelUtils;
Expand Down Expand Up @@ -53,7 +54,7 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
List<String> extraApplications = Arrays.asList(":logger");
List<String> deps = Arrays.asList(
"{:tesla, \"~> 1.0.0\"}",
"{:poison, \">= 1.0.0\"}"
"{:poison, \"~> 3.0.0\"}"
);

public ElixirClientCodegen() {
Expand Down Expand Up @@ -310,6 +311,11 @@ public CodegenModel fromModel(String name, Schema model) {
return new ExtendedCodegenModel(cm);
}

@Override
public CodegenResponse fromResponse(String responseCode, ApiResponse resp) {
return new ExtendedCodegenResponse(super.fromResponse(responseCode, resp));
}

// We should use String.join if we can use Java8
String join(CharSequence charSequence, Iterable<String> iterable) {
StringBuilder buf = new StringBuilder();
Expand Down Expand Up @@ -515,6 +521,91 @@ public String getSchemaType(Schema p) {
return toModelName(type);
}

class ExtendedCodegenResponse extends CodegenResponse {
public boolean isDefinedDefault;

public ExtendedCodegenResponse(CodegenResponse o) {
super();

this.headers.addAll(o.headers);
this.code = o.code;
this.message = o.message;
this.hasMore = o.hasMore;
this.examples = o.examples;
this.dataType = o.dataType;
this.baseType = o.baseType;
this.containerType = o.containerType;
this.hasHeaders = o.hasHeaders;
this.isString = o.isString;
this.isNumeric = o.isNumeric;
this.isInteger = o.isInteger;
this.isLong = o.isLong;
this.isNumber = o.isNumber;
this.isFloat = o.isFloat;
this.isDouble = o.isDouble;
this.isByteArray = o.isByteArray;
this.isBoolean = o.isBoolean;
this.isDate = o.isDate;
this.isDateTime = o.isDateTime;
this.isUuid = o.isUuid;
this.isEmail = o.isEmail;
this.isModel = o.isModel;
this.isFreeFormObject = o.isFreeFormObject;
this.isDefault = o.isDefault;
this.simpleType = o.simpleType;
this.primitiveType = o.primitiveType;
this.isMapContainer = o.isMapContainer;
this.isListContainer = o.isListContainer;
this.isBinary = o.isBinary;
this.isFile = o.isFile;
this.schema = o.schema;
this.jsonSchema = o.jsonSchema;
this.vendorExtensions = o.vendorExtensions;

this.isDefinedDefault = (this.code.equals("0") || this.code.equals("default"));
}

public String codeMappingKey(){
if(this.isDefinedDefault) {
return ":default";
}

if(code.matches("^\\d{3}$")){
return code;
}

LOGGER.warn("Unknown HTTP status code: " + this.code);
return "\"" + code + "\"";
}

public String decodedStruct() {
// Let Poison decode the entire response into a generic blob
if (isMapContainer) {
return "%{}";
}
// Primitive return type, don't even try to decode
if (baseType == null || (simpleType && primitiveType)) {
return "false";
} else if (isListContainer && languageSpecificPrimitives().contains(baseType)) {
return "[]";
}
StringBuilder sb = new StringBuilder();
if (isListContainer) {
sb.append("[");
}
sb.append("%");
sb.append(moduleName);
sb.append(".Model.");
sb.append(baseType);
sb.append("{}");
if (isListContainer) {
sb.append("]");
}
return sb.toString();
}

}

class ExtendedCodegenOperation extends CodegenOperation {
private List<String> pathTemplateNames = new ArrayList<String>();
private String replacedPathName;
Expand Down Expand Up @@ -688,32 +779,6 @@ private void buildTypespec(CodegenProperty property, StringBuilder sb) {
sb.append(".t");
}
}

public String decodedStruct() {
// Let Poison decode the entire response into a generic blob
if (isMapContainer) {
return "";
}
// Primitive return type, don't even try to decode
if (returnBaseType == null || (returnSimpleType && returnTypeIsPrimitive)) {
return "false";
} else if (isListContainer && languageSpecificPrimitives().contains(returnBaseType)) {
return "[]";
}
StringBuilder sb = new StringBuilder();
if (isListContainer) {
sb.append("[");
}
sb.append("%");
sb.append(moduleName);
sb.append(".Model.");
sb.append(returnBaseType);
sb.append("{}");
if (isListContainer) {
sb.append("]");
}
return sb.toString();
}
}

class ExtendedCodegenModel extends CodegenModel {
Expand Down
13 changes: 8 additions & 5 deletions modules/openapi-generator/src/main/resources/elixir/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ defmodule {{moduleName}}.Api.{{classname}} do

@doc """
{{#summary}}
{{summary}}
{{&summary}}
{{/summary}}
{{#notes}}
{{notes}}
{{&notes}}
{{/notes}}

## Parameters

- connection ({{moduleName}}.Connection): Connection to server
{{#requiredParams}}
- {{#underscored}}{{paramName}}{{/underscored}} ({{dataType}}): {{description}}
- {{#underscored}}{{paramName}}{{/underscored}} ({{dataType}}): {{&description}}
{{/requiredParams}}
- opts (KeywordList): [optional] Optional parameters
{{#optionalParams}}
- {{#underscored}}:{{paramName}}{{/underscored}} ({{dataType}}): {{description}}
- {{#underscored}}:{{paramName}}{{/underscored}} ({{dataType}}): {{&description}}
{{/optionalParams}}
## Returns

Expand Down Expand Up @@ -59,7 +59,10 @@ defmodule {{moduleName}}.Api.{{classname}} do
{{/optionalParams}}
|> Enum.into([])
|> (&Connection.request(connection, &1)).()
|> decode({{decodedStruct}})
|> evaluate_response({{#responses}}{{#-first}}[
{{/-first}}
{ {{& codeMappingKey}}, {{decodedStruct}}}{{#hasMore}},{{/hasMore}}
{{#-last}} ]{{/-last}}{{/responses}})
end
{{/operation}}
{{/operations}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,24 +107,34 @@ defmodule {{moduleName}}.RequestBuilder do

## Parameters

- arg1 ({:ok, Tesla.Env.t} | term) - The response object
- arg1 (Tesla.Env.t | term) - The response object
- arg2 (:false | struct | [struct]) - The shape of the struct to deserialize into

## Returns

{:ok, struct} on success
{:error, term} on failure
"""
@spec decode({:ok, Tesla.Env.t} | term()) :: {:ok, struct()} | {:error, Tesla.Env.t} | {:error, term()}
def decode({:ok, %Tesla.Env{status: 200, body: body}}), do: Poison.decode(body)
def decode(response), do: {:error, response}
def decode({:error, _} = error), do: error
def decode(response), do: {:error, response}

@spec decode({:ok, Tesla.Env.t} | term(), :false | struct() | [struct()]) :: {:ok, struct()} | {:error, Tesla.Env.t} | {:error, term()}
def decode({:ok, %Tesla.Env{status: 200}} = env, false), do: {:ok, env}
def decode({:ok, %Tesla.Env{status: 200, body: body}}, struct), do: Poison.decode(body, as: struct)
def decode({:ok, %Tesla.Env{} = response}, _struct), do: {:error, response}
def decode({:error, _} = error, _struct), do: error
def decode(response, _struct), do: {:error, response}
@spec decode(Tesla.Env.t() | term(), false | struct() | [struct()]) ::
{:ok, struct()} | {:ok, Tesla.Env.t()} | {:error, any}
def decode(%Tesla.Env{} = env, false), do: {:ok, env}
def decode(%Tesla.Env{body: body}, struct), do: Poison.decode(body, as: struct)

def evaluate_response({:ok, %Tesla.Env{} = env}, mapping) do
resolve_mapping(env, mapping)
end

def evaluate_response({:error, _} = error, _), do: error

def resolve_mapping(env, mapping, default \\ nil)

def resolve_mapping(%Tesla.Env{status: status} = env, [{mapping_status, struct} | _], _)
when status == mapping_status do
decode(env, struct)
end

def resolve_mapping(env, [{:default, struct} | tail], _), do: resolve_mapping(env, tail, struct)
def resolve_mapping(env, [_ | tail], struct), do: resolve_mapping(env, tail, struct)
def resolve_mapping(env, [], nil), do: {:error, env}
def resolve_mapping(env, [], struct), do: decode(env, struct)
end
2 changes: 1 addition & 1 deletion samples/client/petstore/elixir/.openapi-generator/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.4-SNAPSHOT
4.0.0-SNAPSHOT
8 changes: 4 additions & 4 deletions samples/client/petstore/elixir/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# OpenapiPetstore
# OpenAPIPetstore

This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \&quot; \\

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `openapi_petstore` to your list of dependencies in `mix.exs`:
by adding `open_api_petstore` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[{:openapi_petstore, "~> 0.1.0"}]
[{:open_api_petstore, "~> 0.1.0"}]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/openapi_petstore](https://hexdocs.pm/openapi_petstore).
be found at [https://hexdocs.pm/open_api_petstore](https://hexdocs.pm/open_api_petstore).
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
# https://openapi-generator.tech
# Do not edit the class manually.

defmodule OpenAPIPetstore.Api.AnotherFake do
@moduledoc """
API calls for all endpoints tagged `AnotherFake`.
"""

alias OpenAPIPetstore.Connection
import OpenAPIPetstore.RequestBuilder


@doc """
To test special tags
To test special tags and operation ID starting with number
## Parameters
- connection (OpenAPIPetstore.Connection): Connection to server
- client (Client): client model
- opts (KeywordList): [optional] Optional parameters
## Returns
{:ok, %OpenAPIPetstore.Model.Client{}} on success
{:error, info} on failure
"""
@spec call_123_test_special_tags(Tesla.Env.client, OpenAPIPetstore.Model.Client.t, keyword()) :: {:ok, OpenAPIPetstore.Model.Client.t} | {:error, Tesla.Env.t}
def call_123_test_special_tags(connection, client, _opts \\ []) do
%{}
|> method(:patch)
|> url("/another-fake/dummy")
|> add_param(:body, :body, client)
|> Enum.into([])
|> (&Connection.request(connection, &1)).()
|> evaluate_response([
{ 200, %OpenAPIPetstore.Model.Client{}}
])
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
# https://openapi-generator.tech
# Do not edit the class manually.

defmodule OpenAPIPetstore.Api.Default do
@moduledoc """
API calls for all endpoints tagged `Default`.
"""

alias OpenAPIPetstore.Connection
import OpenAPIPetstore.RequestBuilder


@doc """
## Parameters
- connection (OpenAPIPetstore.Connection): Connection to server
- opts (KeywordList): [optional] Optional parameters
## Returns
{:ok, %OpenAPIPetstore.Model.InlineResponseDefault{}} on success
{:error, info} on failure
"""
@spec foo_get(Tesla.Env.client, keyword()) :: {:ok, OpenAPIPetstore.Model.InlineResponseDefault.t} | {:error, Tesla.Env.t}
def foo_get(connection, _opts \\ []) do
%{}
|> method(:get)
|> url("/foo")
|> Enum.into([])
|> (&Connection.request(connection, &1)).()
|> evaluate_response([
{ :default, %OpenAPIPetstore.Model.InlineResponseDefault{}}
])
end
end
Loading

0 comments on commit 49f3e9a

Please sign in to comment.