feat: gen hot conf schema.json.
This commit is contained in:
parent
574bbafc9b
commit
334d315838
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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) ->
|
||||||
#{
|
#{
|
||||||
|
|
|
@ -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).
|
||||||
|
|
Loading…
Reference in New Issue