Erlang server (#19722)

* Add documentation to server and handlers

* Respond to at least one path-server

* Let url servers override base path

* Handle reading bigger bodies in cowboy

* Improve json error handling

* Regenerate erlang-server handlers

* Rework API module for performance and completion

* Regenerate erlang-server handlers
This commit is contained in:
Nelson Vides
2024-10-01 08:51:12 +02:00
committed by GitHub
parent 2551689ec6
commit 7e1ebe6404
19 changed files with 777 additions and 439 deletions

View File

@@ -46,7 +46,7 @@ accept_callback(Class, OperationID, Req, Context) ->
-export_type([operation_id/0]).
-dialyzer({nowarn_function, [to_binary/1, to_list/1, validate_response_body/4]}).
-dialyzer({nowarn_function, [to_binary/1, validate_response_body/4]}).
-type rule() ::
{type, binary} |
@@ -158,24 +158,31 @@ for the `OperationID` operation.
{{/allParams}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}request_param_info(OperationID, Name) ->
error({unknown_param, OperationID, Name}).
-spec populate_request_params(
operation_id(), [request_param()], cowboy_req:req(), jesse_state:state(), map()) ->
{ok, map(), cowboy_req:req()} | {error, _, cowboy_req:req()}.
populate_request_params(_, [], Req, _, Model) ->
{ok, Model, Req};
populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) ->
case populate_request_param(OperationID, FieldParams, Req0, ValidatorState) of
{ok, K, V, Req} ->
populate_request_params(OperationID, T, Req, ValidatorState, maps:put(K, V, Model));
populate_request_params(OperationID, [ReqParamName | T], Req0, ValidatorState, Model0) ->
case populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) of
{ok, V, Req} ->
Model = maps:put(ReqParamName, V, Model0),
populate_request_params(OperationID, T, Req, ValidatorState, Model);
Error ->
Error
end.
populate_request_param(OperationID, Name, Req0, ValidatorState) ->
#{rules := Rules, source := Source} = request_param_info(OperationID, Name),
case get_value(Source, Name, Req0) of
-spec populate_request_param(
operation_id(), request_param(), cowboy_req:req(), jesse_state:state()) ->
{ok, term(), cowboy_req:req()} | {error, term(), cowboy_req:req()}.
populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) ->
#{rules := Rules, source := Source} = request_param_info(OperationID, ReqParamName),
case get_value(Source, ReqParamName, Req0) of
{error, Reason, Req} ->
{error, Reason, Req};
{Value, Req} ->
case prepare_param(Rules, Name, Value, ValidatorState) of
{ok, Result} -> {ok, Name, Result, Req};
case prepare_param(Rules, ReqParamName, Value, ValidatorState) of
{ok, Result} -> {ok, Result, Req};
{error, Reason} ->
{error, Reason, Req}
end
@@ -185,112 +192,111 @@ populate_request_param(OperationID, Name, Req0, ValidatorState) ->
validate_response_body(list, ReturnBaseType, Body, ValidatorState) ->
[
validate(schema, ReturnBaseType, Item, ValidatorState)
validate(schema, Item, ReturnBaseType, ValidatorState)
|| Item <- Body];
validate_response_body(_, ReturnBaseType, Body, ValidatorState) ->
validate(schema, ReturnBaseType, Body, ValidatorState).
validate(schema, Body, ReturnBaseType, ValidatorState).
validate(Rule = required, Name, Value, _ValidatorState) ->
case Value of
undefined -> validation_error(Rule, Name);
_ -> ok
-spec validate(rule(), term(), request_param(), jesse_state:state()) ->
ok | {ok, term()}.
validate(required, undefined, ReqParamName, _) ->
validation_error(required, ReqParamName, undefined);
validate(required, _Value, _ReqParamName, _) ->
ok;
validate(not_required, _Value, _ReqParamName, _) ->
ok;
validate(_, undefined, _ReqParamName, _) ->
ok;
validate({type, boolean}, Value, _ReqParamName, _) when is_boolean(Value) ->
{ok, Value};
validate({type, integer}, Value, _ReqParamName, _) when is_integer(Value) ->
ok;
validate({type, float}, Value, _ReqParamName, _) when is_float(Value) ->
ok;
validate({type, binary}, Value, _ReqParamName, _) when is_binary(Value) ->
ok;
validate(Rule = {type, binary}, Value, ReqParamName, _) ->
validation_error(Rule, ReqParamName, Value);
validate(Rule = {type, boolean}, Value, ReqParamName, _) ->
case binary_to_lower(Value) of
<<"true">> -> {ok, true};
<<"false">> -> {ok, false};
_ -> validation_error(Rule, ReqParamName, Value)
end;
validate(not_required, _Name, _Value, _ValidatorState) ->
ok;
validate(_, _Name, undefined, _ValidatorState) ->
ok;
validate(Rule = {type, integer}, Name, Value, _ValidatorState) ->
validate(Rule = {type, integer}, Value, ReqParamName, _) ->
try
{ok, to_int(Value)}
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, float}, Name, Value, _ValidatorState) ->
validate(Rule = {type, float}, Value, ReqParamName, _) ->
try
{ok, to_float(Value)}
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, binary}, Name, Value, _ValidatorState) ->
validate(Rule = {type, date}, Value, ReqParamName, _) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(_Rule = {type, boolean}, _Name, Value, _ValidatorState) when is_boolean(Value) ->
{ok, Value};
validate(Rule = {type, boolean}, Name, Value, _ValidatorState) ->
V = binary_to_lower(Value),
try
case binary_to_existing_atom(V, utf8) of
B when is_boolean(B) -> {ok, B};
_ -> validation_error(Rule, Name)
end
catch
error:badarg ->
validation_error(Rule, Name)
end;
validate(Rule = {type, date}, Name, Value, _ValidatorState) ->
validate(Rule = {type, datetime}, Value, ReqParamName, _) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, datetime}, Name, Value, _ValidatorState) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
end;
validate(Rule = {enum, Values}, Name, Value, _ValidatorState) ->
validate(Rule = {enum, Values}, Value, ReqParamName, _) ->
try
FormattedValue = erlang:binary_to_existing_atom(Value, utf8),
case lists:member(FormattedValue, Values) of
true -> {ok, FormattedValue};
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {max, Max}, Name, Value, _ValidatorState) ->
validate(Rule = {max, Max}, Value, ReqParamName, _) ->
case Value =< Max of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {exclusive_max, ExclusiveMax}, Name, Value, _ValidatorState) ->
validate(Rule = {exclusive_max, ExclusiveMax}, Value, ReqParamName, _) ->
case Value > ExclusiveMax of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {min, Min}, Name, Value, _ValidatorState) ->
validate(Rule = {min, Min}, Value, ReqParamName, _) ->
case Value >= Min of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {exclusive_min, ExclusiveMin}, Name, Value, _ValidatorState) ->
validate(Rule = {exclusive_min, ExclusiveMin}, Value, ReqParamName, _) ->
case Value =< ExclusiveMin of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {max_length, MaxLength}, Name, Value, _ValidatorState) ->
validate(Rule = {max_length, MaxLength}, Value, ReqParamName, _) ->
case size(Value) =< MaxLength of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {min_length, MinLength}, Name, Value, _ValidatorState) ->
validate(Rule = {min_length, MinLength}, Value, ReqParamName, _) ->
case size(Value) >= MinLength of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) ->
validate(Rule = {pattern, Pattern}, Value, ReqParamName, _) ->
{ok, MP} = re:compile(Pattern),
case re:run(Value, MP) of
{match, _} -> ok;
_ -> validation_error(Rule, Name)
_ -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = schema, Name, Value, ValidatorState) ->
Definition = list_to_binary("#/components/schemas/" ++ to_list(Name)),
validate(Rule = schema, Value, ReqParamName, ValidatorState) ->
Definition = iolist_to_binary(["#/components/schemas/", atom_to_binary(ReqParamName)]),
try
_ = validate_with_schema(Value, Definition, ValidatorState),
ok
@@ -300,7 +306,7 @@ validate(Rule = schema, Name, Value, ValidatorState) ->
type => schema_invalid,
error => Error
},
validation_error(Rule, Name, Info);
validation_error(Rule, ReqParamName, Value, Info);
throw:[{data_invalid, Schema, Error, _, Path} | _] ->
Info = #{
type => data_invalid,
@@ -308,25 +314,25 @@ validate(Rule = schema, Name, Value, ValidatorState) ->
schema => Schema,
path => Path
},
validation_error(Rule, Name, Info)
validation_error(Rule, ReqParamName, Value, Info)
end;
validate(Rule, Name, _Value, _ValidatorState) ->
?LOG_INFO(#{what => "Cannot validate rule", name => Name, rule => Rule}),
validate(Rule, _Value, ReqParamName, _) ->
?LOG_INFO(#{what => "Cannot validate rule", name => ReqParamName, rule => Rule}),
error({unknown_validation_rule, Rule}).
-spec validation_error(Rule :: any(), Name :: any()) -> no_return().
validation_error(ViolatedRule, Name) ->
validation_error(ViolatedRule, Name, #{}).
-spec validation_error(rule(), request_param(), term()) -> no_return().
validation_error(ViolatedRule, Name, Value) ->
validation_error(ViolatedRule, Name, Value, #{}).
-spec validation_error(Rule :: any(), Name :: any(), Info :: #{_ := _}) -> no_return().
validation_error(ViolatedRule, Name, Info) ->
throw({wrong_param, Name, ViolatedRule, Info}).
-spec validation_error(rule(), request_param(), term(), Info :: #{_ := _}) -> no_return().
validation_error(ViolatedRule, Name, Value, Info) ->
throw({wrong_param, Name, Value, ViolatedRule, Info}).
-spec get_value(body | qs_val | header | binding, Name :: any(), Req0 :: cowboy_req:req()) ->
{Value :: any(), Req :: cowboy_req:req()} |
{error, Reason :: any(), Req :: cowboy_req:req()}.
-spec get_value(body | qs_val | header | binding, request_param(), cowboy_req:req()) ->
{any(), cowboy_req:req()} |
{error, any(), cowboy_req:req()}.
get_value(body, _Name, Req0) ->
{ok, Body, Req} = cowboy_req:read_body(Req0),
{ok, Body, Req} = read_entire_body(Req0),
case prepare_body(Body) of
{error, Reason} ->
{error, Reason, Req};
@@ -342,17 +348,30 @@ get_value(header, Name, Req) ->
Value = maps:get(to_header(Name), Headers, undefined),
{Value, Req};
get_value(binding, Name, Req) ->
Value = cowboy_req:binding(to_binding(Name), Req),
Value = cowboy_req:binding(Name, Req),
{Value, Req}.
-spec read_entire_body(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
read_entire_body(Req) ->
read_entire_body(Req, []).
-spec read_entire_body(cowboy_req:req(), iodata()) -> {ok, binary(), cowboy_req:req()}.
read_entire_body(Request, Acc) -> % {
case cowboy_req:read_body(Request) of
{ok, Data, NewRequest} ->
{ok, iolist_to_binary(lists:reverse([Data | Acc])), NewRequest};
{more, Data, NewRequest} ->
read_entire_body(NewRequest, [Data | Acc])
end.
prepare_body(<<>>) ->
<<>>;
prepare_body(Body) ->
try
json:decode(Body)
catch
error:_ ->
{error, {invalid_body, not_json, Body}}
error:Error ->
{error, {invalid_json, Body, Error}}
end.
validate_with_schema(Body, Definition, ValidatorState) ->
@@ -362,18 +381,17 @@ validate_with_schema(Body, Definition, ValidatorState) ->
ValidatorState
).
prepare_param(Rules, Name, Value, ValidatorState) ->
-spec prepare_param([rule()], request_param(), term(), jesse_state:state()) ->
{ok, term()} | {error, Reason :: any()}.
prepare_param(Rules, ReqParamName, Value, ValidatorState) ->
Fun = fun(Rule, Acc) ->
case validate(Rule, Acc, ReqParamName, ValidatorState) of
ok -> Acc;
{ok, Prepared} -> Prepared
end
end,
try
Result = lists:foldl(
fun(Rule, Acc) ->
case validate(Rule, Name, Acc, ValidatorState) of
ok -> Acc;
{ok, Prepared} -> Prepared
end
end,
Value,
Rules
),
Result = lists:foldl(Fun, Value, Rules),
{ok, Result}
catch
throw:Reason ->
@@ -387,40 +405,28 @@ to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8);
to_binary(V) when is_integer(V) -> integer_to_binary(V);
to_binary(V) when is_float(V) -> float_to_binary(V).
-spec to_list(iodata() | atom() | number()) -> binary().
to_list(V) when is_list(V) -> V;
to_list(V) when is_binary(V) -> binary_to_list(V);
to_list(V) when is_atom(V) -> atom_to_list(V);
to_list(V) when is_integer(V) -> integer_to_list(V);
to_list(V) when is_float(V) -> float_to_list(V).
-spec to_float(binary() | list()) -> integer().
to_float(Data) when is_binary(Data) ->
binary_to_float(Data);
to_float(Data) when is_list(Data) ->
list_to_float(Data).
-spec to_float(iodata()) -> float().
to_float(V) ->
binary_to_float(iolist_to_binary([V])).
-spec to_int(integer() | binary() | list()) -> integer().
to_int(Data) when is_integer(Data) ->
Data;
-spec to_int(binary() | list()) -> integer().
to_int(Data) when is_binary(Data) ->
binary_to_integer(Data);
to_int(Data) when is_list(Data) ->
list_to_integer(Data).
-spec to_header(iodata() | atom() | number()) -> binary().
-spec to_header(request_param()) -> binary().
to_header(Name) ->
to_binary(string:lowercase(to_binary(Name))).
to_binary(string:lowercase(atom_to_binary(Name, utf8))).
binary_to_lower(V) when is_binary(V) ->
string:lowercase(V).
-spec to_qs(iodata() | atom() | number()) -> binary().
-spec to_qs(request_param()) -> binary().
to_qs(Name) ->
to_binary(Name).
-spec to_binding(iodata() | atom() | number()) -> atom().
to_binding(Name) ->
Prepared = to_binary(Name),
binary_to_existing_atom(Prepared, utf8).
atom_to_binary(Name, utf8).
-spec get_opt(any(), []) -> any().
get_opt(Key, Opts) ->

View File

@@ -1,5 +1,11 @@
%% basic handler
-module({{classname}}).
-moduledoc """
Exposes the following operation IDs:
{{#operations}}{{#operation}}
- `{{httpMethod}}` to `{{path}}`, OperationId: `{{operationId}}`:
{{summary}}.
{{/operation}}{{/operations}}
""".
-behaviour(cowboy_rest).

View File

@@ -19,23 +19,38 @@ get_paths(LogicHandler) ->
group_paths() ->
maps:fold(
fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) ->
case maps:find(Path, Acc) of
fun(OperationID, #{servers := Servers, base_path := BasePath, path := Path,
method := Method, handler := Handler}, Acc) ->
FullPaths = build_full_paths(Servers, BasePath, Path),
merge_paths(FullPaths, OperationID, Method, Handler, Acc)
end, #{}, get_operations()).
build_full_paths([], BasePath, Path) ->
[lists:append([BasePath, Path])];
build_full_paths(Servers, _BasePath, Path) ->
[lists:append([Server, Path]) || Server <- Servers ].
merge_paths(FullPaths, OperationID, Method, Handler, Acc) ->
lists:foldl(
fun(Path, Acc0) ->
case maps:find(Path, Acc0) of
{ok, PathInfo0 = #{operations := Operations0}} ->
Operations = Operations0#{Method => OperationID},
PathInfo = PathInfo0#{operations => Operations},
Acc#{Path => PathInfo};
Acc0#{Path => PathInfo};
error ->
Operations = #{Method => OperationID},
PathInfo = #{handler => Handler, operations => Operations},
Acc#{Path => PathInfo}
Acc0#{Path => PathInfo}
end
end, #{}, get_operations()).
end, Acc, FullPaths).
get_operations() ->
#{ {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}
'{{operationId}}' => #{
path => "{{{basePathWithoutHost}}}{{{path}}}",
'{{operationId}}' => #{
servers => [{{#servers}}"{{{url}}}"{{^-last}},{{/-last}}{{/servers}}],
base_path => "{{{basePathWithoutHost}}}",
path => "{{{path}}}",
method => <<"{{httpMethod}}">>,
handler => '{{classname}}'
}{{^-last}},{{/-last}}{{/operation}}{{^-last}},{{/-last}}{{/operations}}{{/apis}}{{/apiInfo}}

View File

@@ -1,4 +1,9 @@
-module({{packageName}}_server).
{{#appDescription}}
-moduledoc """
{{{appDescription}}}
""".
{{/appDescription}}
-define(DEFAULT_LOGIC_HANDLER, {{packageName}}_logic_handler).

View File

@@ -46,7 +46,7 @@ accept_callback(Class, OperationID, Req, Context) ->
-export_type([operation_id/0]).
-dialyzer({nowarn_function, [to_binary/1, to_list/1, validate_response_body/4]}).
-dialyzer({nowarn_function, [to_binary/1, validate_response_body/4]}).
-type rule() ::
{type, binary} |
@@ -637,24 +637,31 @@ request_param_info('TestQueryStyleFormExplodeTrueObjectAllOf', 'query_object') -
request_param_info(OperationID, Name) ->
error({unknown_param, OperationID, Name}).
-spec populate_request_params(
operation_id(), [request_param()], cowboy_req:req(), jesse_state:state(), map()) ->
{ok, map(), cowboy_req:req()} | {error, _, cowboy_req:req()}.
populate_request_params(_, [], Req, _, Model) ->
{ok, Model, Req};
populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) ->
case populate_request_param(OperationID, FieldParams, Req0, ValidatorState) of
{ok, K, V, Req} ->
populate_request_params(OperationID, T, Req, ValidatorState, maps:put(K, V, Model));
populate_request_params(OperationID, [ReqParamName | T], Req0, ValidatorState, Model0) ->
case populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) of
{ok, V, Req} ->
Model = maps:put(ReqParamName, V, Model0),
populate_request_params(OperationID, T, Req, ValidatorState, Model);
Error ->
Error
end.
populate_request_param(OperationID, Name, Req0, ValidatorState) ->
#{rules := Rules, source := Source} = request_param_info(OperationID, Name),
case get_value(Source, Name, Req0) of
-spec populate_request_param(
operation_id(), request_param(), cowboy_req:req(), jesse_state:state()) ->
{ok, term(), cowboy_req:req()} | {error, term(), cowboy_req:req()}.
populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) ->
#{rules := Rules, source := Source} = request_param_info(OperationID, ReqParamName),
case get_value(Source, ReqParamName, Req0) of
{error, Reason, Req} ->
{error, Reason, Req};
{Value, Req} ->
case prepare_param(Rules, Name, Value, ValidatorState) of
{ok, Result} -> {ok, Name, Result, Req};
case prepare_param(Rules, ReqParamName, Value, ValidatorState) of
{ok, Result} -> {ok, Result, Req};
{error, Reason} ->
{error, Reason, Req}
end
@@ -664,112 +671,111 @@ populate_request_param(OperationID, Name, Req0, ValidatorState) ->
validate_response_body(list, ReturnBaseType, Body, ValidatorState) ->
[
validate(schema, ReturnBaseType, Item, ValidatorState)
validate(schema, Item, ReturnBaseType, ValidatorState)
|| Item <- Body];
validate_response_body(_, ReturnBaseType, Body, ValidatorState) ->
validate(schema, ReturnBaseType, Body, ValidatorState).
validate(schema, Body, ReturnBaseType, ValidatorState).
validate(Rule = required, Name, Value, _ValidatorState) ->
case Value of
undefined -> validation_error(Rule, Name);
_ -> ok
-spec validate(rule(), term(), request_param(), jesse_state:state()) ->
ok | {ok, term()}.
validate(required, undefined, ReqParamName, _) ->
validation_error(required, ReqParamName, undefined);
validate(required, _Value, _ReqParamName, _) ->
ok;
validate(not_required, _Value, _ReqParamName, _) ->
ok;
validate(_, undefined, _ReqParamName, _) ->
ok;
validate({type, boolean}, Value, _ReqParamName, _) when is_boolean(Value) ->
{ok, Value};
validate({type, integer}, Value, _ReqParamName, _) when is_integer(Value) ->
ok;
validate({type, float}, Value, _ReqParamName, _) when is_float(Value) ->
ok;
validate({type, binary}, Value, _ReqParamName, _) when is_binary(Value) ->
ok;
validate(Rule = {type, binary}, Value, ReqParamName, _) ->
validation_error(Rule, ReqParamName, Value);
validate(Rule = {type, boolean}, Value, ReqParamName, _) ->
case binary_to_lower(Value) of
<<"true">> -> {ok, true};
<<"false">> -> {ok, false};
_ -> validation_error(Rule, ReqParamName, Value)
end;
validate(not_required, _Name, _Value, _ValidatorState) ->
ok;
validate(_, _Name, undefined, _ValidatorState) ->
ok;
validate(Rule = {type, integer}, Name, Value, _ValidatorState) ->
validate(Rule = {type, integer}, Value, ReqParamName, _) ->
try
{ok, to_int(Value)}
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, float}, Name, Value, _ValidatorState) ->
validate(Rule = {type, float}, Value, ReqParamName, _) ->
try
{ok, to_float(Value)}
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, binary}, Name, Value, _ValidatorState) ->
validate(Rule = {type, date}, Value, ReqParamName, _) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(_Rule = {type, boolean}, _Name, Value, _ValidatorState) when is_boolean(Value) ->
{ok, Value};
validate(Rule = {type, boolean}, Name, Value, _ValidatorState) ->
V = binary_to_lower(Value),
try
case binary_to_existing_atom(V, utf8) of
B when is_boolean(B) -> {ok, B};
_ -> validation_error(Rule, Name)
end
catch
error:badarg ->
validation_error(Rule, Name)
end;
validate(Rule = {type, date}, Name, Value, _ValidatorState) ->
validate(Rule = {type, datetime}, Value, ReqParamName, _) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, datetime}, Name, Value, _ValidatorState) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
end;
validate(Rule = {enum, Values}, Name, Value, _ValidatorState) ->
validate(Rule = {enum, Values}, Value, ReqParamName, _) ->
try
FormattedValue = erlang:binary_to_existing_atom(Value, utf8),
case lists:member(FormattedValue, Values) of
true -> {ok, FormattedValue};
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {max, Max}, Name, Value, _ValidatorState) ->
validate(Rule = {max, Max}, Value, ReqParamName, _) ->
case Value =< Max of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {exclusive_max, ExclusiveMax}, Name, Value, _ValidatorState) ->
validate(Rule = {exclusive_max, ExclusiveMax}, Value, ReqParamName, _) ->
case Value > ExclusiveMax of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {min, Min}, Name, Value, _ValidatorState) ->
validate(Rule = {min, Min}, Value, ReqParamName, _) ->
case Value >= Min of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {exclusive_min, ExclusiveMin}, Name, Value, _ValidatorState) ->
validate(Rule = {exclusive_min, ExclusiveMin}, Value, ReqParamName, _) ->
case Value =< ExclusiveMin of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {max_length, MaxLength}, Name, Value, _ValidatorState) ->
validate(Rule = {max_length, MaxLength}, Value, ReqParamName, _) ->
case size(Value) =< MaxLength of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {min_length, MinLength}, Name, Value, _ValidatorState) ->
validate(Rule = {min_length, MinLength}, Value, ReqParamName, _) ->
case size(Value) >= MinLength of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) ->
validate(Rule = {pattern, Pattern}, Value, ReqParamName, _) ->
{ok, MP} = re:compile(Pattern),
case re:run(Value, MP) of
{match, _} -> ok;
_ -> validation_error(Rule, Name)
_ -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = schema, Name, Value, ValidatorState) ->
Definition = list_to_binary("#/components/schemas/" ++ to_list(Name)),
validate(Rule = schema, Value, ReqParamName, ValidatorState) ->
Definition = iolist_to_binary(["#/components/schemas/", atom_to_binary(ReqParamName)]),
try
_ = validate_with_schema(Value, Definition, ValidatorState),
ok
@@ -779,7 +785,7 @@ validate(Rule = schema, Name, Value, ValidatorState) ->
type => schema_invalid,
error => Error
},
validation_error(Rule, Name, Info);
validation_error(Rule, ReqParamName, Value, Info);
throw:[{data_invalid, Schema, Error, _, Path} | _] ->
Info = #{
type => data_invalid,
@@ -787,25 +793,25 @@ validate(Rule = schema, Name, Value, ValidatorState) ->
schema => Schema,
path => Path
},
validation_error(Rule, Name, Info)
validation_error(Rule, ReqParamName, Value, Info)
end;
validate(Rule, Name, _Value, _ValidatorState) ->
?LOG_INFO(#{what => "Cannot validate rule", name => Name, rule => Rule}),
validate(Rule, _Value, ReqParamName, _) ->
?LOG_INFO(#{what => "Cannot validate rule", name => ReqParamName, rule => Rule}),
error({unknown_validation_rule, Rule}).
-spec validation_error(Rule :: any(), Name :: any()) -> no_return().
validation_error(ViolatedRule, Name) ->
validation_error(ViolatedRule, Name, #{}).
-spec validation_error(rule(), request_param(), term()) -> no_return().
validation_error(ViolatedRule, Name, Value) ->
validation_error(ViolatedRule, Name, Value, #{}).
-spec validation_error(Rule :: any(), Name :: any(), Info :: #{_ := _}) -> no_return().
validation_error(ViolatedRule, Name, Info) ->
throw({wrong_param, Name, ViolatedRule, Info}).
-spec validation_error(rule(), request_param(), term(), Info :: #{_ := _}) -> no_return().
validation_error(ViolatedRule, Name, Value, Info) ->
throw({wrong_param, Name, Value, ViolatedRule, Info}).
-spec get_value(body | qs_val | header | binding, Name :: any(), Req0 :: cowboy_req:req()) ->
{Value :: any(), Req :: cowboy_req:req()} |
{error, Reason :: any(), Req :: cowboy_req:req()}.
-spec get_value(body | qs_val | header | binding, request_param(), cowboy_req:req()) ->
{any(), cowboy_req:req()} |
{error, any(), cowboy_req:req()}.
get_value(body, _Name, Req0) ->
{ok, Body, Req} = cowboy_req:read_body(Req0),
{ok, Body, Req} = read_entire_body(Req0),
case prepare_body(Body) of
{error, Reason} ->
{error, Reason, Req};
@@ -821,17 +827,30 @@ get_value(header, Name, Req) ->
Value = maps:get(to_header(Name), Headers, undefined),
{Value, Req};
get_value(binding, Name, Req) ->
Value = cowboy_req:binding(to_binding(Name), Req),
Value = cowboy_req:binding(Name, Req),
{Value, Req}.
-spec read_entire_body(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
read_entire_body(Req) ->
read_entire_body(Req, []).
-spec read_entire_body(cowboy_req:req(), iodata()) -> {ok, binary(), cowboy_req:req()}.
read_entire_body(Request, Acc) -> % {
case cowboy_req:read_body(Request) of
{ok, Data, NewRequest} ->
{ok, iolist_to_binary(lists:reverse([Data | Acc])), NewRequest};
{more, Data, NewRequest} ->
read_entire_body(NewRequest, [Data | Acc])
end.
prepare_body(<<>>) ->
<<>>;
prepare_body(Body) ->
try
json:decode(Body)
catch
error:_ ->
{error, {invalid_body, not_json, Body}}
error:Error ->
{error, {invalid_json, Body, Error}}
end.
validate_with_schema(Body, Definition, ValidatorState) ->
@@ -841,18 +860,17 @@ validate_with_schema(Body, Definition, ValidatorState) ->
ValidatorState
).
prepare_param(Rules, Name, Value, ValidatorState) ->
-spec prepare_param([rule()], request_param(), term(), jesse_state:state()) ->
{ok, term()} | {error, Reason :: any()}.
prepare_param(Rules, ReqParamName, Value, ValidatorState) ->
Fun = fun(Rule, Acc) ->
case validate(Rule, Acc, ReqParamName, ValidatorState) of
ok -> Acc;
{ok, Prepared} -> Prepared
end
end,
try
Result = lists:foldl(
fun(Rule, Acc) ->
case validate(Rule, Name, Acc, ValidatorState) of
ok -> Acc;
{ok, Prepared} -> Prepared
end
end,
Value,
Rules
),
Result = lists:foldl(Fun, Value, Rules),
{ok, Result}
catch
throw:Reason ->
@@ -866,40 +884,28 @@ to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8);
to_binary(V) when is_integer(V) -> integer_to_binary(V);
to_binary(V) when is_float(V) -> float_to_binary(V).
-spec to_list(iodata() | atom() | number()) -> binary().
to_list(V) when is_list(V) -> V;
to_list(V) when is_binary(V) -> binary_to_list(V);
to_list(V) when is_atom(V) -> atom_to_list(V);
to_list(V) when is_integer(V) -> integer_to_list(V);
to_list(V) when is_float(V) -> float_to_list(V).
-spec to_float(binary() | list()) -> integer().
to_float(Data) when is_binary(Data) ->
binary_to_float(Data);
to_float(Data) when is_list(Data) ->
list_to_float(Data).
-spec to_float(iodata()) -> float().
to_float(V) ->
binary_to_float(iolist_to_binary([V])).
-spec to_int(integer() | binary() | list()) -> integer().
to_int(Data) when is_integer(Data) ->
Data;
-spec to_int(binary() | list()) -> integer().
to_int(Data) when is_binary(Data) ->
binary_to_integer(Data);
to_int(Data) when is_list(Data) ->
list_to_integer(Data).
-spec to_header(iodata() | atom() | number()) -> binary().
-spec to_header(request_param()) -> binary().
to_header(Name) ->
to_binary(string:lowercase(to_binary(Name))).
to_binary(string:lowercase(atom_to_binary(Name, utf8))).
binary_to_lower(V) when is_binary(V) ->
string:lowercase(V).
-spec to_qs(iodata() | atom() | number()) -> binary().
-spec to_qs(request_param()) -> binary().
to_qs(Name) ->
to_binary(Name).
-spec to_binding(iodata() | atom() | number()) -> atom().
to_binding(Name) ->
Prepared = to_binary(Name),
binary_to_existing_atom(Prepared, utf8).
atom_to_binary(Name, utf8).
-spec get_opt(any(), []) -> any().
get_opt(Key, Opts) ->

View File

@@ -1,5 +1,14 @@
%% basic handler
-module(openapi_auth_handler).
-moduledoc """
Exposes the following operation IDs:
- `POST` to `/auth/http/basic`, OperationId: `TestAuthHttpBasic`:
To test HTTP basic authentication.
- `POST` to `/auth/http/bearer`, OperationId: `TestAuthHttpBearer`:
To test HTTP bearer authentication.
""".
-behaviour(cowboy_rest).

View File

@@ -1,5 +1,38 @@
%% basic handler
-module(openapi_body_handler).
-moduledoc """
Exposes the following operation IDs:
- `POST` to `/binary/gif`, OperationId: `TestBinaryGif`:
Test binary (gif) response body.
- `POST` to `/body/application/octetstream/binary`, OperationId: `TestBodyApplicationOctetstreamBinary`:
Test body parameter(s).
- `POST` to `/body/application/octetstream/array_of_binary`, OperationId: `TestBodyMultipartFormdataArrayOfBinary`:
Test array of binary in multipart mime.
- `POST` to `/body/application/octetstream/single_binary`, OperationId: `TestBodyMultipartFormdataSingleBinary`:
Test single binary in multipart mime.
- `POST` to `/echo/body/allOf/Pet`, OperationId: `TestEchoBodyAllOfPet`:
Test body parameter(s).
- `POST` to `/echo/body/FreeFormObject/response_string`, OperationId: `TestEchoBodyFreeFormObjectResponseString`:
Test free form object.
- `POST` to `/echo/body/Pet`, OperationId: `TestEchoBodyPet`:
Test body parameter(s).
- `POST` to `/echo/body/Pet/response_string`, OperationId: `TestEchoBodyPetResponseString`:
Test empty response body.
- `POST` to `/echo/body/string_enum`, OperationId: `TestEchoBodyStringEnum`:
Test string enum response body.
- `POST` to `/echo/body/Tag/response_string`, OperationId: `TestEchoBodyTagResponseString`:
Test empty json (request body).
""".
-behaviour(cowboy_rest).

View File

@@ -1,5 +1,17 @@
%% basic handler
-module(openapi_form_handler).
-moduledoc """
Exposes the following operation IDs:
- `POST` to `/form/integer/boolean/string`, OperationId: `TestFormIntegerBooleanString`:
Test form parameter(s).
- `POST` to `/form/object/multipart`, OperationId: `TestFormObjectMultipart`:
Test form parameter(s) for multipart schema.
- `POST` to `/form/oneof`, OperationId: `TestFormOneof`:
Test form parameter(s) for oneOf schema.
""".
-behaviour(cowboy_rest).

View File

@@ -1,5 +1,11 @@
%% basic handler
-module(openapi_header_handler).
-moduledoc """
Exposes the following operation IDs:
- `GET` to `/header/integer/boolean/string/enums`, OperationId: `TestHeaderIntegerBooleanStringEnums`:
Test header parameter(s).
""".
-behaviour(cowboy_rest).

View File

@@ -1,5 +1,11 @@
%% basic handler
-module(openapi_path_handler).
-moduledoc """
Exposes the following operation IDs:
- `GET` to `/path/string/:path_string/integer/:path_integer/:enum_nonref_string_path/:enum_ref_string_path`, OperationId: `TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}`:
Test path parameter(s).
""".
-behaviour(cowboy_rest).

View File

@@ -1,5 +1,38 @@
%% basic handler
-module(openapi_query_handler).
-moduledoc """
Exposes the following operation IDs:
- `GET` to `/query/enum_ref_string`, OperationId: `TestEnumRefString`:
Test query parameter(s).
- `GET` to `/query/datetime/date/string`, OperationId: `TestQueryDatetimeDateString`:
Test query parameter(s).
- `GET` to `/query/integer/boolean/string`, OperationId: `TestQueryIntegerBooleanString`:
Test query parameter(s).
- `GET` to `/query/style_deepObject/explode_true/object`, OperationId: `TestQueryStyleDeepObjectExplodeTrueObject`:
Test query parameter(s).
- `GET` to `/query/style_deepObject/explode_true/object/allOf`, OperationId: `TestQueryStyleDeepObjectExplodeTrueObjectAllOf`:
Test query parameter(s).
- `GET` to `/query/style_form/explode_false/array_integer`, OperationId: `TestQueryStyleFormExplodeFalseArrayInteger`:
Test query parameter(s).
- `GET` to `/query/style_form/explode_false/array_string`, OperationId: `TestQueryStyleFormExplodeFalseArrayString`:
Test query parameter(s).
- `GET` to `/query/style_form/explode_true/array_string`, OperationId: `TestQueryStyleFormExplodeTrueArrayString`:
Test query parameter(s).
- `GET` to `/query/style_form/explode_true/object`, OperationId: `TestQueryStyleFormExplodeTrueObject`:
Test query parameter(s).
- `GET` to `/query/style_form/explode_true/object/allOf`, OperationId: `TestQueryStyleFormExplodeTrueObjectAllOf`:
Test query parameter(s).
""".
-behaviour(cowboy_rest).

View File

@@ -19,152 +19,219 @@ get_paths(LogicHandler) ->
group_paths() ->
maps:fold(
fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) ->
case maps:find(Path, Acc) of
fun(OperationID, #{servers := Servers, base_path := BasePath, path := Path,
method := Method, handler := Handler}, Acc) ->
FullPaths = build_full_paths(Servers, BasePath, Path),
merge_paths(FullPaths, OperationID, Method, Handler, Acc)
end, #{}, get_operations()).
build_full_paths([], BasePath, Path) ->
[lists:append([BasePath, Path])];
build_full_paths(Servers, _BasePath, Path) ->
[lists:append([Server, Path]) || Server <- Servers ].
merge_paths(FullPaths, OperationID, Method, Handler, Acc) ->
lists:foldl(
fun(Path, Acc0) ->
case maps:find(Path, Acc0) of
{ok, PathInfo0 = #{operations := Operations0}} ->
Operations = Operations0#{Method => OperationID},
PathInfo = PathInfo0#{operations => Operations},
Acc#{Path => PathInfo};
Acc0#{Path => PathInfo};
error ->
Operations = #{Method => OperationID},
PathInfo = #{handler => Handler, operations => Operations},
Acc#{Path => PathInfo}
Acc0#{Path => PathInfo}
end
end, #{}, get_operations()).
end, Acc, FullPaths).
get_operations() ->
#{
'TestAuthHttpBasic' => #{
'TestAuthHttpBasic' => #{
servers => [],
base_path => "",
path => "/auth/http/basic",
method => <<"POST">>,
handler => 'openapi_auth_handler'
},
'TestAuthHttpBearer' => #{
'TestAuthHttpBearer' => #{
servers => [],
base_path => "",
path => "/auth/http/bearer",
method => <<"POST">>,
handler => 'openapi_auth_handler'
},
'TestBinaryGif' => #{
'TestBinaryGif' => #{
servers => [],
base_path => "",
path => "/binary/gif",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestBodyApplicationOctetstreamBinary' => #{
'TestBodyApplicationOctetstreamBinary' => #{
servers => [],
base_path => "",
path => "/body/application/octetstream/binary",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestBodyMultipartFormdataArrayOfBinary' => #{
'TestBodyMultipartFormdataArrayOfBinary' => #{
servers => [],
base_path => "",
path => "/body/application/octetstream/array_of_binary",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestBodyMultipartFormdataSingleBinary' => #{
'TestBodyMultipartFormdataSingleBinary' => #{
servers => [],
base_path => "",
path => "/body/application/octetstream/single_binary",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyAllOfPet' => #{
'TestEchoBodyAllOfPet' => #{
servers => [],
base_path => "",
path => "/echo/body/allOf/Pet",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyFreeFormObjectResponseString' => #{
'TestEchoBodyFreeFormObjectResponseString' => #{
servers => [],
base_path => "",
path => "/echo/body/FreeFormObject/response_string",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyPet' => #{
'TestEchoBodyPet' => #{
servers => [],
base_path => "",
path => "/echo/body/Pet",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyPetResponseString' => #{
'TestEchoBodyPetResponseString' => #{
servers => [],
base_path => "",
path => "/echo/body/Pet/response_string",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyStringEnum' => #{
'TestEchoBodyStringEnum' => #{
servers => [],
base_path => "",
path => "/echo/body/string_enum",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyTagResponseString' => #{
'TestEchoBodyTagResponseString' => #{
servers => [],
base_path => "",
path => "/echo/body/Tag/response_string",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestFormIntegerBooleanString' => #{
'TestFormIntegerBooleanString' => #{
servers => [],
base_path => "",
path => "/form/integer/boolean/string",
method => <<"POST">>,
handler => 'openapi_form_handler'
},
'TestFormObjectMultipart' => #{
'TestFormObjectMultipart' => #{
servers => [],
base_path => "",
path => "/form/object/multipart",
method => <<"POST">>,
handler => 'openapi_form_handler'
},
'TestFormOneof' => #{
'TestFormOneof' => #{
servers => [],
base_path => "",
path => "/form/oneof",
method => <<"POST">>,
handler => 'openapi_form_handler'
},
'TestHeaderIntegerBooleanStringEnums' => #{
'TestHeaderIntegerBooleanStringEnums' => #{
servers => [],
base_path => "",
path => "/header/integer/boolean/string/enums",
method => <<"GET">>,
handler => 'openapi_header_handler'
},
'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}' => #{
'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}' => #{
servers => [],
base_path => "",
path => "/path/string/:path_string/integer/:path_integer/:enum_nonref_string_path/:enum_ref_string_path",
method => <<"GET">>,
handler => 'openapi_path_handler'
},
'TestEnumRefString' => #{
'TestEnumRefString' => #{
servers => [],
base_path => "",
path => "/query/enum_ref_string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryDatetimeDateString' => #{
'TestQueryDatetimeDateString' => #{
servers => [],
base_path => "",
path => "/query/datetime/date/string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryIntegerBooleanString' => #{
'TestQueryIntegerBooleanString' => #{
servers => [],
base_path => "",
path => "/query/integer/boolean/string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleDeepObjectExplodeTrueObject' => #{
'TestQueryStyleDeepObjectExplodeTrueObject' => #{
servers => [],
base_path => "",
path => "/query/style_deepObject/explode_true/object",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleDeepObjectExplodeTrueObjectAllOf' => #{
'TestQueryStyleDeepObjectExplodeTrueObjectAllOf' => #{
servers => [],
base_path => "",
path => "/query/style_deepObject/explode_true/object/allOf",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeFalseArrayInteger' => #{
'TestQueryStyleFormExplodeFalseArrayInteger' => #{
servers => [],
base_path => "",
path => "/query/style_form/explode_false/array_integer",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeFalseArrayString' => #{
'TestQueryStyleFormExplodeFalseArrayString' => #{
servers => [],
base_path => "",
path => "/query/style_form/explode_false/array_string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeTrueArrayString' => #{
'TestQueryStyleFormExplodeTrueArrayString' => #{
servers => [],
base_path => "",
path => "/query/style_form/explode_true/array_string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeTrueObject' => #{
'TestQueryStyleFormExplodeTrueObject' => #{
servers => [],
base_path => "",
path => "/query/style_form/explode_true/object",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeTrueObjectAllOf' => #{
'TestQueryStyleFormExplodeTrueObjectAllOf' => #{
servers => [],
base_path => "",
path => "/query/style_form/explode_true/object/allOf",
method => <<"GET">>,
handler => 'openapi_query_handler'

