Skip to content

Commit

Permalink
[erlang-client] Erlang request utils (#7257)
Browse files Browse the repository at this point in the history
* [erlang-client] fix body param from being included path and base path to remove host

* [erlang-client] move request logic out of api functions to utils module

* [erlang-client] add support for passing http client configuration to requests

* [erlang-client] update auth handling

* [erlang-client] remove underscore2, replacing with original underscore + replaceAll
  • Loading branch information
tsloughter authored and wing328 committed Jan 22, 2018
1 parent 01a076c commit c731185
Show file tree
Hide file tree
Showing 22 changed files with 701 additions and 570 deletions.
2 changes: 1 addition & 1 deletion bin/erlang-petstore-client.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ fi

# if you've executed sbt assembly previously it will use that instead.
export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="$@ generate -t modules/swagger-codegen/src/main/resources/erlang-client -i modules/swagger-codegen/src/test/resources/2_0/petstore.yaml -l erlang-client -o samples/client/petstore/erlang-client"
ags="$@ generate -t modules/swagger-codegen/src/main/resources/erlang-client -DpackageName=petstore -i modules/swagger-codegen/src/test/resources/2_0/petstore.yaml -l erlang-client -o samples/client/petstore/erlang-client"

java $JAVA_OPTS -jar $executable $ags
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public void execute(Template.Fragment fragment, Writer writer) throws IOExceptio

supportingFiles.add(new SupportingFile("rebar.config.mustache","", "rebar.config"));
supportingFiles.add(new SupportingFile("app.src.mustache", "", "src" + File.separator + this.packageName + ".app.src"));
supportingFiles.add(new SupportingFile("utils.mustache", "", "src" + File.separator + this.packageName + "_utils.erl"));
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
}

Expand Down Expand Up @@ -221,23 +222,24 @@ public String toParamName(String name) {

@Override
public String toModelName(String name) {
return this.packageName + "_" + underscore(name.replaceAll("-", "_"));
return this.packageName + "_" + underscore(name.replaceAll("-", "_").replaceAll("\\.", "_"));
}

@Override
public String toApiName(String name) {
return this.packageName + "_" + underscore(name.replaceAll("-", "_"));
return this.packageName + "_" + underscore(name.replaceAll("-", "_").replaceAll("\\.", "_"));
}

@Override
public String toModelFilename(String name) {
return this.packageName + "_" + underscore(name);
return this.packageName + "_" + underscore(name.replaceAll("\\.", "_"));
}

@Override
public String toApiFilename(String name) {
// replace - with _ e.g. created-at => created_at
name = name.replaceAll("-", "_"); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
// FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
name = name.replaceAll("-", "_").replaceAll("\\.", "_");

// e.g. PetApi.erl => pet_api.erl
return this.packageName + "_" + underscore(name) + "_api";
Expand All @@ -247,11 +249,11 @@ public String toApiFilename(String name) {
public String toOperationId(String operationId) {
// method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) {
LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + underscore(sanitizeName("call_" + operationId)));
LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + underscore(sanitizeName("call_" + operationId)).replaceAll("\\.", "_"));
operationId = "call_" + operationId;
}

return underscore(operationId);
return underscore(operationId.replaceAll("\\.", "_"));
}

