feat: gen hot conf schema.json.

This commit is contained in:
Zhongwen Deng 2022-02-24 15:35:40 +08:00
parent 574bbafc9b
commit 334d315838
6 changed files with 243 additions and 65 deletions

View File

@ -17,6 +17,7 @@
-compile({no_auto_import, [get/1, get/2]}). -compile({no_auto_import, [get/1, get/2]}).
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-export([add_handler/2, remove_handler/1]). -export([add_handler/2, remove_handler/1]).
-export([get/1, get/2, get_raw/2, get_all/1]). -export([get/1, get/2, get_raw/2, get_all/1]).
@ -124,14 +125,13 @@ reset(Node, KeyPath, Opts) ->
%% @doc Called from build script. %% @doc Called from build script.
-spec dump_schema(file:name_all()) -> ok. -spec dump_schema(file:name_all()) -> ok.
dump_schema(Dir) -> dump_schema(Dir) ->
SchemaJsonFile = filename:join([Dir, "schema.json"]),
JsonMap = hocon_schema_json:gen(emqx_conf_schema),
IoData = jsx:encode(JsonMap, [space, {indent, 4}]),
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
ok = file:write_file(SchemaJsonFile, IoData),
SchemaMarkdownFile = filename:join([Dir, "config.md"]), SchemaMarkdownFile = filename:join([Dir, "config.md"]),
io:format(user, "===< Generating: ~s~n", [SchemaMarkdownFile ]), io:format(user, "===< Generating: ~s~n", [SchemaMarkdownFile ]),
ok = gen_doc(SchemaMarkdownFile). ok = gen_doc(SchemaMarkdownFile),
SchemaJsonFile = filename:join([Dir, "schema.json"]),
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
ok = gen_hot_conf_schema(SchemaJsonFile),
ok.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
@ -158,3 +158,159 @@ check_cluster_rpc_result(Result) ->
{error, Error} -> %% all MFA return not ok or {ok, term()}. {error, Error} -> %% all MFA return not ok or {ok, term()}.
Error Error
end. end.
%% Only gen hot_conf schema, not all configuration fields.
gen_hot_conf_schema(File) ->
{ApiSpec0, Components0} = emqx_dashboard_swagger:spec(emqx_mgmt_api_configs,
#{schema_to_spec_func => fun hocon_schema_to_spec/2}),
ApiSpec = lists:foldl(fun({Path, Spec, _, _}, Acc) ->
NewSpec = maps:fold(fun(Method, #{responses := Responses}, SubAcc) ->
case Responses of
#{<<"200">> :=
#{<<"content">> := #{<<"application/json">> := #{<<"schema">> := Schema}}}} ->
SubAcc#{Method => Schema};
_ -> SubAcc
end
end, #{}, Spec),
Acc#{list_to_atom(Path) => NewSpec} end, #{}, ApiSpec0),
Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
IoData = jsx:encode(#{
info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
paths => ApiSpec,
components => #{schemas => Components}
}, [space, {indent, 4}]),
file:write_file(File, IoData).
-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_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>,
?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)])).
-define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>,
?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)])).
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
[{Module, StructName}]};
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
[{LocalModule, StructName}]};
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
{typename_to_spec(typerefl:name(Type), LocalModule), []};
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
{Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
{#{type => array, items => Schema}, Refs};
hocon_schema_to_spec(?LAZY(Item), LocalModule) ->
hocon_schema_to_spec(Item, LocalModule);
hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
{#{type => enum, symbols => Items}, []};
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
{#{<<"type">> => object,
<<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
SubRefs};
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 => enum, symbols => [Atom]}, []}.
typename_to_spec("user_id_type()", _Mod) -> #{type => enum, symbols => [clientid, username]};
typename_to_spec("term()", _Mod) -> #{type => string};
typename_to_spec("boolean()", _Mod) -> #{type => boolean};
typename_to_spec("binary()", _Mod) -> #{type => string};
typename_to_spec("float()", _Mod) -> #{type => number};
typename_to_spec("integer()", _Mod) -> #{type => integer};
typename_to_spec("non_neg_integer()", _Mod) -> #{type => integer, minimum => 1};
typename_to_spec("number()", _Mod) -> #{type => number};
typename_to_spec("string()", _Mod) -> #{type => string};
typename_to_spec("atom()", _Mod) -> #{type => string};
typename_to_spec("duration()", _Mod) -> #{type => duration};
typename_to_spec("duration_s()", _Mod) -> #{type => duration};
typename_to_spec("duration_ms()", _Mod) -> #{type => duration};
typename_to_spec("percent()", _Mod) -> #{type => percent};
typename_to_spec("file()", _Mod) -> #{type => file};
typename_to_spec("ip_port()", _Mod) -> #{type => ip_port};
typename_to_spec("url()", _Mod) -> #{type => url};
typename_to_spec("bytesize()", _Mod) -> #{type => byteSize};
typename_to_spec("wordsize()", _Mod) -> #{type => byteSize};
typename_to_spec("qos()", _Mod) -> #{type => enum, symbols => [0, 1, 2]};
typename_to_spec("comma_separated_list()", _Mod) -> #{type => comma_separated_string};
typename_to_spec("comma_separated_atoms()", _Mod) -> #{type => comma_separated_string};
typename_to_spec("pool_type()", _Mod) -> #{type => enum, symbols => [random, hash]};
typename_to_spec("log_level()", _Mod) ->
#{type => enum, symbols => [debug, info, notice, warning, error, critical, alert, emergency, all]};
typename_to_spec("rate()", _Mod) -> #{type => string};
typename_to_spec("capacity()", _Mod) -> #{type => string};
typename_to_spec("burst_rate()", _Mod) -> #{type => string};
typename_to_spec("failure_strategy()", _Mod) -> #{type => enum, symbols => [force, drop, throw]};
typename_to_spec("initial()", _Mod) -> #{type => string};
typename_to_spec(Name, Mod) ->
Spec = range(Name),
Spec1 = remote_module_type(Spec, Name, Mod),
Spec2 = typerefl_array(Spec1, Name, Mod),
Spec3 = integer(Spec2, Name),
default_type(Spec3).
default_type(nomatch) -> #{type => string};
default_type(Type) -> Type.
range(Name) ->
case string:split(Name, "..") of
[MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
Schema = #{type => integer},
Schema1 = add_integer_prop(Schema, minimum, MinStr),
add_integer_prop(Schema1, maximum, MaxStr);
_ -> nomatch
end.
%% Module:Type
remote_module_type(nomatch, Name, Mod) ->
case string:split(Name, ":") of
[_Module, Type] -> typename_to_spec(Type, Mod);
_ -> nomatch
end;
remote_module_type(Spec, _Name, _Mod) -> Spec.
%% [string()] or [integer()] or [xxx].
typerefl_array(nomatch, Name, Mod) ->
case string:trim(Name, leading, "[") of
Name -> nomatch;
Name1 ->
case string:trim(Name1, trailing, "]") of
Name1 -> notmatch;
Name2 ->
Schema = typename_to_spec(Name2, Mod),
#{type => array, items => Schema}
end
end;
typerefl_array(Spec, _Name, _Mod) -> Spec.
%% integer(1)
integer(nomatch, Name) ->
case string:to_integer(Name) of
{Int, []} -> #{type => enum, symbols => [Int], default => Int};
_ -> nomatch
end;
integer(Spec, _Name) -> Spec.
add_integer_prop(Schema, Key, Value) ->
case string:to_integer(Value) of
{error, no_integer} -> Schema;
{Int, []}when Key =:= minimum -> Schema#{Key => Int};
{Int, []} -> Schema#{Key => Int}
end.
to_bin(List) when is_list(List) ->
case io_lib:printable_list(List) of
true -> unicode:characters_to_binary(List);
false -> List
end;
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
to_bin(X) -> X.

View File

@ -21,21 +21,21 @@
%% API %% API
-export([spec/1, spec/2]). -export([spec/1, spec/2]).
-export([namespace/0, fields/1]). -export([namespace/0, namespace/1, fields/1]).
-export([schema_with_example/2, schema_with_examples/2]). -export([schema_with_example/2, schema_with_examples/2]).
-export([error_codes/1, error_codes/2]). -export([error_codes/1, error_codes/2]).
-export([filter_check_request/2, filter_check_request_and_translate_body/2]). -export([filter_check_request/2, filter_check_request_and_translate_body/2]).
-ifdef(TEST). -ifdef(TEST).
-export([ parse_spec_ref/2 -export([ parse_spec_ref/3
, components/1 , components/2
]). ]).
-endif. -endif.
-define(METHODS, [get, post, put, head, delete, patch, options, trace]). -define(METHODS, [get, post, put, head, delete, patch, options, trace]).
-define(DEFAULT_FIELDS, [example, allowReserved, style, format, -define(DEFAULT_FIELDS, [example, allowReserved, style, format, readOnly,
explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]). explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
-define(INIT_SCHEMA, #{fields => #{}, translations => #{}, -define(INIT_SCHEMA, #{fields => #{}, translations => #{},
@ -79,12 +79,12 @@ spec(Module, Options) ->
Paths = apply(Module, paths, []), Paths = apply(Module, paths, []),
{ApiSpec, AllRefs} = {ApiSpec, AllRefs} =
lists:foldl(fun(Path, {AllAcc, AllRefsAcc}) -> lists:foldl(fun(Path, {AllAcc, AllRefsAcc}) ->
{OperationId, Specs, Refs} = parse_spec_ref(Module, Path), {OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options),
CheckSchema = support_check_schema(Options), CheckSchema = support_check_schema(Options),
{[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc], {[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
Refs ++ AllRefsAcc} Refs ++ AllRefsAcc}
end, {[], []}, Paths), end, {[], []}, Paths),
{ApiSpec, components(lists:usort(AllRefs))}. {ApiSpec, components(lists:usort(AllRefs), Options)}.
-spec(namespace() -> hocon_schema:name()). -spec(namespace() -> hocon_schema:name()).
namespace() -> "public". namespace() -> "public".
@ -162,7 +162,7 @@ support_check_schema(#{check_schema := Filter}) when is_function(Filter, 2) ->
support_check_schema(_) -> support_check_schema(_) ->
#{filter => undefined}. #{filter => undefined}.
parse_spec_ref(Module, Path) -> parse_spec_ref(Module, Path, Options) ->
Schema = Schema =
try try
erlang:apply(Module, schema, [Path]) erlang:apply(Module, schema, [Path])
@ -172,7 +172,7 @@ parse_spec_ref(Module, Path) ->
{Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) -> {Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) ->
(not lists:member(Method, ?METHODS)) (not lists:member(Method, ?METHODS))
andalso throw({error, #{module => Module, path => Path, method => Method}}), andalso throw({error, #{module => Module, path => Path, method => Method}}),
{Spec, SubRefs} = meta_to_spec(Meta, Module), {Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
{Acc#{Method => Spec}, SubRefs ++ RefsAcc} {Acc#{Method => Spec}, SubRefs ++ RefsAcc}
end, {#{}, []}, end, {#{}, []},
maps:without(['operationId'], Schema)), maps:without(['operationId'], Schema)),
@ -239,10 +239,10 @@ check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false)when is_map(
Body. Body.
%% tags, description, summary, security, deprecated %% tags, description, summary, security, deprecated
meta_to_spec(Meta, Module) -> meta_to_spec(Meta, Module, Options) ->
{Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module), {Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module),
{RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module), {RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module),
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module), {Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
{ {
to_spec(Meta, Params, RequestBody, Responses), to_spec(Meta, Params, RequestBody, Responses),
lists:usort(Refs1 ++ Refs2 ++ Refs3) lists:usort(Refs1 ++ Refs2 ++ Refs3)
@ -317,28 +317,29 @@ request_body(Schema, Module) ->
HoconSchema = hocon_schema:field_schema(Schema, type), HoconSchema = hocon_schema:field_schema(Schema, type),
SchemaExamples = hocon_schema:field_schema(Schema, examples), SchemaExamples = hocon_schema:field_schema(Schema, examples),
{hocon_schema_to_spec(HoconSchema, Module), SchemaExamples}; {hocon_schema_to_spec(HoconSchema, Module), SchemaExamples};
false -> {parse_object(Schema, Module), undefined} false -> {parse_object(Schema, Module, #{}), undefined}
end, end,
{#{<<"content">> => content(Props, Examples)}, {#{<<"content">> => content(Props, Examples)},
Refs}. Refs}.
responses(Responses, Module) -> responses(Responses, Module, Options) ->
{Spec, Refs, _} = maps:fold(fun response/3, {#{}, [], Module}, Responses), {Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses),
{Spec, Refs}. {Spec, Refs}.
response(Status, Bin, {Acc, RefsAcc, Module}) when is_binary(Bin) -> response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) ->
{Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module}; {Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options};
%% Support swagger raw object(file download). %% Support swagger raw object(file download).
%% TODO: multi type response(i.e. Support both 'application/json' and 'plain/text') %% TODO: multi type response(i.e. Support both 'application/json' and 'plain/text')
response(Status, #{content := _} = Content, {Acc, RefsAcc, Module}) -> response(Status, #{content := _} = Content, {Acc, RefsAcc, Module, Options}) ->
{Acc#{integer_to_binary(Status) => Content}, RefsAcc, Module}; {Acc#{integer_to_binary(Status) => Content}, RefsAcc, Module, Options};
response(Status, ?REF(StructName), {Acc, RefsAcc, Module}) -> response(Status, ?REF(StructName), {Acc, RefsAcc, Module, Options}) ->
response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module}); response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module, Options});
response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module}) -> response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module, Options}) ->
{Spec, Refs} = hocon_schema_to_spec(RRef, Module), SchemaToSpec = schema_to_spec_func(Options),
{Spec, Refs} = SchemaToSpec(RRef, Module),
Content = content(Spec), Content = content(Spec),
{Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module}; {Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module, Options};
response(Status, Schema, {Acc, RefsAcc, Module}) -> response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
case hoconsc:is_schema(Schema) of case hoconsc:is_schema(Schema) of
true -> true ->
Hocon = hocon_schema:field_schema(Schema, type), Hocon = hocon_schema:field_schema(Schema, type),
@ -348,33 +349,34 @@ response(Status, Schema, {Acc, RefsAcc, Module}) ->
Content = content(Spec, Examples), Content = content(Spec, Examples),
{ {
Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}}, Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}},
Refs ++ RefsAcc, Module Refs ++ RefsAcc, Module, Options
}; };
false -> false ->
{Props, Refs} = parse_object(Schema, Module), {Props, Refs} = parse_object(Schema, Module, Options),
Content = #{<<"content">> => content(Props)}, Init = trans_desc(#{}, Schema),
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module} Content = Init#{<<"content">> => content(Props)},
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
end. end.
components(Refs) -> components(Refs, Options) ->
lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [], lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [],
components(Refs, #{}, []))). components(Options, Refs, #{}, []))).
components([], SpecAcc, []) -> SpecAcc; components(_Options, [], SpecAcc, []) -> SpecAcc;
components([], SpecAcc, SubRefAcc) -> components(SubRefAcc, SpecAcc, []); components(Options, [], SpecAcc, SubRefAcc) -> components(Options, SubRefAcc, SpecAcc, []);
components([{Module, Field} | Refs], SpecAcc, SubRefsAcc) -> components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
Props = hocon_schema_fields(Module, Field), Props = hocon_schema_fields(Module, Field),
Namespace = namespace(Module), Namespace = namespace(Module),
{Object, SubRefs} = parse_object(Props, Module), {Object, SubRefs} = parse_object(Props, Module, Options),
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Object}, NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Object},
components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc); components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc);
%% parameters in ref only have one value, not array %% parameters in ref only have one value, not array
components([{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) -> components(Options, [{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) ->
Props = hocon_schema_fields(Module, Field), Props = hocon_schema_fields(Module, Field),
{[Param], SubRefs} = parameters(Props, Module), {[Param], SubRefs} = parameters(Props, Module),
Namespace = namespace(Module), Namespace = namespace(Module),
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param}, NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param},
components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc). components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc).
hocon_schema_fields(Module, StructName) -> hocon_schema_fields(Module, StructName) ->
case apply(Module, fields, [StructName]) of case apply(Module, fields, [StructName]) of
@ -546,7 +548,7 @@ to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
to_bin(X) -> X. to_bin(X) -> X.
parse_object(PropList = [_ | _], Module) when is_list(PropList) -> parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) ->
{Props, Required, Refs} = {Props, Required, Refs} =
lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) -> lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
NameBin = to_bin(Name), NameBin = to_bin(Name),
@ -555,7 +557,8 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) ->
HoconType = hocon_schema:field_schema(Hocon, type), HoconType = hocon_schema:field_schema(Hocon, type),
Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon), Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
Init = trans_desc(Init0, Hocon), Init = trans_desc(Init0, Hocon),
{Prop, Refs1} = hocon_schema_to_spec(HoconType, Module), SchemaToSpec = schema_to_spec_func(Options),
{Prop, Refs1} = SchemaToSpec(HoconType, Module),
NewRequiredAcc = NewRequiredAcc =
case is_required(Hocon) of case is_required(Hocon) of
true -> [NameBin | RequiredAcc]; true -> [NameBin | RequiredAcc];
@ -563,7 +566,7 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) ->
end, end,
{[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc}; {[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc};
false -> false ->
{SubObject, SubRefs} = parse_object(Hocon, Module), {SubObject, SubRefs} = parse_object(Hocon, Module, Options),
{[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc} {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
end end
end, {[], [], []}, PropList), end, {[], [], []}, PropList),
@ -572,10 +575,10 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) ->
[] -> {Object, Refs}; [] -> {Object, Refs};
_ -> {maps:put(required, Required, Object), Refs} _ -> {maps:put(required, Required, Object), Refs}
end; end;
parse_object(Other, Module) -> parse_object(Other, Module, Options) ->
erlang:throw({error, erlang:throw({error,
#{msg => <<"Object only supports not empty proplists">>, #{msg => <<"Object only supports not empty proplists">>,
args => Other, module => Module}}). args => Other, module => Module, options => Options}}).
is_required(Hocon) -> is_required(Hocon) ->
hocon_schema:field_schema(Hocon, required) =:= true orelse hocon_schema:field_schema(Hocon, required) =:= true orelse
@ -592,3 +595,6 @@ content(ApiSpec, Examples) when is_map(Examples) ->
to_ref(Mod, StructName, Acc, RefsAcc) -> to_ref(Mod, StructName, Acc, RefsAcc) ->
Ref = #{<<"$ref">> => ?TO_COMPONENTS_PARAM(Mod, StructName)}, Ref = #{<<"$ref">> => ?TO_COMPONENTS_PARAM(Mod, StructName)},
{[Ref | Acc], [{Mod, StructName, parameter} | RefsAcc]}. {[Ref | Acc], [{Mod, StructName, parameter} | RefsAcc]}.
schema_to_spec_func(Options) ->
maps:get(schema_to_spec_func, Options, fun hocon_schema_to_spec/2).

