Skip to content

Commit 4b9ee1f

Browse files
chingor13wing328
authored andcommitted
[Elixir] Improve Elixir client (#6550)
* Fix dependencies and generate model classes * Better elixir client generation. Responses are parsed and serialized by Poison into the model structs. Use shared helper functions to generate the request. Extract client connection configuration from api calls. Elixir client can sanitize the operationId Correctly output the model variables. Fix typos Fix path replacement when there are multiple replacements Cannot separate globally shared parameters from operations Error handling for the tesla response update templates Can generate clients that compile Can make requests - parse optional params, build query Add oauth to connection. Fix connection directory Add basic auth helper for creating a connection Fix map types. Fix guard clauses for creaing connections Add licenceInfo template. Parse config for moduleName via standard invokerPackage option Can provide and inject a license header into all source files fix location of connection.ex Move shared code into reusable modules Elixir filenames should be underscored Fix visibility of helper functions Parse the packageName from config options Handle date and datetime fields with DateTime.from_iso8601 Fix indentation Update documentation, add typespecs Generate a standard elixir .gitignore typespec is calculated recursively in java Use the JSON middleware and using Poison.Decoder.decode on already parsed structs move decoded struct into java Fix handling of non-json responses Switch basic auth to use the provided Tesla.Middleware.BasicAuth Update README template to include the appDescription Update sample elixir client remove junk client models that don't belong with petstore Only implement Poison.Decoder protocol if needed Update samples with skipped Poison.Deocder impl * Handle multipart file uploads Handle building form params in the body Files are handled as strings for input * Requests with no defined return type will return the Tesla.Env response * Run the bin/elixir-petstore.sh
1 parent 3ac2b80 commit 4b9ee1f

File tree

80 files changed

+2514
-421
lines changed

Some content is hidden

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

80 files changed

+2514
-421
lines changed

modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ElixirClientCodegen.java

Lines changed: 322 additions & 15 deletions
Large diffs are not rendered by default.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
# {{#modulized}}{{appName}}{{/modulized}}
1+
# {{moduleName}}
22

3-
**TODO: Add description**
3+
{{appDescription}}
44

55
## Installation
66

77
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8-
by adding `{{#underscored}}{{appName}}{{/underscored}}` to your list of dependencies in `mix.exs`:
8+
by adding `{{#underscored}}{{packageName}}{{/underscored}}` to your list of dependencies in `mix.exs`:
99

1010
```elixir
1111
def deps do
12-
[{:{{#underscored}}{{appName}}{{/underscored}}, "~> 0.1.0"}]
12+
[{:{{#underscored}}{{packageName}}{{/underscored}}, "~> 0.1.0"}]
1313
end
1414
```
1515

1616
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
1717
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
18-
be found at [https://hexdocs.pm/{{#underscored}}{{appName}}{{/underscored}}](https://hexdocs.pm/{{#underscored}}{{appName}}{{/underscored}}).
18+
be found at [https://hexdocs.pm/{{#underscored}}{{packageName}}{{/underscored}}](https://hexdocs.pm/{{#underscored}}{{packageName}}{{/underscored}}).
Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,59 @@
1-
defmodule {{#modulized}}{{appName}}{{/modulized}}.Api.{{classname}} do
1+
{{>licenseInfo}}
2+
defmodule {{moduleName}}.Api.{{classname}} do
23
@moduledoc """
3-
Documentation for {{#modulized}}{{appName}}{{/modulized}}.Api.{{classname}}.
4+
API calls for all endpoints tagged `{{baseName}}`.
45
"""
56

6-
use Tesla
7+
alias {{moduleName}}.Connection
8+
import {{moduleName}}.RequestBuilder
79

8-
plug Tesla.Middleware.BaseUrl, "{{{basePath}}}"
9-
plug Tesla.Middleware.JSON
1010
{{#operations}}
11-
{{#operation}}
11+
{{#operation}}
1212

1313
@doc """
14+
{{#summary}}
1415
{{summary}}
15-
{{^notes.isEmpty}}
16-
16+
{{/summary}}
17+
{{#notes}}
1718
{{notes}}
18-
{{/notes.isEmpty}}
19-
"""
20-
def {{#underscored}}{{operationId}}{{/underscored}}({{#allParams}}{{^-first}}, {{/-first}}{{#underscored}}{{paramName}}{{/underscored}}{{/allParams}}) do
21-
method = [method: :{{#underscored}}{{httpMethod}}{{/underscored}}]
22-
url = [url: "{{replacedPathName}}"]
23-
query_params = [{{^queryParams.isEmpty}}query: [{{#queryParams}}{{^-first}}, {{/-first}}{:"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}}}{{/queryParams}}]{{/queryParams.isEmpty}}]
24-
header_params = [{{^headerParams.isEmpty}}header: [{{#headerParams}}{{^-first}}, {{/-first}}{:"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}}}{{/headerParams}}]{{/headerParams.isEmpty}}]
25-
body_params = [{{^bodyParams.isEmpty}}body: {{#bodyParams}}{{#underscored}}{{paramName}}{{/underscored}}{{/bodyParams}}{{/bodyParams.isEmpty}}]
26-
form_params = [{{^formParams.isEmpty}}body: Enum.map_join([{{#formParams}}{{^-first}}, {{/-first}}{:"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}}}{{/formParams}}], "&", &("#{elem(&1, 0)}=#{elem(&1, 1)}")){{/formParams.isEmpty}}]
27-
params = query_params ++ header_params ++ body_params ++ form_params
28-
opts = []
29-
options = method ++ url ++ params ++ opts
19+
{{/notes}}
20+
21+
## Parameters
3022

31-
request(options)
23+
- connection ({{moduleName}}.Connection): Connection to server
24+
{{#allParams}}{{#required}} - {{#underscored}}{{paramName}}{{/underscored}} ({{dataType}}): {{description}}
25+
{{/required}}{{/allParams}} - opts (KeywordList): [optional] Optional parameters
26+
{{#allParams}}{{^required}} - {{#underscored}}:{{paramName}}{{/underscored}} ({{dataType}}): {{description}}
27+
{{/required}}{{/allParams}}
28+
## Returns
29+
30+
{:ok, {{#isListContainer}}[%{{returnBaseType}}{}, ...]{{/isListContainer}}{{#isMapContainer}}%{}{{/isMapContainer}}{{^returnType}}%{}{{/returnType}}{{#returnSimpleType}}%{{#returnType}}{{#isMapContainer}}{{/isMapContainer}}{{moduleName}}.Model.{{{returnType}}}{{/returnType}}{}{{/returnSimpleType}}} on success
31+
{:error, info} on failure
32+
"""
33+
{{typespec}}
34+
def {{#underscored}}{{operationId}}{{/underscored}}(connection, {{#allParams}}{{#required}}{{#underscored}}{{paramName}}{{/underscored}}, {{/required}}{{/allParams}}{{^hasOptionalParams}}_{{/hasOptionalParams}}opts \\ []) do
35+
{{#hasOptionalParams}}
36+
optional_params = %{
37+
{{#allParams}}{{^required}}{{^isPathParam}}:"{{baseName}}" => {{#isBodyParam}}:body{{/isBodyParam}}{{#isFormParam}}:form{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}{{/isPathParam}}{{#hasMore}},
38+
{{/hasMore}}{{/required}}{{/allParams}}
39+
}
40+
{{/hasOptionalParams}}
41+
%{}
42+
|> method(:{{#underscored}}{{httpMethod}}{{/underscored}})
43+
|> url("{{replacedPathName}}")
44+
{{#allParams}}
45+
{{#required}}
46+
{{^isPathParam}} |> add_param({{#isBodyParam}}:body{{/isBodyParam}}{{#isFormParam}}{{#isMultipart}}{{#isFile}}:file{{/isFile}}{{^isFile}}:form{{/isFile}}{{/isMultipart}}{{^isMultipart}}:form{{/isMultipart}}{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}, :"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}})
47+
{{/isPathParam}}
48+
{{/required}}
49+
{{/allParams}}
50+
{{#hasOptionalParams}}
51+
|> add_optional_params(optional_params, opts)
52+
{{/hasOptionalParams}}
53+
|> Enum.into([])
54+
|> (&Connection.request(connection, &1)).()
55+
|> decode({{decodedStruct}})
3256
end
33-
{{/operation}}
57+
{{/operation}}
3458
{{/operations}}
3559
end
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{{>licenseInfo}}
2+
defmodule {{moduleName}}.Connection do
3+
@moduledoc """
4+
Handle Tesla connections for {{moduleName}}.
5+
"""
6+
7+
use Tesla
8+
9+
# Add any middleware here (authentication)
10+
plug Tesla.Middleware.BaseUrl, "{{{basePath}}}"
11+
plug Tesla.Middleware.Headers, %{"User-Agent" => "Elixir"}
12+
plug Tesla.Middleware.EncodeJson
13+
14+
{{#hasAuthMethods}}
15+
{{#authMethods}}
16+
{{#isOAuth}}
17+
@scopes [
18+
{{#scopes}}
19+
"{{scope}}"{{#hasMore}},{{/hasMore}} {{#description}}# {{description}}{{/description}}
20+
{{/scopes}}
21+
]
22+
23+
@doc """
24+
Configure a client connection using a provided OAuth2 token as a Bearer token
25+
26+
## Parameters
27+
28+
- token (String): Bearer token
29+
30+
## Returns
31+
32+
Tesla.Env.client
33+
"""
34+
@spec new(String.t) :: Tesla.Env.client
35+
def new(token) when is_binary(token) do
36+
Tesla.build_client([
37+
{Tesla.Middleware.Headers, %{"Authorization" => "Bearer #{token}"}}
38+
])
39+
end
40+
41+
@doc """
42+
Configure a client connection using a function which yields a Bearer token.
43+
44+
## Parameters
45+
46+
- token_fetcher (function arity of 1): Callback which provides an OAuth2 token
47+
given a list of scopes
48+
49+
## Returns
50+
51+
Tesla.Env.client
52+
"""
53+
@spec new(((list(String.t)) -> String.t)) :: Tesla.Env.client
54+
def new(token_fetcher) when is_function(token_fetcher) do
55+
token_fetcher.(@scopes)
56+
|> new
57+
end
58+
{{/isOAuth}}
59+
{{#isBasic}}
60+
@doc """
61+
Configure an client connection using Basic authentication.
62+
63+
## Parameters
64+
65+
- username (String): Username used for authentication
66+
- password (String): Password used for authentication
67+
68+
# Returns
69+
70+
Tesla.Env.client
71+
"""
72+
@spec new(String.t, String.t) :: Tesla.Env.client
73+
def new(username, password) do
74+
Tesla.build_client([
75+
{Tesla.Middleware.BasicAuth, %{username: username, password: password}}
76+
])
77+
end
78+
{{/isBasic}}
79+
{{/authMethods}}
80+
{{/hasAuthMethods}}
81+
@doc """
82+
Configure an authless client connection
83+
84+
# Returns
85+
86+
Tesla.Env.client
87+
"""
88+
@spec new() :: Tesla.Env.client
89+
def new do
90+
Tesla.build_client([])
91+
end
92+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{{>licenseInfo}}
2+
defmodule {{moduleName}}.Deserializer do
3+
@moduledoc """
4+
Helper functions for deserializing responses into models
5+
"""
6+
7+
@doc """
8+
Update the provided model with a deserialization of a nested value
9+
"""
10+
@spec deserialize(struct(), :atom, :atom, struct(), keyword()) :: struct()
11+
def deserialize(model, field, :list, mod, options) do
12+
model
13+
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: [struct(mod)]]))))
14+
end
15+
def deserialize(model, field, :struct, mod, options) do
16+
model
17+
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: struct(mod)]))))
18+
end
19+
def deserialize(model, field, :map, mod, options) do
20+
model
21+
|> Map.update!(field, &(Map.new(&1, fn {key, val} -> {key, Poison.Decode.decode(val, Keyword.merge(options, [as: struct(mod)]))} end)))
22+
end
23+
def deserialize(model, field, :date, _, _options) do
24+
case DateTime.from_iso8601(Map.get(model, field)) do
25+
{:ok, datetime} ->
26+
Map.put(model, field, datetime)
27+
_ ->
28+
model
29+
end
30+
end
31+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{#licenseHeader}}{{licenseHeader}}
2+
3+
{{/licenseHeader}}
4+
# NOTE: This class is auto generated by the swagger code generator program.
5+
# https://github.com/swagger-api/swagger-codegen.git
6+
# Do not edit the class manually.

modules/swagger-codegen/src/main/resources/elixir/mix.exs.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
defmodule {{#modulized}}{{appName}}{{/modulized}}.Mixfile do
1+
defmodule {{moduleName}}.Mixfile do
22
use Mix.Project
33

44
def project do
5-
[app: :{{#underscored}}{{appName}}{{/underscored}},
5+
[app: :{{#underscored}}{{packageName}}{{/underscored}},
66
version: "0.1.0",
77
elixir: "~> {{supportedElixirVersion}}",
88
build_embedded: Mix.env == :prod,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{{>licenseInfo}}
2+
{{#models}}{{#model}}defmodule {{moduleName}}.Model.{{classname}} do
3+
@moduledoc """
4+
{{description}}
5+
"""
6+
7+
@derive [Poison.Encoder]
8+
defstruct [
9+
{{#vars}}:"{{baseName}}"{{#hasMore}},
10+
{{/hasMore}}{{/vars}}
11+
]
12+
end
13+
14+
defimpl Poison.Decoder, for: {{moduleName}}.Model.{{classname}} do
15+
{{#hasComplexVars}}
16+
import {{moduleName}}.Deserializer
17+
def decode(value, options) do
18+
value
19+
{{#vars}}
20+
{{^isPrimitiveType}}
21+
{{#datatype}}|> deserialize(:"{{baseName}}", {{#isListContainer}}:list, {{moduleName}}.Model.{{items.datatype}}{{/isListContainer}}{{#isMapContainer}}:map, {{moduleName}}.Model.{{items.datatype}}{{/isMapContainer}}{{#isDate}}:date, nil{{/isDate}}{{#isDateTime}}:date, nil{{/isDateTime}}{{^isDate}}{{^isDateTime}}{{^isMapContainer}}{{^isListContainer}}:struct, {{moduleName}}.Model.{{datatype}}{{/isListContainer}}{{/isMapContainer}}{{/isDateTime}}{{/isDate}}, options)
22+
{{/datatype}}
23+
{{/isPrimitiveType}}
24+
{{/vars}}
25+
{{/hasComplexVars}}
26+
{{^hasComplexVars}}
27+
def decode(value, _options) do
28+
value
29+
{{/hasComplexVars}}
30+
end
31+
end
32+
{{/model}}{{/models}}

0 commit comments

Comments
 (0)