@Override
Expand Down Expand Up @@ -379,8 +381,8 @@ public ExtendedCodegenOperation(CodegenOperation o) {
this.produces = o.produces;
this.bodyParam = o.bodyParam;
this.allParams = o.allParams;
this.arityRequired = Integer.toString(lengthRequired(o.allParams));
this.arityOptional = Integer.toString(lengthRequired(o.allParams)+1);
this.arityRequired = Integer.toString(lengthRequired(o.allParams)+1);
this.arityOptional = Integer.toString(lengthRequired(o.allParams)+2);
this.bodyParams = o.bodyParams;
this.pathParams = o.pathParams;
this.queryParams = o.queryParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,32 @@
-export([{{#operations}}{{#operation}}{{^-first}},
{{/-first}}{{operationId}}/{{arityRequired}}, {{operationId}}/{{arityOptional}}{{/operation}}{{/operations}}]).

-define(BASE_URL, <<"{{{basePath}}}">>).
-define(BASE_URL, "{{{basePathWithoutHost}}}").

{{#operations}}
{{#operation}}
%% @doc {{{summary}}}
{{^notes.isEmpty}}
%% {{{notes}}}
{{/notes.isEmpty}}
-spec {{operationId}}({{#allParams}}{{#required}}{{^-first}}, {{/-first}}{{{dataType}}}{{/required}}{{/allParams}}{{^bodyParams.isEmpty}}{{#bodyParams}}, term(){{/bodyParams}}{{/bodyParams.isEmpty}}) -> {{#returnType}}{ok, list(), {{{returnType}}}} | {error, string()}{{/returnType}}{{^returnType}}ok | {error, integer()}{{/returnType}}.
{{operationId}}({{#allParams}}{{#required}}{{^-first}}, {{/-first}}{{paramName}}{{/required}}{{/allParams}}{{^bodyParams.isEmpty}}{{#bodyParams}}, {{paramName}}{{/bodyParams}}{{/bodyParams.isEmpty}}) ->
{{operationId}}({{#allParams}}{{#required}}{{paramName}}, {{/required}}{{/allParams}}{{^bodyParams.isEmpty}}{{#bodyParams}}{{paramName}}, {{/bodyParams}}{{/bodyParams.isEmpty}}#{}).
-spec {{operationId}}(ctx:ctx(){{#allParams}}{{#required}}, {{{dataType}}}{{/required}}{{/allParams}}) -> {ok, {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}[]{{/returnType}}, {{packageName}}_utils:response_info()} | {ok, hackney:client_ref()} | {error, term(), {{packageName}}_utils:response_info()}.
{{operationId}}(Ctx{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}}) ->
{{operationId}}(Ctx{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}}, #{}).

-spec {{operationId}}(ctx:ctx(){{#allParams}}{{#required}}, {{{dataType}}}{{/required}}{{/allParams}}, maps:map()) -> {ok, {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}[]{{/returnType}}, {{packageName}}_utils:response_info()} | {ok, hackney:client_ref()} | {error, term(), {{packageName}}_utils:response_info()}.
{{operationId}}(Ctx{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}}, Optional) ->
_OptionalParams = maps:get(params, Optional, #{}),
Cfg = maps:get(cfg, Optional, application:get_env(kuberl, config, #{})),

-spec {{operationId}}({{#allParams}}{{#required}}{{{dataType}}}, {{/required}}{{/allParams}}{{^bodyParams.isEmpty}}{{#bodyParams}}term(), {{/bodyParams}}{{/bodyParams.isEmpty}}maps:map()) -> {{#returnType}}{ok, list(), {{{returnType}}}} | {error, string()}{{/returnType}}{{^returnType}}ok | {error, integer()}{{/returnType}}.
{{operationId}}({{#allParams}}{{#required}}{{paramName}}, {{/required}}{{/allParams}}{{^bodyParams.isEmpty}}{{#bodyParams}}{{paramName}}{{/bodyParams}}, {{/bodyParams.isEmpty}}_Optional) ->
Method = {{httpMethod}},
Path = ["{{{replacedPathName}}}"],
QS = {{#queryParams.isEmpty}}[]{{/queryParams.isEmpty}}{{^queryParams.isEmpty}}lists:flatten([{{#queryParams}}{{#required}}{{^-first}}, {{/-first}}{{#qsEncode}}{{this}}{{/qsEncode}}{{/required}}{{/queryParams}}])++[{X, maps:get(X, _Optional)} || X <- [{{#queryParams}}{{^required}}{{^-first}}, {{/-first}}'{{baseName}}'{{/required}}{{/queryParams}}], maps:is_key(X, _Optional)]{{/queryParams.isEmpty}},
Headers = {{#headerParams.isEmpty}}[]{{/headerParams.isEmpty}}{{^headerParams.isEmpty}}[{{#headerParams}}{{#required}}{{^-first}}, {{/-first}}{<<"{{baseName}}">>, {{paramName}}}{{/required}}{{/headerParams}}]++[{X, maps:get(X, _Optional)} || X <- [{{#headerParams}}{{^required}}{{^-first}}, {{/-first}}'{{baseName}}'{{/required}}{{/headerParams}}], maps:is_key(X, _Optional)]{{/headerParams.isEmpty}},
Body1 = {{^formParams.isEmpty}}{form, [{{#formParams}}{{#required}}{{^-first}}, {{/-first}}{<<"{{baseName}}">>, {{paramName}}}{{/required}}{{/formParams}}]++[{X, maps:get(X, _Optional)} || X <- [{{#formParams}}{{^required}}{{^-first}}, {{/-first}}'{{baseName}}'{{/required}}{{/formParams}}], maps:is_key(X, _Optional)]}{{/formParams.isEmpty}}{{#formParams.isEmpty}}{{#bodyParams.isEmpty}}[]{{/bodyParams.isEmpty}}{{^bodyParams.isEmpty}}{{#bodyParams}}{{paramName}}{{/bodyParams}}{{/bodyParams.isEmpty}}{{/formParams.isEmpty}},
Opts = [],
Url = hackney_url:make_url(?BASE_URL, Path, QS),

case hackney:request(Method, Url, Headers, Body1, Opts) of
{{#returnType}}
{{#responses}}
{{#isDefault}}
{ok, {{code}}, RespHeaders, ClientRef} ->
{ok, ResponseBody} = hackney:body(ClientRef),
{ok, RespHeaders, jsx:decode(ResponseBody, [return_maps])}{{#hasMore}}; {{/hasMore}}
{{/isDefault}}
{{^isDefault}}
{ok, {{code}}, _RespHeaders, _ClientRef} ->
{error, "{{message}}"}{{#hasMore}}; {{/hasMore}}
{{/isDefault}}
{{/responses}}
{{/returnType}}
{{^returnType}}
{ok, 200, _RespHeaders, _ClientRef} ->
ok;
{ok, Status, _RespHeaders, _ClientRef} ->
{error, Status}
{{/returnType}}
end.
QS = {{#queryParams.isEmpty}}[]{{/queryParams.isEmpty}}{{^queryParams.isEmpty}}lists:flatten([{{#queryParams}}{{#required}}{{^-first}}, {{/-first}}{{#qsEncode}}{{this}}{{/qsEncode}}{{/required}}{{/queryParams}}])++{{packageName}}_utils:optional_params([{{#queryParams}}{{^required}}{{^-first}}, {{/-first}}'{{baseName}}'{{/required}}{{/queryParams}}], _OptionalParams){{/queryParams.isEmpty}},
Headers = {{#headerParams.isEmpty}}[]{{/headerParams.isEmpty}}{{^headerParams.isEmpty}}[{{#headerParams}}{{#required}}{{^-first}}, {{/-first}}{<<"{{baseName}}">>, {{paramName}}}{{/required}}{{/headerParams}}]++{{packageName}}_utils:optional_params([{{#headerParams}}{{^required}}{{^-first}}, {{/-first}}'{{baseName}}'{{/required}}{{/headerParams}}], _OptionalParams){{/headerParams.isEmpty}},
Body1 = {{^formParams.isEmpty}}{form, [{{#formParams}}{{#required}}{{^-first}}, {{/-first}}{<<"{{baseName}}">>, {{paramName}}}{{/required}}{{/formParams}}]++{{packageName}}_utils:optional_params([{{#formParams}}{{^required}}{{^-first}}, {{/-first}}'{{baseName}}'{{/required}}{{/formParams}}], _OptionalParams)}{{/formParams.isEmpty}}{{#formParams.isEmpty}}{{#bodyParams.isEmpty}}[]{{/bodyParams.isEmpty}}{{^bodyParams.isEmpty}}{{#bodyParams}}{{paramName}}{{/bodyParams}}{{/bodyParams.isEmpty}}{{/formParams.isEmpty}},
ContentTypeHeader = {{packageName}}_utils:select_header_content_type([{{#consumes}}{{^-first}}, {{/-first}}<<"{{mediaType}}">>{{/consumes}}]),
Opts = maps:get(hackney_opts, Optional, []),

{{packageName}}_utils:request(Ctx, Method, [?BASE_URL, Path], QS, ContentTypeHeader++Headers, Body1, Opts, Cfg).

{{/operation}}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{application, {{packageName}},
[{description, {{#appDescription}}"{{appDescription}}"{{/appDescription}}{{^appDescription}}"Swagger client library"{{/appDescription}}},
{vsn, "{{apiVersion}}"},
{vsn, "{{#apiVersion}}{{apiVersion}}{{/apiVersion}}{{^apiVersion}}0.1.0{{/apiVersion}}"},
{registered, []},
{applications,
[kernel,
stdlib,
ssl,
hackney
hackney,
ctx
]},
{env, []},
{env, [{host, "{{#host}}{{{host}}}{{/host}}{{^host}}localhost{{/host}}"}]},
{modules, []},

{maintainers, []},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{erl_opts, [debug_info, warnings_as_errors, warn_untyped_record]}.

{deps, [jsx, hackney]}.
{deps, [ctx, jsx, hackney]}.

{shell, [{apps, [{{packageName}}]}]}.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
-module({{packageName}}_utils).

-export([request/8,
select_header_content_type/1,
optional_params/2]).

-type response_info() :: #{status := integer(),
headers := list()}.
-export_type([response_info/0]).

request(_Ctx, Method, Path, QS, Headers, Body, Opts, Cfg) ->
{Headers1, QS1} = update_params_with_auth(Cfg, Headers, QS),
Host = maps:get(host, Cfg, "localhost:8001"),
Url = hackney_url:make_url(Host, Path, QS1),

ConfigHackneyOpts = maps:get(hackney_opts, Cfg, []),
Body1 = case lists:keyfind(<<"Content-Type">>, 1, Headers1) of
{_, <<"application/json", _/binary>>} ->
jsx:encode(Body);
_ ->
Body
end,

case hackney:request(Method, Url, Headers1, Body1, Opts++ConfigHackneyOpts) of
{ok, ClientRef} ->
%% return value if Opts includes `async`
{ok, ClientRef};
{ok, Status, RespHeaders, ClientRef} when Status >= 200,
Status =< 299 ->
{ok, ResponseBody} = hackney:body(ClientRef),
Resp = decode_response(RespHeaders, ResponseBody),
{ok, Resp, #{status => Status,
headers => RespHeaders}};
{ok, Status, RespHeaders, ClientRef} when Status >= 300 ->
{ok, ResponseBody} = hackney:body(ClientRef),
Resp = decode_response(RespHeaders, ResponseBody),
{error, Resp, #{status => Status,
headers => RespHeaders}}
end.

decode_response(Headers, Body) ->
case lists:keyfind(<<"Content-Type">>, 1, Headers) of
{_, <<"application/json", _/binary>>} ->
jsx:decode(Body, [return_maps, {labels, atom}]);
%% TODO: yml, protobuf, user defined function
_ ->
Body
end.

optional_params([], _Params) -> [];
optional_params(Keys, Params) ->
[{Key, maps:get(Key, Params)} || Key <- Keys, maps:is_key(Key, Params)].

select_header_content_type([]) ->
[];
select_header_content_type(ContentTypes) ->
case lists:member(<<"application/json">>, ContentTypes) orelse lists:member(<<"*/*">>, ContentTypes) of
true ->
[{<<"Content-Type">>, <<"application/json">>}];
false ->
[{<<"Content-Type">>, hd(ContentTypes)}]
end.

auth_with_prefix(Cfg, Key, Token) ->
Prefixes = maps:get(api_key_prefix, Cfg, #{}),
case maps:get(Key, Prefixes, undefined) of
undefined ->
Token;
Prefix ->
<<Prefix/binary, " ", Token/binary>>
end.

update_params_with_auth(Cfg, Headers, QS) ->
AuthSettings = maps:get(auth, Cfg, #{}),
Auths = #{ {{#authMethods}}'{{name}}' =>
#{type => '{{type}}',
key => <<"{{keyParamName}}">>,
in => {{#isKeyInHeader}}header{{/isKeyInHeader}}{{#isKeyInQuery}}query{{/isKeyInQuery}}}{{#hasMore}}, {{/hasMore}}{{/authMethods}}},

maps:fold(fun(AuthName, #{type := _Type,
in := In,
key := Key}, {HeadersAcc, QSAcc}) ->
case maps:get(AuthName, AuthSettings, undefined) of
undefined ->
{HeadersAcc, QSAcc};
Value ->
case In of
header ->
{[{Key, auth_with_prefix(Cfg, Key, Value)} | HeadersAcc], QSAcc};
query ->
{HeadersAcc, [{Key, auth_with_prefix(Cfg, Key, Value)} | QSAcc]}
end
end
end, {Headers, QS}, Auths).
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.3.0-SNAPSHOT
2.3.0
4 changes: 3 additions & 1 deletion samples/client/petstore/erlang-client/rebar.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{erl_opts, [debug_info, warnings_as_errors, warn_untyped_record]}.

{deps, [jsx, hackney]}.
{deps, [ctx, jsx, hackney]}.

{shell, [{apps, [petstore]}]}.
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{application, swagger,
{application, petstore,
[{description, "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key &#x60;special-key&#x60; to test the authorization filters."},
{vsn, ""},
{vsn, "0.1.0"},
{registered, []},
{applications,
[kernel,
stdlib,
ssl,
hackney
hackney,
ctx
]},
{env, []},
{env, [{host, "petstore.swagger.io"}]},
{modules, []},

{maintainers, []},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
-module(swagger_api_response).
-module(petstore_api_response).

-export([encode/1]).

-export_type([swagger_api_response/0]).
-export_type([petstore_api_response/0]).

-type swagger_api_response() ::
-type petstore_api_response() ::
#{ 'code' => integer(),
'type' => binary(),
'message' => binary()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
-module(swagger_category).
-module(petstore_category).

-export([encode/1]).

-export_type([swagger_category/0]).
-export_type([petstore_category/0]).

-type swagger_category() ::
-type petstore_category() ::
#{ 'id' => integer(),
'name' => binary()
}.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
-module(swagger_order).
-module(petstore_order).

-export([encode/1]).

-export_type([swagger_order/0]).
-export_type([petstore_order/0]).

-type swagger_order() ::
-type petstore_order() ::
#{ 'id' => integer(),
'petId' => integer(),
'quantity' => integer(),
'shipDate' => swagger_date_time:swagger_date_time(),
'shipDate' => petstore_date_time:petstore_date_time(),
'status' => binary(),
'complete' => boolean()
}.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
-module(swagger_pet).
-module(petstore_pet).

-export([encode/1]).

-export_type([swagger_pet/0]).
-export_type([petstore_pet/0]).

-type swagger_pet() ::
-type petstore_pet() ::
#{ 'id' => integer(),
'category' => swagger_category:swagger_category(),
'category' => petstore_category:petstore_category(),
'name' := binary(),
'photoUrls' := list(),
'tags' => list(),
Expand Down
Loading

0 comments on commit c731185

Please sign in to comment.