View File

@ -50,8 +50,8 @@ t_ref(_Config) ->
LocalPath = "/test/in/ref/local", LocalPath = "/test/in/ref/local",
Path = "/test/in/ref", Path = "/test/in/ref",
Expect = [#{<<"$ref">> => <<"#/components/parameters/emqx_swagger_parameter_SUITE.page">>}], Expect = [#{<<"$ref">> => <<"#/components/parameters/emqx_swagger_parameter_SUITE.page">>}],
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, LocalPath), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, LocalPath, #{}),
?assertEqual(test, OperationId), ?assertEqual(test, OperationId),
Params = maps:get(parameters, maps:get(post, Spec)), Params = maps:get(parameters, maps:get(post, Spec)),
?assertEqual(Expect, Params), ?assertEqual(Expect, Params),
@ -64,7 +64,7 @@ t_public_ref(_Config) ->
#{<<"$ref">> => <<"#/components/parameters/public.page">>}, #{<<"$ref">> => <<"#/components/parameters/public.page">>},
#{<<"$ref">> => <<"#/components/parameters/public.limit">>} #{<<"$ref">> => <<"#/components/parameters/public.limit">>}
], ],
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId), ?assertEqual(test, OperationId),
Params = maps:get(parameters, maps:get(post, Spec)), Params = maps:get(parameters, maps:get(post, Spec)),
?assertEqual(Expect, Params), ?assertEqual(Expect, Params),
@ -80,7 +80,7 @@ t_public_ref(_Config) ->
#{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>, #{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>,
example => 1,in => query,name => page, example => 1,in => query,name => page,
schema => #{default => 1,example => 100,type => integer}}}], schema => #{default => 1,example => 100,type => integer}}}],
?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs)), ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs,#{})),
ok. ok.
t_in_mix(_Config) -> t_in_mix(_Config) ->
@ -110,7 +110,7 @@ t_in_mix(_Config) ->
t_without_in(_Config) -> t_without_in(_Config) ->
?assertThrow({error, <<"missing in:path/query field in parameters">>}, ?assertThrow({error, <<"missing in:path/query field in parameters">>},
emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in")), emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in", #{})),
ok. ok.
t_require(_Config) -> t_require(_Config) ->
@ -133,10 +133,10 @@ t_nullable(_Config) ->
t_method(_Config) -> t_method(_Config) ->
PathOk = "/method/ok", PathOk = "/method/ok",
PathError = "/method/error", PathError = "/method/error",
{test, Spec, []} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk), {test, Spec, []} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk, #{}),
?assertEqual(lists:sort(?METHODS), lists:sort(maps:keys(Spec))), ?assertEqual(lists:sort(?METHODS), lists:sort(maps:keys(Spec))),
?assertThrow({error, #{module := ?MODULE, path := PathError, method := bar}}, ?assertThrow({error, #{module := ?MODULE, path := PathError, method := bar}},
emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError)), emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError, #{})),
ok. ok.
t_in_path_trans(_Config) -> t_in_path_trans(_Config) ->
@ -242,7 +242,7 @@ assert_all_filters_equal(Spec, Filter) ->
Spec). Spec).
validate(Path, ExpectParams) -> validate(Path, ExpectParams) ->
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId), ?assertEqual(test, OperationId),
Params = maps:get(parameters, maps:get(post, Spec)), Params = maps:get(parameters, maps:get(post, Spec)),
?assertEqual(ExpectParams, Params), ?assertEqual(ExpectParams, Params),

