feat(swagger): swagger support hocon schema
This commit is contained in:
parent
1d9076e2eb
commit
dab5fbf285
|
@ -16,7 +16,7 @@
|
|||
, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}}
|
||||
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}}
|
||||
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
|
||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.17.1"}}}
|
||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.0"}}}
|
||||
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
|
||||
, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}
|
||||
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}
|
||||
|
|
|
@ -156,11 +156,11 @@ fields("stats") ->
|
|||
|
||||
fields("authorization") ->
|
||||
[ {"no_match",
|
||||
sc(union(allow, deny),
|
||||
sc(hoconsc:union([allow, deny]),
|
||||
#{ default => allow
|
||||
})}
|
||||
, {"deny_action",
|
||||
sc(union(ignore, disconnect),
|
||||
sc(hoconsc:union([ignore, disconnect]),
|
||||
#{ default => ignore
|
||||
})}
|
||||
, {"cache",
|
||||
|
@ -939,9 +939,9 @@ ssl(Defaults) ->
|
|||
sc(boolean(),
|
||||
#{ default => Df("secure_renegotiate", true)
|
||||
, desc => """
|
||||
SSL parameter renegotiation is a feature that allows a client and a server
|
||||
to renegotiate the parameters of the SSL connection on the fly.
|
||||
RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation,
|
||||
SSL parameter renegotiation is a feature that allows a client and a server
|
||||
to renegotiate the parameters of the SSL connection on the fly.
|
||||
RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation,
|
||||
you drop support for the insecure renegotiation, prone to MitM attacks.
|
||||
"""
|
||||
})
|
||||
|
@ -950,13 +950,13 @@ you drop support for the insecure renegotiation, prone to MitM attacks.
|
|||
sc(boolean(),
|
||||
#{ default => Df("client_renegotiation", true)
|
||||
, desc => """
|
||||
In protocols that support client-initiated renegotiation,
|
||||
the cost of resources of such an operation is higher for the server than the client.
|
||||
This can act as a vector for denial of service attacks.
|
||||
The SSL application already takes measures to counter-act such attempts,
|
||||
but client-initiated renegotiation can be strictly disabled by setting this option to false.
|
||||
The default value is true. Note that disabling renegotiation can result in
|
||||
long-lived connections becoming unusable due to limits on
|
||||
In protocols that support client-initiated renegotiation,
|
||||
the cost of resources of such an operation is higher for the server than the client.
|
||||
This can act as a vector for denial of service attacks.
|
||||
The SSL application already takes measures to counter-act such attempts,
|
||||
but client-initiated renegotiation can be strictly disabled by setting this option to false.
|
||||
The default value is true. Note that disabling renegotiation can result in
|
||||
long-lived connections becoming unusable due to limits on
|
||||
the number of messages the underlying cipher suite can encipher.
|
||||
"""
|
||||
})
|
||||
|
|
|
@ -13,3 +13,4 @@
|
|||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
||||
{eunit_first_files, ["test/emqx_swagger_remote_schema.erl"]}.
|
||||
|
|
|
@ -29,135 +29,132 @@
|
|||
-behaviour(minirest_api).
|
||||
|
||||
-include("emqx_dashboard.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-import(hoconsc, [mk/2, ref/2, array/1, enum/1]).
|
||||
|
||||
-import(emqx_mgmt_util, [ schema/1
|
||||
, object_schema/1
|
||||
, object_schema/2
|
||||
, object_array_schema/1
|
||||
, bad_request/0
|
||||
, properties/1
|
||||
]).
|
||||
|
||||
-export([api_spec/0]).
|
||||
|
||||
-export([ login/2
|
||||
, logout/2
|
||||
, users/2
|
||||
, user/2
|
||||
, change_pwd/2
|
||||
]).
|
||||
-export([api_spec/0, fields/1, paths/0, schema/1, namespace/0]).
|
||||
-export([login/2, logout/2, users/2, user/2, change_pwd/2]).
|
||||
|
||||
-define(EMPTY(V), (V == undefined orelse V == <<>>)).
|
||||
|
||||
-define(ERROR_USERNAME_OR_PWD, 'ERROR_USERNAME_OR_PWD').
|
||||
|
||||
namespace() -> "dashboard".
|
||||
|
||||
api_spec() ->
|
||||
{[ login_api()
|
||||
, logout_api()
|
||||
, users_api()
|
||||
, user_api()
|
||||
, change_pwd_api()
|
||||
],
|
||||
[]}.
|
||||
emqx_dashboard_swagger:spec(?MODULE).
|
||||
|
||||
login_api() ->
|
||||
AuthProps = properties([{username, string, <<"Username">>},
|
||||
{password, string, <<"Password">>}]),
|
||||
paths() -> ["/login", "/logout", "/users",
|
||||
"/users/:username", "/users/:username/change_pwd"].
|
||||
|
||||
TokenProps = properties([{token, string, <<"JWT Token">>},
|
||||
{license, object, [{edition, string, <<"License">>, [community, enterprise]}]},
|
||||
{version, string}]),
|
||||
Metadata = #{
|
||||
schema("/login") ->
|
||||
#{
|
||||
operationId => login,
|
||||
post => #{
|
||||
tags => [dashboard],
|
||||
tags => [<<"dashboard">>],
|
||||
description => <<"Dashboard Auth">>,
|
||||
'requestBody' => object_schema(AuthProps),
|
||||
summary => <<"Dashboard Auth">>,
|
||||
requestBody =>
|
||||
[
|
||||
{username, mk(binary(),
|
||||
#{desc => <<"The User for which to create the token.">>,
|
||||
maxLength => 100, example => <<"admin">>})},
|
||||
{password, mk(binary(),
|
||||
#{desc => "password", example => "public"})}
|
||||
],
|
||||
responses => #{
|
||||
<<"200">> =>
|
||||
object_schema(TokenProps, <<"Dashboard Auth successfully">>),
|
||||
<<"401">> => unauthorized_request()
|
||||
200 => [
|
||||
{token, mk(string(), #{desc => <<"JWT Token">>})},
|
||||
{license, [{edition,
|
||||
mk(enum([community, enterprise]), #{desc => <<"license">>,
|
||||
example => "community"})}]},
|
||||
{version, mk(string(), #{desc => <<"version">>, example => <<"5.0.0">>})}],
|
||||
401 => [
|
||||
{code, mk(string(), #{example => 'ERROR_USERNAME_OR_PWD'})},
|
||||
{message, mk(string(), #{example => "Unauthorized"})}]
|
||||
},
|
||||
security => []
|
||||
}
|
||||
},
|
||||
{"/login", Metadata, login}.
|
||||
|
||||
logout_api() ->
|
||||
LogoutProps = properties([{username, string, <<"Username">>}]),
|
||||
Metadata = #{
|
||||
}};
|
||||
schema("/logout") ->
|
||||
#{
|
||||
operationId => logout,
|
||||
post => #{
|
||||
tags => [dashboard],
|
||||
description => <<"Dashboard Auth">>,
|
||||
'requestBody' => object_schema(LogoutProps),
|
||||
tags => [<<"dashboard">>],
|
||||
description => <<"Dashboard User logout">>,
|
||||
requestBody => [
|
||||
{username, mk(binary(),
|
||||
#{desc => <<"The User for which to create the token.">>,
|
||||
maxLength => 100, example => <<"admin">>})}
|
||||
],
|
||||
responses => #{
|
||||
<<"200">> => schema(<<"Dashboard Auth successfully">>)
|
||||
200 => <<"Dashboard logout successfully">>
|
||||
}
|
||||
}
|
||||
},
|
||||
{"/logout", Metadata, logout}.
|
||||
|
||||
users_api() ->
|
||||
BaseProps = properties([{username, string, <<"Username">>},
|
||||
{password, string, <<"Password">>},
|
||||
{tag, string, <<"Tag">>}]),
|
||||
Metadata = #{
|
||||
};
|
||||
schema("/users") ->
|
||||
#{
|
||||
operationId => users,
|
||||
get => #{
|
||||
tags => [dashboard],
|
||||
tags => [<<"dashboard">>],
|
||||
description => <<"Get dashboard users">>,
|
||||
responses => #{
|
||||
<<"200">> => object_array_schema(maps:without([password], BaseProps))
|
||||
}
|
||||
},
|
||||
post => #{
|
||||
tags => [dashboard],
|
||||
description => <<"Create dashboard users">>,
|
||||
'requestBody' => object_schema(BaseProps),
|
||||
responses => #{
|
||||
<<"200">> => schema(<<"Create Users successfully">>),
|
||||
<<"400">> => bad_request()
|
||||
200 => mk(array(ref(?MODULE, user)),
|
||||
#{desc => "User lists"})
|
||||
}
|
||||
}
|
||||
},
|
||||
{"/users", Metadata, users}.
|
||||
};
|
||||
|
||||
user_api() ->
|
||||
Metadata = #{
|
||||
delete => #{
|
||||
tags => [dashboard],
|
||||
description => <<"Delete dashboard users">>,
|
||||
parameters => parameters(),
|
||||
responses => #{
|
||||
<<"200">> => schema(<<"Delete User successfully">>),
|
||||
<<"400">> => bad_request()
|
||||
}
|
||||
},
|
||||
schema("/users/:username") ->
|
||||
#{
|
||||
operationId => user,
|
||||
put => #{
|
||||
tags => [dashboard],
|
||||
tags => [<<"dashboard">>],
|
||||
description => <<"Update dashboard users">>,
|
||||
parameters => parameters(),
|
||||
'requestBody' => object_schema(properties([{tag, string, <<"Tag">>}])),
|
||||
parameters => [{username, mk(binary(),
|
||||
#{in => path, example => <<"admin">>})}],
|
||||
requestBody => [{tag, mk(binary(), #{desc => <<"Tag">>})}],
|
||||
responses => #{
|
||||
<<"200">> => schema(<<"Update Users successfully">>),
|
||||
<<"400">> => bad_request()
|
||||
}
|
||||
}
|
||||
},
|
||||
{"/users/:username", Metadata, user}.
|
||||
|
||||
change_pwd_api() ->
|
||||
Metadata = #{
|
||||
200 => <<"Update User successfully">>,
|
||||
400 => [{code, mk(string(), #{example => 'UPDATE_FAIL'})},
|
||||
{message, mk(string(), #{example => "Update Failed unknown"})}]}},
|
||||
delete => #{
|
||||
tags => [<<"dashboard">>],
|
||||
description => <<"Delete dashboard users">>,
|
||||
parameters => [{username, mk(binary(),
|
||||
#{in => path, example => <<"admin">>})}],
|
||||
responses => #{
|
||||
200 => <<"Delete User successfully">>,
|
||||
400 => [
|
||||
{code, mk(string(), #{example => 'CANNOT_DELETE_ADMIN'})},
|
||||
{message, mk(string(), #{example => "CANNOT DELETE ADMIN"})}]}}
|
||||
};
|
||||
schema("/users/:username/change_pwd") ->
|
||||
#{
|
||||
operationId => change_pwd,
|
||||
put => #{
|
||||
tags => [dashboard],
|
||||
tags => [<<"dashboard">>],
|
||||
description => <<"Update dashboard users password">>,
|
||||
parameters => parameters(),
|
||||
'requestBody' => object_schema(properties([old_pwd, new_pwd])),
|
||||
parameters => [{username, mk(binary(),
|
||||
#{in => path, required => true, example => <<"admin">>})}],
|
||||
requestBody => [
|
||||
{old_pwd, mk(binary(), #{required => true})},
|
||||
{new_pwd, mk(binary(), #{required => true})}
|
||||
],
|
||||
responses => #{
|
||||
<<"200">> => schema(<<"Update Users password successfully">>),
|
||||
<<"400">> => bad_request()
|
||||
}
|
||||
}
|
||||
},
|
||||
{"/users/:username/change_pwd", Metadata, change_pwd}.
|
||||
200 => <<"Update user password successfully">>,
|
||||
400 => [
|
||||
{code, mk(string(), #{example => 'UPDATE_FAIL'})},
|
||||
{message, mk(string(), #{example => "Failed Reason"})}]}}
|
||||
}.
|
||||
|
||||
fields(user) ->
|
||||
[
|
||||
{tag,
|
||||
mk(string(),
|
||||
#{desc => <<"tag">>, example => "administrator"})},
|
||||
{username,
|
||||
mk(string(),
|
||||
#{desc => <<"username">>, example => "emqx"})}
|
||||
].
|
||||
|
||||
login(post, #{body := Params}) ->
|
||||
Username = maps:get(<<"username">>, Params),
|
||||
|
@ -171,7 +168,7 @@ login(post, #{body := Params}) ->
|
|||
end.
|
||||
|
||||
logout(_, #{body := #{<<"username">> := Username},
|
||||
headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}}) ->
|
||||
headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}}) ->
|
||||
case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of
|
||||
ok ->
|
||||
200;
|
||||
|
@ -187,9 +184,9 @@ users(post, #{body := Params}) ->
|
|||
Username = maps:get(<<"username">>, Params),
|
||||
Password = maps:get(<<"password">>, Params),
|
||||
case ?EMPTY(Username) orelse ?EMPTY(Password) of
|
||||
true ->
|
||||
true ->
|
||||
{400, #{code => <<"CREATE_USER_FAIL">>,
|
||||
message => <<"Username or password undefined">>}};
|
||||
message => <<"Username or password undefined">>}};
|
||||
false ->
|
||||
case emqx_dashboard_admin:add_user(Username, Password, Tag) of
|
||||
ok -> {200};
|
||||
|
@ -208,8 +205,8 @@ user(put, #{bindings := #{username := Username}, body := Params}) ->
|
|||
|
||||
user(delete, #{bindings := #{username := Username}}) ->
|
||||
case Username == <<"admin">> of
|
||||
true -> {400, #{code => <<"CONNOT_DELETE_ADMIN">>,
|
||||
message => <<"Cannot delete admin">>}};
|
||||
true -> {400, #{code => <<"CANNOT_DELETE_ADMIN">>,
|
||||
message => <<"Cannot delete admin">>}};
|
||||
false ->
|
||||
_ = emqx_dashboard_admin:remove_user(Username),
|
||||
{200}
|
||||
|
@ -226,20 +223,3 @@ change_pwd(put, #{bindings := #{username := Username}, body := Params}) ->
|
|||
|
||||
row(#mqtt_admin{username = Username, tags = Tag}) ->
|
||||
#{username => Username, tag => Tag}.
|
||||
|
||||
parameters() ->
|
||||
[#{
|
||||
name => username,
|
||||
in => path,
|
||||
required => true,
|
||||
schema => #{type => string},
|
||||
example => <<"admin">>
|
||||
}].
|
||||
|
||||
unauthorized_request() ->
|
||||
object_schema(
|
||||
properties([{message, string},
|
||||
{code, string, <<"Resp Code">>, [?ERROR_USERNAME_OR_PWD]}
|
||||
]),
|
||||
<<"Unauthorized">>
|
||||
).
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-export([ roots/0
|
||||
, fields/1]).
|
||||
, fields/1
|
||||
,namespace/0]).
|
||||
|
||||
namespace() -> <<"dashboard">>.
|
||||
roots() -> ["emqx_dashboard"].
|
||||
|
||||
fields("emqx_dashboard") ->
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
-module(emqx_dashboard_swagger).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
||||
%% API
|
||||
-export([spec/1]).
|
||||
-export([translate_req/2]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
|
||||
|
||||
-define(DEFAULT_FIELDS, [example, allowReserved, style,
|
||||
explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
|
||||
|
||||
-define(DEFAULT_FILTER, #{filter => fun ?MODULE:translate_req/2}).
|
||||
|
||||
-define(INIT_SCHEMA, #{fields => #{}, translations => #{}, validations => [], namespace => undefined}).
|
||||
|
||||
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
||||
-define(TO_COMPONENTS(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>, ?TO_REF(namespace(_M_), _F_)])).
|
||||
|
||||
-spec(spec(module()) -> {list({Path, Specs, OperationId, Options}), list(Component)} when
|
||||
Path :: string()|binary(),
|
||||
Specs :: map(),
|
||||
OperationId :: atom(),
|
||||
Options :: #{filter => fun((map(),
|
||||
#{module => module(), path => string(), method => atom()}) -> map())},
|
||||
Component :: map()).
|
||||
spec(Module) ->
|
||||
Paths = apply(Module, paths, []),
|
||||
{ApiSpec, AllRefs} =
|
||||
lists:foldl(fun(Path, {AllAcc, AllRefsAcc}) ->
|
||||
{OperationId, Specs, Refs} = parse_spec_ref(Module, Path),
|
||||
{[{Path, Specs, OperationId, ?DEFAULT_FILTER} | AllAcc],
|
||||
Refs ++ AllRefsAcc}
|
||||
end, {[], []}, Paths),
|
||||
{ApiSpec, components(lists:usort(AllRefs))}.
|
||||
|
||||
-spec(translate_req(#{binding => list(), query_string => list(), body => map()},
|
||||
#{module => module(), path => string(), method => atom()}) ->
|
||||
{ok, #{binding => list(), query_string => list(), body => map()}}|
|
||||
{400, 'BAD_REQUEST', binary()}).
|
||||
translate_req(Request, #{module := Module, path := Path, method := Method}) ->
|
||||
#{Method := Spec} = apply(Module, schema, [Path]),
|
||||
try
|
||||
Params = maps:get(parameters, Spec, []),
|
||||
Body = maps:get(requestBody, Spec, []),
|
||||
{Bindings, QueryStr} = check_parameters(Request, Params),
|
||||
NewBody = check_requestBody(Request, Body, Module, hoconsc:is_schema(Body)),
|
||||
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
||||
catch throw:Error ->
|
||||
{_, [{validation_error, ValidErr}]} = Error,
|
||||
#{path := Key, reason := Reason} = ValidErr,
|
||||
{400, 'BAD_REQUEST', iolist_to_binary(io_lib:format("~s : ~p", [Key, Reason]))}
|
||||
end.
|
||||
|
||||
parse_spec_ref(Module, Path) ->
|
||||
Schema =
|
||||
try
|
||||
erlang:apply(Module, schema, [Path])
|
||||
catch error: Reason -> %% better error message
|
||||
throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
|
||||
end,
|
||||
{Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) ->
|
||||
(not lists:member(Method, ?METHODS))
|
||||
andalso throw({error, #{module => Module, path => Path, method => Method}}),
|
||||
{Spec, SubRefs} = meta_to_spec(Meta, Module),
|
||||
{Acc#{Method => Spec}, SubRefs ++ RefsAcc}
|
||||
end, {#{}, []},
|
||||
maps:without([operationId], Schema)),
|
||||
{maps:get(operationId, Schema), Specs, Refs}.
|
||||
|
||||
check_parameters(Request, Spec) ->
|
||||
#{bindings := Bindings, query_string := QueryStr} = Request,
|
||||
BindingsBin = maps:fold(fun(Key, Value, Acc) -> Acc#{atom_to_binary(Key) => Value} end, #{}, Bindings),
|
||||
check_parameter(Spec, BindingsBin, QueryStr, #{}, #{}).
|
||||
|
||||
check_parameter([], _Bindings, _QueryStr, NewBindings, NewQueryStr) -> {NewBindings, NewQueryStr};
|
||||
check_parameter([{Name, Type} | Spec], Bindings, QueryStr, BindingsAcc, QueryStrAcc) ->
|
||||
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
||||
case hocon_schema:field_schema(Type, in) of
|
||||
path ->
|
||||
NewBindings = hocon_schema:check_plain(Schema, Bindings, #{atom_key => true}),
|
||||
NewBindingsAcc = maps:merge(BindingsAcc, NewBindings),
|
||||
check_parameter(Spec, Bindings, QueryStr, NewBindingsAcc, QueryStrAcc);
|
||||
query ->
|
||||
NewQueryStr = hocon_schema:check_plain(Schema, QueryStr),
|
||||
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
|
||||
check_parameter(Spec, Bindings, QueryStr, BindingsAcc, NewQueryStrAcc)
|
||||
end.
|
||||
|
||||
check_requestBody(#{body := Body}, Schema, Module, true) ->
|
||||
Type0 = hocon_schema:field_schema(Schema, type),
|
||||
Type =
|
||||
case Type0 of
|
||||
?REF(StructName) -> ?R_REF(Module, StructName);
|
||||
_ -> Type0
|
||||
end,
|
||||
NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]},
|
||||
#{<<"root">> := NewBody} = hocon_schema:check_plain(NewSchema, #{<<"root">> => Body}),
|
||||
NewBody;
|
||||
%% TODO not support nest object check yet, please use ref!
|
||||
%% RequestBody = [ {per_page, mk(integer(), #{}},
|
||||
%% {nest_object, [
|
||||
%% {good_nest_1, mk(integer(), #{})},
|
||||
%% {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
|
||||
%% ]}
|
||||
%% ]
|
||||
check_requestBody(#{body := Body}, Spec, _Module, false) ->
|
||||
lists:foldl(fun({Name, Type}, Acc) ->
|
||||
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
||||
maps:merge(Acc, hocon_schema:check_plain(Schema, Body))
|
||||
end, #{}, Spec).
|
||||
|
||||
%% tags, description, summary, security, deprecated
|
||||
meta_to_spec(Meta, Module) ->
|
||||
{Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module),
|
||||
{RequestBody, Refs2} = requestBody(maps:get(requestBody, Meta, []), Module),
|
||||
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module),
|
||||
{
|
||||
to_spec(Meta, Params, RequestBody, Responses),
|
||||
lists:usort(Refs1 ++ Refs2 ++ Refs3)
|
||||
}.
|
||||
|
||||
to_spec(Meta, Params, [], Responses) ->
|
||||
Spec = maps:without([parameters, requestBody, responses], Meta),
|
||||
Spec#{parameters => Params, responses => Responses};
|
||||
to_spec(Meta, Params, RequestBody, Responses) ->
|
||||
Spec = to_spec(Meta, Params, [], Responses),
|
||||
maps:put(requestBody, RequestBody, Spec).
|
||||
|
||||
parameters(Params, Module) ->
|
||||
{SpecList, AllRefs} =
|
||||
lists:foldl(fun({Name, Type}, {Acc, RefsAcc}) ->
|
||||
In = hocon_schema:field_schema(Type, in),
|
||||
In =:= undefined andalso throw({error, <<"missing in:path/query field in parameters">>}),
|
||||
Nullable = hocon_schema:field_schema(Type, nullable),
|
||||
Default = hocon_schema:field_schema(Type, default),
|
||||
HoconType = hocon_schema:field_schema(Type, type),
|
||||
Meta = init_meta(Nullable, Default),
|
||||
{ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
|
||||
Spec0 = init_prop([required | ?DEFAULT_FIELDS],
|
||||
#{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type),
|
||||
Spec1 = trans_required(Spec0, Nullable, In),
|
||||
Spec2 = trans_desc(Spec1, Type),
|
||||
{[Spec2 | Acc], Refs ++ RefsAcc}
|
||||
end, {[], []}, Params),
|
||||
{lists:reverse(SpecList), AllRefs}.
|
||||
|
||||
init_meta(Nullable, Default) ->
|
||||
Init =
|
||||
case Nullable of
|
||||
true -> #{nullable => true};
|
||||
_ -> #{}
|
||||
end,
|
||||
case Default =:= undefined of
|
||||
true -> Init;
|
||||
false -> Init#{default => Default}
|
||||
end.
|
||||
|
||||
init_prop(Keys, Init, Type) ->
|
||||
lists:foldl(fun(Key, Acc) ->
|
||||
case hocon_schema:field_schema(Type, Key) of
|
||||
undefined -> Acc;
|
||||
Schema -> Acc#{Key => to_bin(Schema)}
|
||||
end
|
||||
end, Init, Keys).
|
||||
|
||||
trans_required(Spec, false, _) -> Spec#{required => true};
|
||||
trans_required(Spec, _, path) -> Spec#{required => true};
|
||||
trans_required(Spec, _, _) -> Spec.
|
||||
|
||||
trans_desc(Spec, Hocon) ->
|
||||
case hocon_schema:field_schema(Hocon, desc) of
|
||||
undefined -> Spec;
|
||||
Desc -> Spec#{description => Desc}
|
||||
end.
|
||||
|
||||
requestBody([], _Module) -> {[], []};
|
||||
requestBody(Schema, Module) ->
|
||||
{Props, Refs} =
|
||||
case hoconsc:is_schema(Schema) of
|
||||
true ->
|
||||
HoconSchema = hocon_schema:field_schema(Schema, type),
|
||||
hocon_schema_to_spec(HoconSchema, Module);
|
||||
false -> parse_object(Schema, Module)
|
||||
end,
|
||||
{#{<<"content">> => #{<<"application/json">> => #{<<"schema">> => Props}}},
|
||||
Refs}.
|
||||
|
||||
responses(Responses, Module) ->
|
||||
{Spec, Refs, _} = maps:fold(fun response/3, {#{}, [], Module}, Responses),
|
||||
{Spec, Refs}.
|
||||
|
||||
response(Status, Bin, {Acc, RefsAcc, Module}) when is_binary(Bin) ->
|
||||
{Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module};
|
||||
response(Status, ?REF(StructName), {Acc, RefsAcc, Module}) ->
|
||||
response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module});
|
||||
response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module}) ->
|
||||
{Spec, Refs} = hocon_schema_to_spec(RRef, Module),
|
||||
Content = #{<<"application/json">> => #{<<"schema">> => Spec}},
|
||||
{Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module};
|
||||
response(Status, Schema, {Acc, RefsAcc, Module}) ->
|
||||
case hoconsc:is_schema(Schema) of
|
||||
true ->
|
||||
Hocon = hocon_schema:field_schema(Schema, type),
|
||||
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
|
||||
Init = trans_desc(#{}, Schema),
|
||||
Content = #{<<"application/json">> => #{<<"schema">> => Spec}},
|
||||
{Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}}, Refs ++ RefsAcc, Module};
|
||||
false ->
|
||||
{Props, Refs} = parse_object(Schema, Module),
|
||||
Content = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => Props}}},
|
||||
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module}
|
||||
end.
|
||||
|
||||
components(Refs) ->
|
||||
lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [],
|
||||
components(Refs, #{}, []))).
|
||||
|
||||
components([], SpecAcc, []) -> SpecAcc;
|
||||
components([], SpecAcc, SubRefAcc) -> components(SubRefAcc, SpecAcc, []);
|
||||
components([{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
|
||||
Props = apply(Module, fields, [Field]),
|
||||
Namespace = namespace(Module),
|
||||
{Object, SubRefs} = parse_object(Props, Module),
|
||||
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Object},
|
||||
components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc).
|
||||
|
||||
namespace(Module) ->
|
||||
case hocon_schema:namespace(Module) of
|
||||
undefined -> Module;
|
||||
NameSpace -> NameSpace
|
||||
end.
|
||||
|
||||
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
||||
{#{<<"$ref">> => ?TO_COMPONENTS(Module, StructName)},
|
||||
[{Module, StructName}]};
|
||||
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
|
||||
{#{<<"$ref">> => ?TO_COMPONENTS(LocalModule, StructName)},
|
||||
[{LocalModule, StructName}]};
|
||||
hocon_schema_to_spec(Type, _LocalModule) when ?IS_TYPEREFL(Type) ->
|
||||
{typename_to_spec(typerefl:name(Type)), []};
|
||||
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
||||
{Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
|
||||
{#{type => array, items => Schema}, Refs};
|
||||
hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
||||
{#{type => string, enum => Items}, []};
|
||||
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
|
||||
{OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
|
||||
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
||||
{[Schema | Acc], SubRefs ++ RefsAcc}
|
||||
end, {[], []}, Types),
|
||||
{#{<<"oneOf">> => OneOf}, Refs};
|
||||
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
|
||||
{#{type => string, enum => [Atom]}, []}.
|
||||
|
||||
typename_to_spec("boolean()") -> #{type => boolean, example => true};
|
||||
typename_to_spec("binary()") -> #{type => string, example =><<"binary example">>};
|
||||
typename_to_spec("float()") -> #{type =>number, example =>3.14159};
|
||||
typename_to_spec("integer()") -> #{type =>integer, example =>100};
|
||||
typename_to_spec("number()") -> #{type =>number, example =>42};
|
||||
typename_to_spec("string()") -> #{type =>string, example =><<"string example">>};
|
||||
typename_to_spec("atom()") -> #{type =>string, example =>atom};
|
||||
typename_to_spec("duration()") -> #{type =>string, example =><<"12m">>};
|
||||
typename_to_spec("duration_s()") -> #{type =>string, example =><<"1h">>};
|
||||
typename_to_spec("duration_ms()") -> #{type =>string, example =><<"32s">>};
|
||||
typename_to_spec("percent()") -> #{type =>number, example =><<"12%">>};
|
||||
typename_to_spec("file()") -> #{type =>string, example =><<"/path/to/file">>};
|
||||
typename_to_spec("ip_port()") -> #{type => string, example =><<"127.0.0.1:80">>};
|
||||
typename_to_spec(Name) ->
|
||||
case string:split(Name, "..") of
|
||||
[MinStr, MaxStr] -> %% 1..10
|
||||
{Min, []} = string:to_integer(MinStr),
|
||||
{Max, []} = string:to_integer(MaxStr),
|
||||
#{type => integer, example => Min, minimum => Min, maximum => Max};
|
||||
_ -> %% Module:Type().
|
||||
case string:split(Name, ":") of
|
||||
[_Module, Type] -> typename_to_spec(Type);
|
||||
_ -> throw({error, #{msg => <<"Unsupport Type">>, type => Name}})
|
||||
end
|
||||
end.
|
||||
|
||||
to_bin(List) when is_list(List) -> list_to_binary(List);
|
||||
to_bin(B) when is_boolean(B) -> B;
|
||||
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
|
||||
to_bin(X) -> X.
|
||||
|
||||
parse_object(PropList = [_|_], Module) when is_list(PropList) ->
|
||||
{Props, Required, Refs} =
|
||||
lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
|
||||
NameBin = to_bin(Name),
|
||||
case hoconsc:is_schema(Hocon) of
|
||||
true ->
|
||||
HoconType = hocon_schema:field_schema(Hocon, type),
|
||||
Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
|
||||
Init = trans_desc(Init0, Hocon),
|
||||
{Prop, Refs1} = hocon_schema_to_spec(HoconType, Module),
|
||||
NewRequiredAcc =
|
||||
case is_required(Hocon) of
|
||||
true -> [NameBin | RequiredAcc];
|
||||
false -> RequiredAcc
|
||||
end,
|
||||
{[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc};
|
||||
false ->
|
||||
{SubObject, SubRefs} = parse_object(Hocon, Module),
|
||||
{[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
|
||||
end
|
||||
end, {[], [], []}, PropList),
|
||||
Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)},
|
||||
case Required of
|
||||
[] -> {Object, Refs};
|
||||
_ -> {maps:put(required, Required, Object), Refs}
|
||||
end;
|
||||
parse_object(Other, Module) ->
|
||||
erlang:throw({error,
|
||||
#{msg => <<"Object only supports not empty proplists">>,
|
||||
args => Other, module => Module}}).
|
||||
|
||||
is_required(Hocon) ->
|
||||
hocon_schema:field_schema(Hocon, required) =:= true orelse
|
||||
hocon_schema:field_schema(Hocon, nullable) =:= false.
|
|
@ -0,0 +1,261 @@
|
|||
-module(emqx_swagger_parameter_SUITE).
|
||||
-behaviour(minirest_api).
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
%% API
|
||||
-export([paths/0, api_spec/0, schema/1]).
|
||||
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1]).
|
||||
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
||||
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1]).
|
||||
-export([t_in_path_trans_error/1, t_in_query_trans_error/1, t_in_mix_trans_error/1]).
|
||||
-export([all/0, suite/0, groups/0]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-import(hoconsc, [mk/2]).
|
||||
|
||||
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
|
||||
|
||||
all() -> [{group, spec}, {group, validation}].
|
||||
suite() -> [{timetrap, {minutes, 1}}].
|
||||
groups() -> [
|
||||
{spec, [parallel], [t_api_spec, t_in_path, t_in_query, t_in_mix,
|
||||
t_without_in, t_require, t_nullable, t_method]},
|
||||
{validation, [parallel], [t_in_path_trans, t_in_query_trans, t_in_mix_trans,
|
||||
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
||||
].
|
||||
|
||||
t_in_path(_Config) ->
|
||||
Expect =
|
||||
[#{description => <<"Indicates which sorts of issues to return">>,
|
||||
example => <<"all">>, in => path, name => filter,
|
||||
required => true,
|
||||
schema => #{enum => [assigned, created, mentioned, all], type => string}}
|
||||
],
|
||||
validate("/test/in/:filter", Expect),
|
||||
ok.
|
||||
|
||||
t_in_query(_Config) ->
|
||||
Expect =
|
||||
[#{description => <<"results per page (max 100)">>,
|
||||
example => 1, in => query, name => per_page,
|
||||
schema => #{example => 1, maximum => 100, minimum => 1, type => integer}}],
|
||||
validate("/test/in/query", Expect),
|
||||
ok.
|
||||
|
||||
t_in_mix(_Config) ->
|
||||
Expect =
|
||||
[#{description => <<"Indicates which sorts of issues to return">>,
|
||||
example => <<"all">>,in => query,name => filter,
|
||||
schema => #{enum => [assigned,created,mentioned,all],type => string}},
|
||||
#{description => <<"Indicates the state of the issues to return.">>,
|
||||
example => <<"12m">>,in => path,name => state,required => true,
|
||||
schema => #{example => <<"1h">>,type => string}},
|
||||
#{example => 10,in => query,name => per_page, required => false,
|
||||
schema => #{default => 5,example => 1,maximum => 50,minimum => 1, type => integer}},
|
||||
#{in => query,name => is_admin, schema => #{example => true,type => boolean}},
|
||||
#{in => query,name => timeout,
|
||||
schema => #{<<"oneOf">> => [#{enum => [infinity],type => string},
|
||||
#{example => 30,maximum => 60,minimum => 30, type => integer}]}}],
|
||||
ExpectMeta = #{
|
||||
tags => [tags, good],
|
||||
description => <<"good description">>,
|
||||
summary => <<"good summary">>,
|
||||
security => [],
|
||||
deprecated => true,
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}},
|
||||
GotSpec = validate("/test/in/mix/:state", Expect),
|
||||
?assertEqual(ExpectMeta, maps:without([parameters], maps:get(post, GotSpec))),
|
||||
ok.
|
||||
|
||||
t_without_in(_Config) ->
|
||||
?assertThrow({error, <<"missing in:path/query field in parameters">>},
|
||||
emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in")),
|
||||
ok.
|
||||
|
||||
t_require(_Config) ->
|
||||
ExpectSpec = [#{
|
||||
in => query,name => userid, required => false,
|
||||
schema => #{example => <<"binary example">>, type => string}}],
|
||||
validate("/required/false", ExpectSpec),
|
||||
ok.
|
||||
|
||||
t_nullable(_Config) ->
|
||||
NullableFalse = [#{in => query,name => userid, required => true,
|
||||
schema => #{example => <<"binary example">>, type => string}}],
|
||||
NullableTrue = [#{in => query,name => userid,
|
||||
schema => #{example => <<"binary example">>, type => string,
|
||||
nullable => true}}],
|
||||
validate("/nullable/false", NullableFalse),
|
||||
validate("/nullable/true", NullableTrue),
|
||||
ok.
|
||||
|
||||
t_method(_Config) ->
|
||||
PathOk = "/method/ok",
|
||||
PathError = "/method/error",
|
||||
{test, Spec, []} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk),
|
||||
?assertEqual(lists:sort(?METHODS), lists:sort(maps:keys(Spec))),
|
||||
?assertThrow({error, #{module := ?MODULE, path := PathError, method := bar}},
|
||||
emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError)),
|
||||
ok.
|
||||
|
||||
t_in_path_trans(_Config) ->
|
||||
Path = "/test/in/:filter",
|
||||
Bindings = #{filter => <<"created">>},
|
||||
Expect = {ok,#{bindings => #{filter => created},
|
||||
body => #{}, query_string => #{}}},
|
||||
?assertEqual(Expect, trans_parameters(Path, Bindings, #{})),
|
||||
ok.
|
||||
|
||||
t_in_query_trans(_Config) ->
|
||||
Path = "/test/in/query",
|
||||
Expect = {ok, #{bindings => #{},body => #{},
|
||||
query_string => #{<<"per_page">> => 100}}},
|
||||
?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
|
||||
ok.
|
||||
|
||||
t_in_mix_trans(_Config) ->
|
||||
Path = "/test/in/mix/:state",
|
||||
Bindings = #{
|
||||
state => <<"12m">>,
|
||||
per_page => <<"1">>
|
||||
},
|
||||
Query = #{
|
||||
<<"filter">> => <<"created">>,
|
||||
<<"is_admin">> => true,
|
||||
<<"timeout">> => <<"34">>
|
||||
},
|
||||
Expect = {ok,
|
||||
#{body => #{},
|
||||
bindings => #{state => 720},
|
||||
query_string => #{<<"filter">> => created,<<"is_admin">> => true, <<"per_page">> => 5,<<"timeout">> => 34}}},
|
||||
?assertEqual(Expect, trans_parameters(Path, Bindings, Query)),
|
||||
ok.
|
||||
|
||||
t_in_path_trans_error(_Config) ->
|
||||
Path = "/test/in/:filter",
|
||||
Bindings = #{filter => <<"created1">>},
|
||||
Expect = {400,'BAD_REQUEST', <<"filter : unable_to_convert_to_enum_symbol">>},
|
||||
?assertEqual(Expect, trans_parameters(Path, Bindings, #{})),
|
||||
ok.
|
||||
|
||||
t_in_query_trans_error(_Config) ->
|
||||
Path = "/test/in/query",
|
||||
{400,'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 101}),
|
||||
?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
|
||||
ok.
|
||||
|
||||
t_in_mix_trans_error(_Config) ->
|
||||
Path = "/test/in/mix/:state",
|
||||
Bindings = #{
|
||||
state => <<"1d2m">>,
|
||||
per_page => <<"1">>
|
||||
},
|
||||
Query = #{
|
||||
<<"filter">> => <<"cdreated">>,
|
||||
<<"is_admin">> => true,
|
||||
<<"timeout">> => <<"34">>
|
||||
},
|
||||
Expect = {400,'BAD_REQUEST', <<"filter : unable_to_convert_to_enum_symbol">>},
|
||||
?assertEqual(Expect, trans_parameters(Path, Bindings, Query)),
|
||||
ok.
|
||||
|
||||
t_api_spec(_Config) ->
|
||||
emqx_dashboard_swagger:spec(?MODULE),
|
||||
ok.
|
||||
|
||||
validate(Path, ExpectParams) ->
|
||||
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||
?assertEqual(test, OperationId),
|
||||
Params = maps:get(parameters, maps:get(post, Spec)),
|
||||
?assertEqual(ExpectParams, Params),
|
||||
?assertEqual([], Refs),
|
||||
Spec.
|
||||
|
||||
trans_parameters(Path, Bindings, QueryStr) ->
|
||||
Meta = #{module => ?MODULE, method => post, path => Path},
|
||||
Request = #{bindings => Bindings, query_string => QueryStr, body => #{}},
|
||||
emqx_dashboard_swagger:translate_req(Request, Meta).
|
||||
|
||||
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
||||
|
||||
paths() -> ["/test/in/:filter", "/test/in/query", "/test/in/mix/:state",
|
||||
"/required/false", "/nullable/false", "/nullable/true", "/method/ok"].
|
||||
|
||||
schema("/test/in/:filter") ->
|
||||
#{
|
||||
operationId => test,
|
||||
post => #{
|
||||
parameters => [
|
||||
{filter,
|
||||
mk(hoconsc:enum([assigned, created, mentioned, all]),
|
||||
#{in => path, desc => <<"Indicates which sorts of issues to return">>, example => "all"})}
|
||||
],
|
||||
responses => #{200 => <<"ok">>}
|
||||
}
|
||||
};
|
||||
schema("/test/in/query") ->
|
||||
#{
|
||||
operationId => test,
|
||||
post => #{
|
||||
parameters => [
|
||||
{per_page,
|
||||
mk(range(1, 100),
|
||||
#{in => query, desc => <<"results per page (max 100)">>, example => 1})}
|
||||
],
|
||||
responses => #{200 => <<"ok">>}
|
||||
}
|
||||
};
|
||||
schema("/test/in/mix/:state") ->
|
||||
#{
|
||||
operationId => test,
|
||||
post => #{
|
||||
tags => [tags, good],
|
||||
description => <<"good description">>,
|
||||
summary => <<"good summary">>,
|
||||
security => [],
|
||||
deprecated => true,
|
||||
parameters => [
|
||||
{filter, hoconsc:mk(hoconsc:enum([assigned, created, mentioned, all]),
|
||||
#{in => query, desc => <<"Indicates which sorts of issues to return">>, example => "all"})},
|
||||
{state, mk(emqx_schema:duration_s(),
|
||||
#{in => path, required => true, example => "12m", desc => <<"Indicates the state of the issues to return.">>})},
|
||||
{per_page, mk(range(1, 50),
|
||||
#{in => query, required => false, example => 10, default => 5})},
|
||||
{is_admin, mk(boolean(), #{in => query})},
|
||||
{timeout, mk(hoconsc:union([range(30, 60), infinity]), #{in => query})}
|
||||
],
|
||||
responses => #{200 => <<"ok">>}
|
||||
}
|
||||
};
|
||||
schema("/test/without/in") ->
|
||||
#{
|
||||
operationId => test,
|
||||
post => #{
|
||||
parameters => [
|
||||
{'x-request-id', mk(binary(), #{})}
|
||||
],
|
||||
responses => #{200 => <<"ok">>}
|
||||
}
|
||||
};
|
||||
schema("/required/false") ->
|
||||
to_schema([{'userid', mk(binary(), #{in => query, required => false})}]);
|
||||
schema("/nullable/false") ->
|
||||
to_schema([{'userid', mk(binary(), #{in => query, nullable => false})}]);
|
||||
schema("/nullable/true") ->
|
||||
to_schema([{'userid', mk(binary(), #{in => query, nullable => true})}]);
|
||||
schema("/method/ok") ->
|
||||
Response = #{responses => #{200 => <<"ok">>}},
|
||||
lists:foldl(fun(Method, Acc) -> Acc#{Method => Response} end,
|
||||
#{operationId => test}, ?METHODS);
|
||||
schema("/method/error") ->
|
||||
#{operationId => test, bar => #{200 => <<"ok">>}}.
|
||||
to_schema(Params) ->
|
||||
#{
|
||||
operationId => test,
|
||||
post => #{
|
||||
parameters => Params,
|
||||
responses => #{200 => <<"ok">>}
|
||||
}
|
||||
}.
|
|
@ -0,0 +1,60 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_swagger_remote_schema).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-export([ roots/0, fields/1,namespace/0]).
|
||||
-import(hoconsc, [mk/2]).
|
||||
namespace() -> <<"remote">>.
|
||||
roots() -> ["root"].
|
||||
|
||||
fields("root") ->
|
||||
[
|
||||
{listeners, hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "ref1"),
|
||||
hoconsc:ref(?MODULE, "ref2")]))},
|
||||
{default_username, fun default_username/1},
|
||||
{default_password, fun default_password/1},
|
||||
{sample_interval, mk(emqx_schema:duration_s(), #{default => "10s"})},
|
||||
{token_expired_time, mk(emqx_schema:duration(), #{default => "30m"})}
|
||||
];
|
||||
|
||||
fields("ref1") ->
|
||||
[
|
||||
{"protocol", hoconsc:enum([http, https])},
|
||||
{"port", mk(integer(), #{default => 18083})}
|
||||
];
|
||||
|
||||
fields("ref2") ->
|
||||
[
|
||||
{page, mk(range(1,100), #{desc => <<"good page">>})},
|
||||
{another_ref, hoconsc:ref(?MODULE, "ref3")}
|
||||
];
|
||||
fields("ref3") ->
|
||||
[
|
||||
{ip, mk(emqx_schema:ip_port(), #{desc => <<"IP:Port">>, example => "127.0.0.1:80"})},
|
||||
{version, mk(string(), #{desc => "a good version", example => "1.0.0"})}
|
||||
].
|
||||
|
||||
default_username(type) -> string();
|
||||
default_username(default) -> "admin";
|
||||
default_username(nullable) -> false;
|
||||
default_username(_) -> undefined.
|
||||
|
||||
default_password(type) -> string();
|
||||
default_password(default) -> "public";
|
||||
default_password(nullable) -> false;
|
||||
default_password(_) -> undefined.
|
|
@ -0,0 +1,471 @@
|
|||
-module(emqx_swagger_requestBody_SUITE).
|
||||
|
||||
-behaviour(minirest_api).
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
%% API
|
||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
||||
-export([t_object/1, t_nest_object/1, t_api_spec/1,
|
||||
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
|
||||
t_ref_array_with_key/1, t_ref_array_without_key/1
|
||||
]).
|
||||
-export([
|
||||
t_object_trans/1, t_nest_object_trans/1, t_local_ref_trans/1,
|
||||
t_remote_ref_trans/1, t_nest_ref_trans/1,
|
||||
t_ref_array_with_key_trans/1, t_ref_array_without_key_trans/1,
|
||||
t_ref_trans_error/1, t_object_trans_error/1
|
||||
]).
|
||||
-export([all/0, suite/0, groups/0]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-import(hoconsc, [mk/2]).
|
||||
|
||||
all() -> [{group, spec}, {group, validation}].
|
||||
|
||||
suite() -> [{timetrap, {minutes, 1}}].
|
||||
groups() -> [
|
||||
{spec, [parallel], [
|
||||
t_api_spec, t_object, t_nest_object,
|
||||
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
|
||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]},
|
||||
{validation, [parallel],
|
||||
[
|
||||
t_object_trans, t_local_ref_trans, t_remote_ref_trans,
|
||||
t_ref_array_with_key_trans, t_ref_array_without_key_trans, t_nest_ref_trans,
|
||||
t_ref_trans_error, t_object_trans_error
|
||||
%% t_nest_object_trans,
|
||||
]}
|
||||
].
|
||||
|
||||
t_object(_Config) ->
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> =>
|
||||
#{<<"application/json">> =>
|
||||
#{<<"schema">> =>
|
||||
#{required => [<<"timeout">>, <<"per_page">>],
|
||||
<<"properties">> =>[
|
||||
{<<"per_page">>, #{description => <<"good per page desc">>, example => 1, maximum => 100, minimum => 1, type => integer}},
|
||||
{<<"timeout">>, #{default => 5, <<"oneOf">> => [#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||
{<<"inner_ref">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
|
||||
<<"type">> => object}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{?MODULE, good_ref}],
|
||||
validate("/object", Spec, Refs),
|
||||
ok.
|
||||
|
||||
t_nest_object(_Config) ->
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> => #{<<"application/json">> =>
|
||||
#{<<"schema">> =>
|
||||
#{required => [<<"timeout">>],
|
||||
<<"properties">> =>
|
||||
[{<<"per_page">>, #{description => <<"good per page desc">>, example => 1, maximum => 100, minimum => 1, type => integer}},
|
||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||
{<<"nest_object">>,
|
||||
#{<<"properties">> =>
|
||||
[{<<"good_nest_1">>, #{example => 100, type => integer}},
|
||||
{<<"good_nest_2">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],<<"type">> => object}},
|
||||
{<<"inner_ref">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
|
||||
<<"type">> => object}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{?MODULE, good_ref}],
|
||||
validate("/nest/object", Spec, Refs),
|
||||
ok.
|
||||
|
||||
t_local_ref(_Config) ->
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> => #{<<"application/json">> =>
|
||||
#{<<"schema">> => #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{?MODULE, good_ref}],
|
||||
validate("/ref/local", Spec, Refs),
|
||||
ok.
|
||||
|
||||
t_remote_ref(_Config) ->
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> => #{<<"application/json">> =>
|
||||
#{<<"schema">> => #{<<"$ref">> => <<"#/components/schemas/remote.ref2">>}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{emqx_swagger_remote_schema, "ref2"}],
|
||||
{_, Components} = validate("/ref/remote", Spec, Refs),
|
||||
ExpectComponents = [
|
||||
#{<<"remote.ref2">> => #{<<"properties">> => [
|
||||
{<<"page">>, #{description => <<"good page">>,example => 1, maximum => 100,minimum => 1,type => integer}},
|
||||
{<<"another_ref">>, #{<<"$ref">> => <<"#/components/schemas/remote.ref3">>}}], <<"type">> => object}},
|
||||
#{<<"remote.ref3">> => #{<<"properties">> => [
|
||||
{<<"ip">>, #{description => <<"IP:Port">>, example => <<"127.0.0.1:80">>,type => string}},
|
||||
{<<"version">>, #{description => "a good version", example => <<"1.0.0">>,type => string}}],
|
||||
<<"type">> => object}}],
|
||||
?assertEqual(ExpectComponents, Components),
|
||||
ok.
|
||||
|
||||
t_nest_ref(_Config) ->
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> => #{<<"application/json">> =>
|
||||
#{<<"schema">> => #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.nest_ref">>}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{?MODULE, nest_ref}],
|
||||
ExpectComponents = lists:sort([
|
||||
#{<<"emqx_swagger_requestBody_SUITE.nest_ref">> => #{<<"properties">> => [
|
||||
{<<"env">>, #{enum => [test,dev,prod],type => string}},
|
||||
{<<"another_ref">>, #{description => "nest ref", <<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
|
||||
<<"type">> => object}},
|
||||
#{<<"emqx_swagger_requestBody_SUITE.good_ref">> => #{<<"properties">> => [
|
||||
{<<"webhook-host">>, #{default => <<"127.0.0.1:80">>, example => <<"127.0.0.1:80">>,type => string}},
|
||||
{<<"log_dir">>, #{example => <<"var/log/emqx">>,type => string}},
|
||||
{<<"tag">>, #{description => <<"tag">>, example => <<"binary example">>,type => string}}],
|
||||
<<"type">> => object}}]),
|
||||
{_, Components} = validate("/ref/nest/ref", Spec, Refs),
|
||||
?assertEqual(ExpectComponents, Components),
|
||||
ok.
|
||||
|
||||
t_none_ref(_Config) ->
|
||||
Path = "/ref/none",
|
||||
?assertThrow({error, #{mfa := {?MODULE, schema, [Path]}}},
|
||||
emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path)),
|
||||
ok.
|
||||
|
||||
t_bad_ref(_Config) ->
|
||||
Path = "/ref/bad",
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
|
||||
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.bad_ref">>}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{?MODULE, bad_ref}],
|
||||
Fields = fields(bad_ref),
|
||||
?assertThrow({error, #{msg := <<"Object only supports not empty proplists">>, args := Fields}},
|
||||
validate(Path, Spec, Refs)),
|
||||
ok.
|
||||
|
||||
t_ref_array_with_key(_Config) ->
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> => #{<<"application/json">> =>
|
||||
#{<<"schema">> => #{required => [<<"timeout">>],
|
||||
<<"type">> => object, <<"properties">> =>
|
||||
[
|
||||
{<<"per_page">>, #{description => <<"good per page desc">>, example => 1, maximum => 100, minimum => 1, type => integer}},
|
||||
{<<"timeout">>, #{default => 5, <<"oneOf">> => [#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||
{<<"array_refs">>, #{items => #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}, type => array}}
|
||||
]}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{?MODULE, good_ref}],
|
||||
validate("/ref/array/with/key", Spec, Refs),
|
||||
ok.
|
||||
|
||||
t_ref_array_without_key(_Config) ->
|
||||
Spec = #{
|
||||
post => #{parameters => [],
|
||||
requestBody => #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
|
||||
#{items => #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}, type => array}}}},
|
||||
responses => #{<<"200">> => #{description => <<"ok">>}}}},
|
||||
Refs = [{?MODULE, good_ref}],
|
||||
validate("/ref/array/without/key", Spec, Refs),
|
||||
ok.
|
||||
|
||||
t_api_spec(_Config) ->
|
||||
emqx_dashboard_swagger:spec(?MODULE),
|
||||
ok.
|
||||
|
||||
t_object_trans(_Config) ->
|
||||
Path = "/object",
|
||||
Body = #{
|
||||
<<"per_page">> => 1,
|
||||
<<"timeout">> => <<"infinity">>,
|
||||
<<"inner_ref">> => #{
|
||||
<<"webhook-host">> => <<"127.0.0.1:80">>,
|
||||
<<"log_dir">> => <<"var/log/test">>,
|
||||
<<"tag">> => <<"god_tag">>
|
||||
}
|
||||
},
|
||||
Expect =
|
||||
#{
|
||||
bindings => #{},
|
||||
query_string => #{},
|
||||
body =>
|
||||
#{
|
||||
<<"per_page">> => 1,
|
||||
<<"timeout">> => infinity,
|
||||
<<"inner_ref">> => #{
|
||||
<<"log_dir">> => "var/log/test",
|
||||
<<"tag">> => <<"god_tag">>,
|
||||
<<"webhook-host">> => {{127, 0, 0, 1}, 80}}
|
||||
}
|
||||
},
|
||||
{ok, ActualBody} = trans_requestBody(Path, Body),
|
||||
?assertEqual(Expect, ActualBody),
|
||||
ok.
|
||||
|
||||
t_nest_object_trans(_Config) ->
|
||||
Path = "/nest/object",
|
||||
Body = #{
|
||||
<<"timeout">> => "10m",
|
||||
<<"per_page">> => 10,
|
||||
<<"inner_ref">> => #{
|
||||
<<"webhook-host">> => <<"127.0.0.1:80">>,
|
||||
<<"log_dir">> => <<"var/log/test">>,
|
||||
<<"tag">> => <<"god_tag">>
|
||||
},
|
||||
<<"nest_object">> => #{
|
||||
<<"good_nest_1">> => 1,
|
||||
<<"good_nest_2">> => #{
|
||||
<<"webhook-host">> => <<"127.0.0.1:80">>,
|
||||
<<"log_dir">> => <<"var/log/test">>,
|
||||
<<"tag">> => <<"god_tag">>
|
||||
}
|
||||
}
|
||||
},
|
||||
Expect = #{
|
||||
bindings => #{},
|
||||
query_string => #{},
|
||||
body => #{<<"per_page">> => 10,
|
||||
<<"timeout">> => 600}
|
||||
},
|
||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
||||
?assertEqual(Expect, NewRequest),
|
||||
ok.
|
||||
|
||||
t_local_ref_trans(_Config) ->
|
||||
Path = "/ref/local",
|
||||
Body = #{
|
||||
<<"webhook-host">> => <<"127.0.0.1:80">>,
|
||||
<<"log_dir">> => <<"var/log/test">>,
|
||||
<<"tag">> => <<"A">>
|
||||
},
|
||||
Expect = #{
|
||||
bindings => #{},
|
||||
query_string => #{},
|
||||
body => #{
|
||||
<<"log_dir">> => "var/log/test",
|
||||
<<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
|
||||
}
|
||||
},
|
||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
||||
?assertEqual(Expect, NewRequest),
|
||||
ok.
|
||||
|
||||
t_remote_ref_trans(_Config) ->
|
||||
Path = "/ref/remote",
|
||||
Body = #{
|
||||
<<"page">> => 10,
|
||||
<<"another_ref">> => #{
|
||||
<<"version">> => "2.1.0",
|
||||
<<"ip">> => <<"198.12.2.1:89">>}
|
||||
},
|
||||
Expect = #{
|
||||
bindings => #{},
|
||||
query_string => #{},
|
||||
body => #{
|
||||
<<"page">> => 10,
|
||||
<<"another_ref">> => #{
|
||||
<<"version">> => "2.1.0",
|
||||
<<"ip">> => {{198,12,2,1}, 89}}
|
||||
}
|
||||
},
|
||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
||||
?assertEqual(Expect, NewRequest),
|
||||
ok.
|
||||
|
||||
t_nest_ref_trans(_Config) ->
|
||||
Path = "/ref/nest/ref",
|
||||
Body = #{<<"env">> => <<"prod">>,
|
||||
<<"another_ref">> => #{
|
||||
<<"log_dir">> => "var/log/dev",
|
||||
<<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => "127.0.0.1:80"
|
||||
}},
|
||||
Expect = #{
|
||||
bindings => #{},
|
||||
query_string => #{},
|
||||
body => #{
|
||||
<<"another_ref">> => #{
|
||||
<<"log_dir">> => "var/log/dev", <<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => {{127, 0, 0, 1}, 80}},
|
||||
<<"env">> => prod}
|
||||
},
|
||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
||||
?assertEqual(Expect, NewRequest),
|
||||
ok.
|
||||
|
||||
t_ref_array_with_key_trans(_Config) ->
|
||||
Path = "/ref/array/with/key",
|
||||
Body = #{
|
||||
<<"per_page">> => 100,
|
||||
<<"timeout">> => "100m",
|
||||
<<"array_refs">> => [
|
||||
#{
|
||||
<<"log_dir">> => "var/log/dev",
|
||||
<<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => "127.0.0.1:80"
|
||||
},
|
||||
#{
|
||||
<<"log_dir">> => "var/log/test",
|
||||
<<"tag">> => <<"B">>,
|
||||
<<"webhook-host">> => "127.0.0.1:81"
|
||||
}]
|
||||
},
|
||||
Expect = #{
|
||||
bindings => #{},
|
||||
query_string => #{},
|
||||
body => #{
|
||||
<<"per_page">> => 100,
|
||||
<<"timeout">> => 6000,
|
||||
<<"array_refs">> => [
|
||||
#{
|
||||
<<"log_dir">> => "var/log/dev",
|
||||
<<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
|
||||
},
|
||||
#{
|
||||
<<"log_dir">> => "var/log/test",
|
||||
<<"tag">> => <<"B">>,
|
||||
<<"webhook-host">> => {{127, 0, 0, 1}, 81}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
||||
?assertEqual(Expect, NewRequest),
|
||||
ok.
|
||||
t_ref_array_without_key_trans(_Config) ->
|
||||
Path = "/ref/array/without/key",
|
||||
Body = [#{
|
||||
<<"log_dir">> => "var/log/dev",
|
||||
<<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => "127.0.0.1:80"
|
||||
},
|
||||
#{
|
||||
<<"log_dir">> => "var/log/test",
|
||||
<<"tag">> => <<"B">>,
|
||||
<<"webhook-host">> => "127.0.0.1:81"
|
||||
}],
|
||||
Expect = #{
|
||||
bindings => #{},
|
||||
query_string => #{},
|
||||
body => [
|
||||
#{
|
||||
<<"log_dir">> => "var/log/dev",
|
||||
<<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
|
||||
},
|
||||
#{
|
||||
<<"log_dir">> => "var/log/test",
|
||||
<<"tag">> => <<"B">>,
|
||||
<<"webhook-host">> => {{127, 0, 0, 1}, 81}
|
||||
}]
|
||||
},
|
||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
||||
?assertEqual(Expect, NewRequest),
|
||||
ok.
|
||||
|
||||
t_ref_trans_error(_Config) ->
|
||||
Path = "/ref/nest/ref",
|
||||
Body = #{<<"env">> => <<"prod">>,
|
||||
<<"another_ref">> => #{
|
||||
<<"log_dir">> => "var/log/dev",
|
||||
<<"tag">> => <<"A">>,
|
||||
<<"webhook-host">> => "127.0..0.1:80"
|
||||
}},
|
||||
{400, 'BAD_REQUEST', _} = trans_requestBody(Path, Body),
|
||||
ok.
|
||||
|
||||
t_object_trans_error(_Config) ->
|
||||
Path = "/object",
|
||||
Body = #{
|
||||
<<"per_page">> => 99,
|
||||
<<"timeout">> => <<"infinity">>,
|
||||
<<"inner_ref">> => #{
|
||||
<<"webhook-host">> => <<"127.0.0..1:80">>,
|
||||
<<"log_dir">> => <<"var/log/test">>,
|
||||
<<"tag">> => <<"god_tag">>
|
||||
}
|
||||
},
|
||||
{400, 'BAD_REQUEST', Reason} = trans_requestBody(Path, Body),
|
||||
?assertNotEqual(nomatch, binary:match(Reason, [<<"webhook-host">>])),
|
||||
ok.
|
||||
|
||||
validate(Path, ExpectSpec, ExpectRefs) ->
|
||||
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||
?assertEqual(test, OperationId),
|
||||
?assertEqual(ExpectSpec, Spec),
|
||||
?assertEqual(ExpectRefs, Refs),
|
||||
{Spec, emqx_dashboard_swagger:components(Refs)}.
|
||||
|
||||
trans_requestBody(Path, Body) ->
|
||||
Meta = #{module => ?MODULE, method => post, path => Path},
|
||||
Request = #{bindings => #{}, query_string => #{}, body => Body},
|
||||
emqx_dashboard_swagger:translate_req(Request, Meta).
|
||||
|
||||
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
||||
paths() ->
|
||||
["/object", "/nest/object", "/ref/local", "/ref/nest/ref", "/ref/array/with/key", "/ref/array/without/key"].
|
||||
|
||||
schema("/object") ->
|
||||
to_schema([
|
||||
{per_page, mk(range(1, 100), #{nullable => false, desc => <<"good per page desc">>})},
|
||||
{timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
|
||||
#{default => 5, nullable => false})},
|
||||
{inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
|
||||
]);
|
||||
schema("/nest/object") ->
|
||||
to_schema([
|
||||
{per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
|
||||
{timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
|
||||
#{default => 5, nullable => false})},
|
||||
{nest_object, [
|
||||
{good_nest_1, mk(integer(), #{})},
|
||||
{good_nest_2, mk(hoconsc:ref(?MODULE, good_ref), #{})}
|
||||
]},
|
||||
{inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
|
||||
]);
|
||||
schema("/ref/local") ->
|
||||
to_schema(mk(hoconsc:ref(good_ref), #{}));
|
||||
schema("/ref/remote") ->
|
||||
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "ref2"), #{}));
|
||||
schema("/ref/bad") ->
|
||||
to_schema(mk(hoconsc:ref(?MODULE, bad_ref), #{}));
|
||||
schema("/ref/nest/ref") ->
|
||||
to_schema(mk(hoconsc:ref(?MODULE, nest_ref), #{}));
|
||||
schema("/ref/array/with/key") ->
|
||||
to_schema([
|
||||
{per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
|
||||
{timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
|
||||
#{default => 5, required => true})},
|
||||
{array_refs, mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{})}
|
||||
]);
|
||||
schema("/ref/array/without/key") ->
|
||||
to_schema(mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{})).
|
||||
|
||||
to_schema(Body) ->
|
||||
#{
|
||||
operationId => test,
|
||||
post => #{requestBody => Body, responses => #{200 => <<"ok">>}}
|
||||
}.
|
||||
|
||||
fields(good_ref) ->
|
||||
[
|
||||
{'webhook-host', mk(emqx_schema:ip_port(), #{default => "127.0.0.1:80"})},
|
||||
{log_dir, mk(emqx_schema:file(), #{example => "var/log/emqx"})},
|
||||
{tag, mk(binary(), #{desc => <<"tag">>})}
|
||||
];
|
||||
fields(nest_ref) ->
|
||||
[
|
||||
{env, mk(hoconsc:enum([test, dev, prod]), #{})},
|
||||
{another_ref, mk(hoconsc:ref(good_ref), #{desc => "nest ref"})}
|
||||
];
|
||||
|
||||
fields(bad_ref) -> %% don't support maps
|
||||
#{
|
||||
username => mk(string(), #{}),
|
||||
is_admin => mk(boolean(), #{})
|
||||
}.
|
|
@ -0,0 +1,292 @@
|
|||
-module(emqx_swagger_response_SUITE).
|
||||
|
||||
-behaviour(minirest_api).
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-import(hoconsc, [mk/2]).
|
||||
|
||||
-export([all/0, suite/0, groups/0]).
|
||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
||||
-export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1,
|
||||
t_raw_local_ref/1, t_raw_remote_ref/1, t_hocon_schema_function/1,
|
||||
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
|
||||
t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]).
|
||||
|
||||
all() -> [{group, spec}].
|
||||
suite() -> [{timetrap, {minutes, 1}}].
|
||||
groups() -> [
|
||||
{spec, [parallel], [
|
||||
t_api_spec, t_simple_binary, t_object, t_nest_object,
|
||||
t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
|
||||
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
|
||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
|
||||
].
|
||||
|
||||
t_simple_binary(_config) ->
|
||||
Path = "/simple/bin",
|
||||
ExpectSpec = #{description => <<"binary ok">>},
|
||||
ExpectRefs = [],
|
||||
validate(Path, ExpectSpec, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_object(_config) ->
|
||||
Path = "/object",
|
||||
Object =
|
||||
#{<<"content">> => #{<<"application/json">> =>
|
||||
#{<<"schema">> => #{required => [<<"timeout">>, <<"per_page">>],
|
||||
<<"properties">> => [
|
||||
{<<"per_page">>, #{description => <<"good per page desc">>, example => 1, maximum => 100, minimum => 1, type => integer}},
|
||||
{<<"timeout">>, #{default => 5, <<"oneOf">> => [#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||
{<<"inner_ref">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}],
|
||||
<<"type">> => object}}}},
|
||||
ExpectRefs = [{?MODULE, good_ref}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_nest_object(_Config) ->
|
||||
Path = "/nest/object",
|
||||
Object =
|
||||
#{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
|
||||
#{required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
|
||||
{<<"per_page">>, #{description => <<"good per page desc">>, example => 1, maximum => 100, minimum => 1, type => integer}},
|
||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||
{<<"nest_object">>, #{<<"type">> => object, <<"properties">> => [
|
||||
{<<"good_nest_1">>, #{example => 100, type => integer}},
|
||||
{<<"good_nest_2">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}
|
||||
}]}},
|
||||
{<<"inner_ref">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}]
|
||||
}}}},
|
||||
ExpectRefs = [{?MODULE, good_ref}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_empty(_Config) ->
|
||||
?assertThrow({error,
|
||||
#{msg := <<"Object only supports not empty proplists">>,
|
||||
args := [], module := ?MODULE}}, validate("/empty", error, [])),
|
||||
ok.
|
||||
|
||||
t_raw_local_ref(_Config) ->
|
||||
Path = "/raw/ref/local",
|
||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
|
||||
<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}}},
|
||||
ExpectRefs = [{?MODULE, good_ref}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_raw_remote_ref(_Config) ->
|
||||
Path = "/raw/ref/remote",
|
||||
Object = #{<<"content">> =>
|
||||
#{<<"application/json">> => #{<<"schema">> => #{
|
||||
<<"$ref">> => <<"#/components/schemas/remote.ref1">>}}}},
|
||||
ExpectRefs = [{emqx_swagger_remote_schema, "ref1"}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_local_ref(_Config) ->
|
||||
Path = "/ref/local",
|
||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
|
||||
<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}}},
|
||||
ExpectRefs = [{?MODULE, good_ref}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_remote_ref(_Config) ->
|
||||
Path = "/ref/remote",
|
||||
Object = #{<<"content">> =>
|
||||
#{<<"application/json">> => #{<<"schema">> => #{
|
||||
<<"$ref">> => <<"#/components/schemas/remote.ref1">>}}}},
|
||||
ExpectRefs = [{emqx_swagger_remote_schema, "ref1"}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_bad_ref(_Config) ->
|
||||
Path = "/ref/bad",
|
||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
|
||||
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.bad_ref">>}}}},
|
||||
ExpectRefs = [{?MODULE, bad_ref}],
|
||||
?assertThrow({error, #{module := ?MODULE, msg := <<"Object only supports not empty proplists">>}},
|
||||
validate(Path, Object, ExpectRefs)),
|
||||
ok.
|
||||
|
||||
t_none_ref(_Config) ->
|
||||
Path = "/ref/none",
|
||||
?assertThrow({error, #{mfa := {?MODULE, schema, ["/ref/none"]},
|
||||
reason := function_clause}}, validate(Path, #{}, [])),
|
||||
ok.
|
||||
|
||||
t_nest_ref(_Config) ->
|
||||
Path = "/ref/nest/ref",
|
||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
|
||||
<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.nest_ref">>}}}},
|
||||
ExpectRefs = [{?MODULE, nest_ref}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_ref_array_with_key(_Config) ->
|
||||
Path = "/ref/array/with/key",
|
||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
|
||||
required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
|
||||
{<<"per_page">>, #{description => <<"good per page desc">>, example => 1, maximum => 100, minimum => 1, type => integer}},
|
||||
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
|
||||
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
|
||||
{<<"assert">>, #{description => <<"money">>, example => 3.14159,type => number}},
|
||||
{<<"number_ex">>, #{description => <<"number example">>, example => 42,type => number}},
|
||||
{<<"percent_ex">>, #{description => <<"percent example">>, example => <<"12%">>,type => number}},
|
||||
{<<"duration_ms_ex">>, #{description => <<"duration ms example">>, example => <<"32s">>,type => string}},
|
||||
{<<"atom_ex">>, #{description => <<"atom ex">>, example => atom, type => string}},
|
||||
{<<"array_refs">>, #{items => #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}, type => array}}
|
||||
]}
|
||||
}}},
|
||||
ExpectRefs = [{?MODULE, good_ref}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
|
||||
t_ref_array_without_key(_Config) ->
|
||||
Path = "/ref/array/without/key",
|
||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
|
||||
items => #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>},
|
||||
type => array}}}},
|
||||
ExpectRefs = [{?MODULE, good_ref}],
|
||||
validate(Path, Object, ExpectRefs),
|
||||
ok.
|
||||
t_hocon_schema_function(_Config) ->
|
||||
Path = "/ref/hocon/schema/function",
|
||||
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
|
||||
#{<<"$ref">> => <<"#/components/schemas/remote.root">>}}}},
|
||||
ExpectComponents = [
|
||||
#{<<"remote.ref1">> => #{<<"type">> => object,
|
||||
<<"properties">> => [
|
||||
{<<"protocol">>, #{enum => [http, https], type => string}},
|
||||
{<<"port">>, #{default => 18083, example => 100, type => integer}}]
|
||||
}},
|
||||
#{<<"remote.ref2">> => #{<<"type">> => object,
|
||||
<<"properties">> => [
|
||||
{<<"page">>, #{description => <<"good page">>, example => 1, maximum => 100, minimum => 1, type => integer}},
|
||||
{<<"another_ref">>, #{<<"$ref">> => <<"#/components/schemas/remote.ref3">>}}
|
||||
]
|
||||
}},
|
||||
#{<<"remote.ref3">> => #{<<"type">> => object,
|
||||
<<"properties">> => [
|
||||
{<<"ip">>, #{description => <<"IP:Port">>, example => <<"127.0.0.1:80">>,type => string}},
|
||||
{<<"version">>, #{description => "a good version", example => <<"1.0.0">>, type => string}}]
|
||||
}},
|
||||
#{<<"remote.root">> => #{required => [<<"default_password">>, <<"default_username">>],
|
||||
<<"properties">> => [{<<"listeners">>, #{items =>
|
||||
#{<<"oneOf">> =>
|
||||
[#{<<"$ref">> => <<"#/components/schemas/remote.ref2">>},
|
||||
#{<<"$ref">> => <<"#/components/schemas/remote.ref1">>}]}, type => array}},
|
||||
{<<"default_username">>,
|
||||
#{default => <<"admin">>, example => <<"string example">>, type => string}},
|
||||
{<<"default_password">>, #{default => <<"public">>, example => <<"string example">>, type => string}},
|
||||
{<<"sample_interval">>, #{default => <<"10s">>, example => <<"1h">>, type => string}},
|
||||
{<<"token_expired_time">>, #{default => <<"30m">>, example => <<"12m">>, type => string}}],
|
||||
<<"type">> => object}}],
|
||||
ExpectRefs = [{emqx_swagger_remote_schema, "root"}],
|
||||
{_, Components} = validate(Path, Object, ExpectRefs),
|
||||
?assertEqual(ExpectComponents, Components),
|
||||
ok.
|
||||
|
||||
t_api_spec(_Config) ->
|
||||
emqx_dashboard_swagger:spec(?MODULE),
|
||||
ok.
|
||||
|
||||
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
||||
|
||||
paths() ->
|
||||
["/simple/bin", "/object", "/nest/object", "/ref/local",
|
||||
"/ref/nest/ref", "/raw/ref/local", "/raw/ref/remote",
|
||||
"/ref/array/with/key", "/ref/array/without/key",
|
||||
"/ref/hocon/schema/function"].
|
||||
|
||||
schema("/simple/bin") ->
|
||||
to_schema(<<"binary ok">>);
|
||||
schema("/object") ->
|
||||
Object = [
|
||||
{per_page, mk(range(1, 100), #{nullable => false, desc => <<"good per page desc">>})},
|
||||
{timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
|
||||
#{default => 5, nullable => false})},
|
||||
{inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
|
||||
],
|
||||
to_schema(Object);
|
||||
schema("/nest/object") ->
|
||||
Response = [
|
||||
{per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
|
||||
{timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
|
||||
#{default => 5, nullable => false})},
|
||||
{nest_object, [
|
||||
{good_nest_1, mk(integer(), #{})},
|
||||
{good_nest_2, mk(hoconsc:ref(?MODULE, good_ref), #{})}
|
||||
]},
|
||||
{inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}],
|
||||
to_schema(Response);
|
||||
schema("/empty") ->
|
||||
to_schema([]);
|
||||
schema("/raw/ref/local") ->
|
||||
to_schema(hoconsc:ref(good_ref));
|
||||
schema("/raw/ref/remote") ->
|
||||
to_schema(hoconsc:ref(emqx_swagger_remote_schema, "ref1"));
|
||||
schema("/ref/local") ->
|
||||
to_schema(mk(hoconsc:ref(good_ref), #{}));
|
||||
schema("/ref/remote") ->
|
||||
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "ref1"), #{}));
|
||||
schema("/ref/bad") ->
|
||||
to_schema(mk(hoconsc:ref(?MODULE, bad_ref), #{}));
|
||||
schema("/ref/nest/ref") ->
|
||||
to_schema(mk(hoconsc:ref(?MODULE, nest_ref), #{}));
|
||||
schema("/ref/array/with/key") ->
|
||||
to_schema([
|
||||
{per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
|
||||
{timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
|
||||
#{default => 5, required => true})},
|
||||
{assert, mk(float(), #{desc => <<"money">>})},
|
||||
{number_ex, mk(number(), #{desc => <<"number example">>})},
|
||||
{percent_ex, mk(emqx_schema:percent(), #{desc => <<"percent example">>})},
|
||||
{duration_ms_ex, mk(emqx_schema:duration_ms(), #{desc => <<"duration ms example">>})},
|
||||
{atom_ex, mk(atom(), #{desc => <<"atom ex">>})},
|
||||
{array_refs, mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{})}
|
||||
]);
|
||||
schema("/ref/array/without/key") ->
|
||||
to_schema(mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{}));
|
||||
schema("/ref/hocon/schema/function") ->
|
||||
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "root"), #{})).
|
||||
|
||||
validate(Path, ExpectObject, ExpectRefs) ->
|
||||
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||
?assertEqual(test, OperationId),
|
||||
Response = maps:get(responses, maps:get(post, Spec)),
|
||||
?assertEqual(ExpectObject, maps:get(<<"200">>, Response)),
|
||||
?assertEqual(ExpectObject, maps:get(<<"201">>, Response)),
|
||||
?assertEqual(#{}, maps:without([<<"201">>, <<"200">>], Response)),
|
||||
?assertEqual(ExpectRefs, Refs),
|
||||
{Spec, emqx_dashboard_swagger:components(Refs)}.
|
||||
|
||||
to_schema(Object) ->
|
||||
#{
|
||||
operationId => test,
|
||||
post => #{responses => #{200 => Object, 201 => Object}}
|
||||
}.
|
||||
|
||||
fields(good_ref) ->
|
||||
[
|
||||
{'webhook-host', mk(emqx_schema:ip_port(), #{default => "127.0.0.1:80"})},
|
||||
{log_dir, mk(emqx_schema:file(), #{example => "var/log/emqx"})},
|
||||
{tag, mk(binary(), #{desc => <<"tag">>})}
|
||||
];
|
||||
fields(nest_ref) ->
|
||||
[
|
||||
{env, mk(hoconsc:enum([test, dev, prod]), #{})},
|
||||
{another_ref, mk(hoconsc:ref(good_ref), #{desc => "nest ref"})}
|
||||
];
|
||||
|
||||
fields(bad_ref) -> %% don't support maps
|
||||
#{
|
||||
username => mk(string(), #{}),
|
||||
is_admin => mk(boolean(), #{})
|
||||
}.
|
|
@ -52,7 +52,7 @@
|
|||
, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}}
|
||||
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}}
|
||||
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
|
||||
, {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.3"}}}
|
||||
, {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.4"}}}
|
||||
, {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}}
|
||||
, {replayq, "0.3.3"}
|
||||
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
|
||||
|
@ -61,7 +61,7 @@
|
|||
, {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x
|
||||
, {getopt, "1.0.2"}
|
||||
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}
|
||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.17.1"}}}
|
||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.0"}}}
|
||||
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.1"}}}
|
||||
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
|
||||
, {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}
|
||||
|
|
Loading…
Reference in New Issue