View File

@@ -1,4 +1,7 @@
-module(openapi_server).
-moduledoc """
Echo Server API
""".
-define(DEFAULT_LOGIC_HANDLER, openapi_logic_handler).

View File

@@ -46,7 +46,7 @@ accept_callback(Class, OperationID, Req, Context) ->
-export_type([operation_id/0]).
-dialyzer({nowarn_function, [to_binary/1, to_list/1, validate_response_body/4]}).
-dialyzer({nowarn_function, [to_binary/1, validate_response_body/4]}).
-type rule() ::
{type, binary} |
@@ -484,24 +484,31 @@ request_param_info('UpdateUser', 'User') ->
request_param_info(OperationID, Name) ->
error({unknown_param, OperationID, Name}).
-spec populate_request_params(
operation_id(), [request_param()], cowboy_req:req(), jesse_state:state(), map()) ->
{ok, map(), cowboy_req:req()} | {error, _, cowboy_req:req()}.
populate_request_params(_, [], Req, _, Model) ->
{ok, Model, Req};
populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) ->
case populate_request_param(OperationID, FieldParams, Req0, ValidatorState) of
{ok, K, V, Req} ->
populate_request_params(OperationID, T, Req, ValidatorState, maps:put(K, V, Model));
populate_request_params(OperationID, [ReqParamName | T], Req0, ValidatorState, Model0) ->
case populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) of
{ok, V, Req} ->
Model = maps:put(ReqParamName, V, Model0),
populate_request_params(OperationID, T, Req, ValidatorState, Model);
Error ->
Error
end.
populate_request_param(OperationID, Name, Req0, ValidatorState) ->
#{rules := Rules, source := Source} = request_param_info(OperationID, Name),
case get_value(Source, Name, Req0) of
-spec populate_request_param(
operation_id(), request_param(), cowboy_req:req(), jesse_state:state()) ->
{ok, term(), cowboy_req:req()} | {error, term(), cowboy_req:req()}.
populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) ->
#{rules := Rules, source := Source} = request_param_info(OperationID, ReqParamName),
case get_value(Source, ReqParamName, Req0) of
{error, Reason, Req} ->
{error, Reason, Req};
{Value, Req} ->
case prepare_param(Rules, Name, Value, ValidatorState) of
{ok, Result} -> {ok, Name, Result, Req};
case prepare_param(Rules, ReqParamName, Value, ValidatorState) of
{ok, Result} -> {ok, Result, Req};
{error, Reason} ->
{error, Reason, Req}
end
@@ -511,112 +518,111 @@ populate_request_param(OperationID, Name, Req0, ValidatorState) ->
validate_response_body(list, ReturnBaseType, Body, ValidatorState) ->
[
validate(schema, ReturnBaseType, Item, ValidatorState)
validate(schema, Item, ReturnBaseType, ValidatorState)
|| Item <- Body];
validate_response_body(_, ReturnBaseType, Body, ValidatorState) ->
validate(schema, ReturnBaseType, Body, ValidatorState).
validate(schema, Body, ReturnBaseType, ValidatorState).
validate(Rule = required, Name, Value, _ValidatorState) ->
case Value of
undefined -> validation_error(Rule, Name);
_ -> ok
-spec validate(rule(), term(), request_param(), jesse_state:state()) ->
ok | {ok, term()}.
validate(required, undefined, ReqParamName, _) ->
validation_error(required, ReqParamName, undefined);
validate(required, _Value, _ReqParamName, _) ->
ok;
validate(not_required, _Value, _ReqParamName, _) ->
ok;
validate(_, undefined, _ReqParamName, _) ->
ok;
validate({type, boolean}, Value, _ReqParamName, _) when is_boolean(Value) ->
{ok, Value};
validate({type, integer}, Value, _ReqParamName, _) when is_integer(Value) ->
ok;
validate({type, float}, Value, _ReqParamName, _) when is_float(Value) ->
ok;
validate({type, binary}, Value, _ReqParamName, _) when is_binary(Value) ->
ok;
validate(Rule = {type, binary}, Value, ReqParamName, _) ->
validation_error(Rule, ReqParamName, Value);
validate(Rule = {type, boolean}, Value, ReqParamName, _) ->
case binary_to_lower(Value) of
<<"true">> -> {ok, true};
<<"false">> -> {ok, false};
_ -> validation_error(Rule, ReqParamName, Value)
end;
validate(not_required, _Name, _Value, _ValidatorState) ->
ok;
validate(_, _Name, undefined, _ValidatorState) ->
ok;
validate(Rule = {type, integer}, Name, Value, _ValidatorState) ->
validate(Rule = {type, integer}, Value, ReqParamName, _) ->
try
{ok, to_int(Value)}
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, float}, Name, Value, _ValidatorState) ->
validate(Rule = {type, float}, Value, ReqParamName, _) ->
try
{ok, to_float(Value)}
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, binary}, Name, Value, _ValidatorState) ->
validate(Rule = {type, date}, Value, ReqParamName, _) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(_Rule = {type, boolean}, _Name, Value, _ValidatorState) when is_boolean(Value) ->
{ok, Value};
validate(Rule = {type, boolean}, Name, Value, _ValidatorState) ->
V = binary_to_lower(Value),
try
case binary_to_existing_atom(V, utf8) of
B when is_boolean(B) -> {ok, B};
_ -> validation_error(Rule, Name)
end
catch
error:badarg ->
validation_error(Rule, Name)
end;
validate(Rule = {type, date}, Name, Value, _ValidatorState) ->
validate(Rule = {type, datetime}, Value, ReqParamName, _) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {type, datetime}, Name, Value, _ValidatorState) ->
case is_binary(Value) of
true -> ok;
false -> validation_error(Rule, Name)
end;
validate(Rule = {enum, Values}, Name, Value, _ValidatorState) ->
validate(Rule = {enum, Values}, Value, ReqParamName, _) ->
try
FormattedValue = erlang:binary_to_existing_atom(Value, utf8),
case lists:member(FormattedValue, Values) of
true -> {ok, FormattedValue};
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end
catch
error:badarg ->
validation_error(Rule, Name)
validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {max, Max}, Name, Value, _ValidatorState) ->
validate(Rule = {max, Max}, Value, ReqParamName, _) ->
case Value =< Max of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {exclusive_max, ExclusiveMax}, Name, Value, _ValidatorState) ->
validate(Rule = {exclusive_max, ExclusiveMax}, Value, ReqParamName, _) ->
case Value > ExclusiveMax of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {min, Min}, Name, Value, _ValidatorState) ->
validate(Rule = {min, Min}, Value, ReqParamName, _) ->
case Value >= Min of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {exclusive_min, ExclusiveMin}, Name, Value, _ValidatorState) ->
validate(Rule = {exclusive_min, ExclusiveMin}, Value, ReqParamName, _) ->
case Value =< ExclusiveMin of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {max_length, MaxLength}, Name, Value, _ValidatorState) ->
validate(Rule = {max_length, MaxLength}, Value, ReqParamName, _) ->
case size(Value) =< MaxLength of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {min_length, MinLength}, Name, Value, _ValidatorState) ->
validate(Rule = {min_length, MinLength}, Value, ReqParamName, _) ->
case size(Value) >= MinLength of
true -> ok;
false -> validation_error(Rule, Name)
false -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) ->
validate(Rule = {pattern, Pattern}, Value, ReqParamName, _) ->
{ok, MP} = re:compile(Pattern),
case re:run(Value, MP) of
{match, _} -> ok;
_ -> validation_error(Rule, Name)
_ -> validation_error(Rule, ReqParamName, Value)
end;
validate(Rule = schema, Name, Value, ValidatorState) ->
Definition = list_to_binary("#/components/schemas/" ++ to_list(Name)),
validate(Rule = schema, Value, ReqParamName, ValidatorState) ->
Definition = iolist_to_binary(["#/components/schemas/", atom_to_binary(ReqParamName)]),
try
_ = validate_with_schema(Value, Definition, ValidatorState),
ok
@@ -626,7 +632,7 @@ validate(Rule = schema, Name, Value, ValidatorState) ->
type => schema_invalid,
error => Error
},
validation_error(Rule, Name, Info);
validation_error(Rule, ReqParamName, Value, Info);
throw:[{data_invalid, Schema, Error, _, Path} | _] ->
Info = #{
type => data_invalid,
@@ -634,25 +640,25 @@ validate(Rule = schema, Name, Value, ValidatorState) ->
schema => Schema,
path => Path
},
validation_error(Rule, Name, Info)
validation_error(Rule, ReqParamName, Value, Info)
end;
validate(Rule, Name, _Value, _ValidatorState) ->
?LOG_INFO(#{what => "Cannot validate rule", name => Name, rule => Rule}),
validate(Rule, _Value, ReqParamName, _) ->
?LOG_INFO(#{what => "Cannot validate rule", name => ReqParamName, rule => Rule}),
error({unknown_validation_rule, Rule}).
-spec validation_error(Rule :: any(), Name :: any()) -> no_return().
validation_error(ViolatedRule, Name) ->
validation_error(ViolatedRule, Name, #{}).
-spec validation_error(rule(), request_param(), term()) -> no_return().
validation_error(ViolatedRule, Name, Value) ->
validation_error(ViolatedRule, Name, Value, #{}).
-spec validation_error(Rule :: any(), Name :: any(), Info :: #{_ := _}) -> no_return().
validation_error(ViolatedRule, Name, Info) ->
throw({wrong_param, Name, ViolatedRule, Info}).
-spec validation_error(rule(), request_param(), term(), Info :: #{_ := _}) -> no_return().
validation_error(ViolatedRule, Name, Value, Info) ->
throw({wrong_param, Name, Value, ViolatedRule, Info}).
-spec get_value(body | qs_val | header | binding, Name :: any(), Req0 :: cowboy_req:req()) ->
{Value :: any(), Req :: cowboy_req:req()} |
{error, Reason :: any(), Req :: cowboy_req:req()}.
-spec get_value(body | qs_val | header | binding, request_param(), cowboy_req:req()) ->
{any(), cowboy_req:req()} |
{error, any(), cowboy_req:req()}.
get_value(body, _Name, Req0) ->
{ok, Body, Req} = cowboy_req:read_body(Req0),
{ok, Body, Req} = read_entire_body(Req0),
case prepare_body(Body) of
{error, Reason} ->
{error, Reason, Req};
@@ -668,17 +674,30 @@ get_value(header, Name, Req) ->
Value = maps:get(to_header(Name), Headers, undefined),
{Value, Req};
get_value(binding, Name, Req) ->
Value = cowboy_req:binding(to_binding(Name), Req),
Value = cowboy_req:binding(Name, Req),
{Value, Req}.
-spec read_entire_body(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
read_entire_body(Req) ->
read_entire_body(Req, []).
-spec read_entire_body(cowboy_req:req(), iodata()) -> {ok, binary(), cowboy_req:req()}.
read_entire_body(Request, Acc) -> % {
case cowboy_req:read_body(Request) of
{ok, Data, NewRequest} ->
{ok, iolist_to_binary(lists:reverse([Data | Acc])), NewRequest};
{more, Data, NewRequest} ->
read_entire_body(NewRequest, [Data | Acc])
end.
prepare_body(<<>>) ->
<<>>;
prepare_body(Body) ->
try
json:decode(Body)
catch
error:_ ->
{error, {invalid_body, not_json, Body}}
error:Error ->
{error, {invalid_json, Body, Error}}
end.
validate_with_schema(Body, Definition, ValidatorState) ->
@@ -688,18 +707,17 @@ validate_with_schema(Body, Definition, ValidatorState) ->
ValidatorState
).
prepare_param(Rules, Name, Value, ValidatorState) ->
-spec prepare_param([rule()], request_param(), term(), jesse_state:state()) ->
{ok, term()} | {error, Reason :: any()}.
prepare_param(Rules, ReqParamName, Value, ValidatorState) ->
Fun = fun(Rule, Acc) ->
case validate(Rule, Acc, ReqParamName, ValidatorState) of
ok -> Acc;
{ok, Prepared} -> Prepared
end
end,
try
Result = lists:foldl(
fun(Rule, Acc) ->
case validate(Rule, Name, Acc, ValidatorState) of
ok -> Acc;
{ok, Prepared} -> Prepared
end
end,
Value,
Rules
),
Result = lists:foldl(Fun, Value, Rules),
{ok, Result}
catch
throw:Reason ->
@@ -713,40 +731,28 @@ to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8);
to_binary(V) when is_integer(V) -> integer_to_binary(V);
to_binary(V) when is_float(V) -> float_to_binary(V).
-spec to_list(iodata() | atom() | number()) -> binary().
to_list(V) when is_list(V) -> V;
to_list(V) when is_binary(V) -> binary_to_list(V);
to_list(V) when is_atom(V) -> atom_to_list(V);
to_list(V) when is_integer(V) -> integer_to_list(V);
to_list(V) when is_float(V) -> float_to_list(V).
-spec to_float(binary() | list()) -> integer().
to_float(Data) when is_binary(Data) ->
binary_to_float(Data);
to_float(Data) when is_list(Data) ->
list_to_float(Data).
-spec to_float(iodata()) -> float().
to_float(V) ->
binary_to_float(iolist_to_binary([V])).
-spec to_int(integer() | binary() | list()) -> integer().
to_int(Data) when is_integer(Data) ->
Data;
-spec to_int(binary() | list()) -> integer().
to_int(Data) when is_binary(Data) ->
binary_to_integer(Data);
to_int(Data) when is_list(Data) ->
list_to_integer(Data).
-spec to_header(iodata() | atom() | number()) -> binary().
-spec to_header(request_param()) -> binary().
to_header(Name) ->
to_binary(string:lowercase(to_binary(Name))).
to_binary(string:lowercase(atom_to_binary(Name, utf8))).
binary_to_lower(V) when is_binary(V) ->
string:lowercase(V).
-spec to_qs(iodata() | atom() | number()) -> binary().
-spec to_qs(request_param()) -> binary().
to_qs(Name) ->
to_binary(Name).
-spec to_binding(iodata() | atom() | number()) -> atom().
to_binding(Name) ->
Prepared = to_binary(Name),
binary_to_existing_atom(Prepared, utf8).
atom_to_binary(Name, utf8).
-spec get_opt(any(), []) -> any().
get_opt(Key, Opts) ->

View File

@@ -1,5 +1,32 @@
%% basic handler
-module(openapi_pet_handler).
-moduledoc """
Exposes the following operation IDs:
- `POST` to `/pet`, OperationId: `AddPet`:
Add a new pet to the store.
- `DELETE` to `/pet/:petId`, OperationId: `DeletePet`:
Deletes a pet.
- `GET` to `/pet/findByStatus`, OperationId: `FindPetsByStatus`:
Finds Pets by status.
- `GET` to `/pet/findByTags`, OperationId: `FindPetsByTags`:
Finds Pets by tags.
- `GET` to `/pet/:petId`, OperationId: `GetPetById`:
Find pet by ID.
- `PUT` to `/pet`, OperationId: `UpdatePet`:
Update an existing pet.
- `POST` to `/pet/:petId`, OperationId: `UpdatePetWithForm`:
Updates a pet in the store with form data.
- `POST` to `/pet/:petId/uploadImage`, OperationId: `UploadFile`:
uploads an image.
""".
-behaviour(cowboy_rest).

View File

@@ -19,118 +19,171 @@ get_paths(LogicHandler) ->
group_paths() ->
maps:fold(
fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) ->
case maps:find(Path, Acc) of
fun(OperationID, #{servers := Servers, base_path := BasePath, path := Path,
method := Method, handler := Handler}, Acc) ->
FullPaths = build_full_paths(Servers, BasePath, Path),
merge_paths(FullPaths, OperationID, Method, Handler, Acc)
end, #{}, get_operations()).
build_full_paths([], BasePath, Path) ->
[lists:append([BasePath, Path])];
build_full_paths(Servers, _BasePath, Path) ->
[lists:append([Server, Path]) || Server <- Servers ].
merge_paths(FullPaths, OperationID, Method, Handler, Acc) ->
lists:foldl(
fun(Path, Acc0) ->
case maps:find(Path, Acc0) of
{ok, PathInfo0 = #{operations := Operations0}} ->
Operations = Operations0#{Method => OperationID},
PathInfo = PathInfo0#{operations => Operations},
Acc#{Path => PathInfo};
Acc0#{Path => PathInfo};
error ->
Operations = #{Method => OperationID},
PathInfo = #{handler => Handler, operations => Operations},
Acc#{Path => PathInfo}
Acc0#{Path => PathInfo}
end
end, #{}, get_operations()).
end, Acc, FullPaths).
get_operations() ->
#{
'AddPet' => #{
path => "/v2/pet",
'AddPet' => #{
servers => [],
base_path => "/v2",
path => "/pet",
method => <<"POST">>,
handler => 'openapi_pet_handler'
},
'DeletePet' => #{
path => "/v2/pet/:petId",
'DeletePet' => #{
servers => [],
base_path => "/v2",
path => "/pet/:petId",
method => <<"DELETE">>,
handler => 'openapi_pet_handler'
},
'FindPetsByStatus' => #{
path => "/v2/pet/findByStatus",
'FindPetsByStatus' => #{
servers => [],
base_path => "/v2",
path => "/pet/findByStatus",
method => <<"GET">>,
handler => 'openapi_pet_handler'
},
'FindPetsByTags' => #{
path => "/v2/pet/findByTags",
'FindPetsByTags' => #{
servers => [],
base_path => "/v2",
path => "/pet/findByTags",
method => <<"GET">>,
handler => 'openapi_pet_handler'
},
'GetPetById' => #{
path => "/v2/pet/:petId",
'GetPetById' => #{
servers => [],
base_path => "/v2",
path => "/pet/:petId",
method => <<"GET">>,
handler => 'openapi_pet_handler'
},
'UpdatePet' => #{
path => "/v2/pet",
'UpdatePet' => #{
servers => [],
base_path => "/v2",
path => "/pet",
method => <<"PUT">>,
handler => 'openapi_pet_handler'
},
'UpdatePetWithForm' => #{
path => "/v2/pet/:petId",
'UpdatePetWithForm' => #{
servers => [],
base_path => "/v2",
path => "/pet/:petId",
method => <<"POST">>,
handler => 'openapi_pet_handler'
},
'UploadFile' => #{
path => "/v2/pet/:petId/uploadImage",
'UploadFile' => #{
servers => [],
base_path => "/v2",
path => "/pet/:petId/uploadImage",
method => <<"POST">>,
handler => 'openapi_pet_handler'
},
'DeleteOrder' => #{
path => "/v2/store/order/:orderId",
'DeleteOrder' => #{
servers => [],
base_path => "/v2",
path => "/store/order/:orderId",
method => <<"DELETE">>,
handler => 'openapi_store_handler'
},
'GetInventory' => #{
path => "/v2/store/inventory",
'GetInventory' => #{
servers => [],
base_path => "/v2",
path => "/store/inventory",
method => <<"GET">>,
handler => 'openapi_store_handler'
},
'GetOrderById' => #{
path => "/v2/store/order/:orderId",
'GetOrderById' => #{
servers => [],
base_path => "/v2",
path => "/store/order/:orderId",
method => <<"GET">>,
handler => 'openapi_store_handler'
},
'PlaceOrder' => #{
path => "/v2/store/order",
'PlaceOrder' => #{
servers => [],
base_path => "/v2",
path => "/store/order",
method => <<"POST">>,
handler => 'openapi_store_handler'
},
'CreateUser' => #{
path => "/v2/user",
'CreateUser' => #{
servers => [],
base_path => "/v2",
path => "/user",
method => <<"POST">>,
handler => 'openapi_user_handler'
},
'CreateUsersWithArrayInput' => #{
path => "/v2/user/createWithArray",
'CreateUsersWithArrayInput' => #{
servers => [],
base_path => "/v2",
path => "/user/createWithArray",
method => <<"POST">>,
handler => 'openapi_user_handler'
},
'CreateUsersWithListInput' => #{
path => "/v2/user/createWithList",
'CreateUsersWithListInput' => #{
servers => [],
base_path => "/v2",
path => "/user/createWithList",
method => <<"POST">>,
handler => 'openapi_user_handler'
},
'DeleteUser' => #{
path => "/v2/user/:username",
'DeleteUser' => #{
servers => [],
base_path => "/v2",
path => "/user/:username",
method => <<"DELETE">>,
handler => 'openapi_user_handler'
},
'GetUserByName' => #{
path => "/v2/user/:username",
'GetUserByName' => #{
servers => [],
base_path => "/v2",
path => "/user/:username",
method => <<"GET">>,
handler => 'openapi_user_handler'
},
'LoginUser' => #{
path => "/v2/user/login",
'LoginUser' => #{
servers => [],
base_path => "/v2",
path => "/user/login",
method => <<"GET">>,
handler => 'openapi_user_handler'
},
'LogoutUser' => #{
path => "/v2/user/logout",
'LogoutUser' => #{
servers => [],
base_path => "/v2",
path => "/user/logout",
method => <<"GET">>,
handler => 'openapi_user_handler'
},
'UpdateUser' => #{
path => "/v2/user/:username",
'UpdateUser' => #{
servers => [],
base_path => "/v2",
path => "/user/:username",
method => <<"PUT">>,
handler => 'openapi_user_handler'
}