View File

@ -151,7 +151,7 @@ t_nest_ref(_Config) ->
t_none_ref(_Config) -> t_none_ref(_Config) ->
Path = "/ref/none", Path = "/ref/none",
?assertThrow({error, #{mfa := {?MODULE, schema, [Path]}}}, ?assertThrow({error, #{mfa := {?MODULE, schema, [Path]}}},
emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path)), emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{})),
ok. ok.
t_sub_fields(_Config) -> t_sub_fields(_Config) ->
@ -472,11 +472,11 @@ t_object_trans_error(_Config) ->
ok. ok.
validate(Path, ExpectSpec, ExpectRefs) -> validate(Path, ExpectSpec, ExpectRefs) ->
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId), ?assertEqual(test, OperationId),
?assertEqual(ExpectSpec, Spec), ?assertEqual(ExpectSpec, Spec),
?assertEqual(ExpectRefs, Refs), ?assertEqual(ExpectRefs, Refs),
{Spec, emqx_dashboard_swagger:components(Refs)}. {Spec, emqx_dashboard_swagger:components(Refs, #{})}.
filter(ApiSpec, Path) -> filter(ApiSpec, Path) ->

View File

@ -69,7 +69,7 @@ t_error(_Config) ->
{<<"message">>, #{description => <<"Details description of the error.">>, {<<"message">>, #{description => <<"Details description of the error.">>,
example => <<"Error code to troubleshoot problems.">>, type => string}}] example => <<"Error code to troubleshoot problems.">>, type => string}}]
}}}}, }}}},
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId), ?assertEqual(test, OperationId),
Response = maps:get(responses, maps:get(get, Spec)), Response = maps:get(responses, maps:get(get, Spec)),
?assertEqual(Error400, maps:get(<<"400">>, Response)), ?assertEqual(Error400, maps:get(<<"400">>, Response)),
@ -197,7 +197,7 @@ t_complicated_type(_Config) ->
{<<"fix_integer">>, #{default => 100, enum => [100], example => 100,type => integer}} {<<"fix_integer">>, #{default => 100, enum => [100], example => 100,type => integer}}
], ],
<<"type">> => object}}}}, <<"type">> => object}}}},
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId), ?assertEqual(test, OperationId),
Response = maps:get(responses, maps:get(post, Spec)), Response = maps:get(responses, maps:get(post, Spec)),
?assertEqual(Object, maps:get(<<"200">>, Response)), ?assertEqual(Object, maps:get(<<"200">>, Response)),
@ -379,14 +379,14 @@ schema("/fields/sub") ->
to_schema(hoconsc:ref(sub_fields)). to_schema(hoconsc:ref(sub_fields)).
validate(Path, ExpectObject, ExpectRefs) -> validate(Path, ExpectObject, ExpectRefs) ->
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId), ?assertEqual(test, OperationId),
Response = maps:get(responses, maps:get(post, Spec)), Response = maps:get(responses, maps:get(post, Spec)),
?assertEqual(ExpectObject, maps:get(<<"200">>, Response)), ?assertEqual(ExpectObject, maps:get(<<"200">>, Response)),
?assertEqual(ExpectObject, maps:get(<<"201">>, Response)), ?assertEqual(ExpectObject, maps:get(<<"201">>, Response)),
?assertEqual(#{}, maps:without([<<"201">>, <<"200">>], Response)), ?assertEqual(#{}, maps:without([<<"201">>, <<"200">>], Response)),
?assertEqual(ExpectRefs, Refs), ?assertEqual(ExpectRefs, Refs),
{Spec, emqx_dashboard_swagger:components(Refs)}. {Spec, emqx_dashboard_swagger:components(Refs, #{})}.
to_schema(Object) -> to_schema(Object) ->
#{ #{

View File

@ -19,7 +19,7 @@
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-behaviour(minirest_api). -behaviour(minirest_api).
-export([api_spec/0]). -export([api_spec/0, namespace/0]).
-export([paths/0, schema/1, fields/1]). -export([paths/0, schema/1, fields/1]).
-export([config/3, config_reset/3, configs/3, get_full_config/0]). -export([config/3, config_reset/3, configs/3, get_full_config/0]).
@ -29,15 +29,31 @@
-define(PREFIX, "/configs/"). -define(PREFIX, "/configs/").
-define(PREFIX_RESET, "/configs_reset/"). -define(PREFIX_RESET, "/configs_reset/").
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))). -define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
-define(EXCLUDES, [listeners, node, cluster, gateway, rule_engine]). -define(EXCLUDES, [
exhook,
gateway,
plugins,
bridges,
"rule_engine",
"authorization",
"authentication",
"rpc",
"db",
"connectors",
"slow_subs",
"psk_authentication"
]).
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE). emqx_dashboard_swagger:spec(?MODULE).
namespace() -> "configuration".
paths() -> paths() ->
["/configs", "/configs_reset/:rootname"] ++ ["/configs", "/configs_reset/:rootname"] ++
lists:map(fun({Name, _Type}) -> ?PREFIX ++ to_list(Name) end, config_list(?EXCLUDES)). lists:map(fun({Name, _Type}) -> ?PREFIX ++ to_list(Name) end, config_list(?EXCLUDES)).
schema("/configs") -> schema("/configs") ->
#{ #{
'operationId' => configs, 'operationId' => configs,
@ -189,7 +205,7 @@ conf_path_from_querystr(Req) ->
config_list(Exclude) -> config_list(Exclude) ->
Roots = emqx_conf_schema:roots(), Roots = emqx_conf_schema:roots(),
lists:foldl(fun(Key, Acc) -> lists:delete(Key, Acc) end, Roots, Exclude). lists:foldl(fun(Key, Acc) -> lists:keydelete(Key, 1, Acc) end, Roots, Exclude).
to_list(L) when is_list(L) -> L; to_list(L) when is_list(L) -> L;
to_list(Atom) when is_atom(Atom) -> atom_to_list(Atom). to_list(Atom) when is_atom(Atom) -> atom_to_list(Atom).