View File

@@ -1,4 +1,7 @@
-module(openapi_server).
-moduledoc """
This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
""".
-define(DEFAULT_LOGIC_HANDLER, openapi_logic_handler).

View File

@@ -1,5 +1,20 @@
%% basic handler
-module(openapi_store_handler).
-moduledoc """
Exposes the following operation IDs:
- `DELETE` to `/store/order/:orderId`, OperationId: `DeleteOrder`:
Delete purchase order by ID.
- `GET` to `/store/inventory`, OperationId: `GetInventory`:
Returns pet inventories by status.
- `GET` to `/store/order/:orderId`, OperationId: `GetOrderById`:
Find purchase order by ID.
- `POST` to `/store/order`, OperationId: `PlaceOrder`:
Place an order for a pet.
""".
-behaviour(cowboy_rest).

View File

@@ -1,5 +1,32 @@
%% basic handler
-module(openapi_user_handler).
-moduledoc """
Exposes the following operation IDs:
- `POST` to `/user`, OperationId: `CreateUser`:
Create user.
- `POST` to `/user/createWithArray`, OperationId: `CreateUsersWithArrayInput`:
Creates list of users with given input array.
- `POST` to `/user/createWithList`, OperationId: `CreateUsersWithListInput`:
Creates list of users with given input array.
- `DELETE` to `/user/:username`, OperationId: `DeleteUser`:
Delete user.
- `GET` to `/user/:username`, OperationId: `GetUserByName`:
Get user by user name.
- `GET` to `/user/login`, OperationId: `LoginUser`:
Logs user into the system.
- `GET` to `/user/logout`, OperationId: `LogoutUser`:
Logs out current logged in user session.
- `PUT` to `/user/:username`, OperationId: `UpdateUser`:
Updated user.
""".
-behaviour(cowboy_rest).