diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index daf9a48a6..64b2a6a8b 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -17,6 +17,7 @@ -compile({no_auto_import, [get/1, get/2]}). -include_lib("emqx/include/logger.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). -export([add_handler/2, remove_handler/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. -spec dump_schema(file:name_all()) -> ok. 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"]), 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 @@ -158,3 +158,159 @@ check_cluster_rpc_result(Result) -> {error, Error} -> %% all MFA return not ok or {ok, term()}. Error 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. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 768c85bb6..f0b67dab8 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -21,21 +21,21 @@ %% API -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([error_codes/1, error_codes/2]). -export([filter_check_request/2, filter_check_request_and_translate_body/2]). -ifdef(TEST). --export([ parse_spec_ref/2 - , components/1 +-export([ parse_spec_ref/3 + , components/2 ]). -endif. -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]). -define(INIT_SCHEMA, #{fields => #{}, translations => #{}, @@ -79,12 +79,12 @@ spec(Module, Options) -> Paths = apply(Module, paths, []), {ApiSpec, AllRefs} = 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), {[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc], Refs ++ AllRefsAcc} end, {[], []}, Paths), - {ApiSpec, components(lists:usort(AllRefs))}. + {ApiSpec, components(lists:usort(AllRefs), Options)}. -spec(namespace() -> hocon_schema:name()). namespace() -> "public". @@ -162,7 +162,7 @@ support_check_schema(#{check_schema := Filter}) when is_function(Filter, 2) -> support_check_schema(_) -> #{filter => undefined}. -parse_spec_ref(Module, Path) -> +parse_spec_ref(Module, Path, Options) -> Schema = try erlang:apply(Module, schema, [Path]) @@ -172,7 +172,7 @@ parse_spec_ref(Module, Path) -> {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), + {Spec, SubRefs} = meta_to_spec(Meta, Module, Options), {Acc#{Method => Spec}, SubRefs ++ RefsAcc} end, {#{}, []}, maps:without(['operationId'], Schema)), @@ -239,10 +239,10 @@ check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false)when is_map( Body. %% tags, description, summary, security, deprecated -meta_to_spec(Meta, Module) -> +meta_to_spec(Meta, Module, Options) -> {Params, Refs1} = parameters(maps:get(parameters, 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), lists:usort(Refs1 ++ Refs2 ++ Refs3) @@ -317,28 +317,29 @@ request_body(Schema, Module) -> HoconSchema = hocon_schema:field_schema(Schema, type), SchemaExamples = hocon_schema:field_schema(Schema, examples), {hocon_schema_to_spec(HoconSchema, Module), SchemaExamples}; - false -> {parse_object(Schema, Module), undefined} + false -> {parse_object(Schema, Module, #{}), undefined} end, {#{<<"content">> => content(Props, Examples)}, Refs}. -responses(Responses, Module) -> - {Spec, Refs, _} = maps:fold(fun response/3, {#{}, [], Module}, Responses), +responses(Responses, Module, Options) -> + {Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses), {Spec, Refs}. -response(Status, Bin, {Acc, RefsAcc, Module}) when is_binary(Bin) -> - {Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module}; +response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) -> + {Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options}; %% Support swagger raw object(file download). %% TODO: multi type response(i.e. Support both 'application/json' and 'plain/text') -response(Status, #{content := _} = Content, {Acc, RefsAcc, Module}) -> - {Acc#{integer_to_binary(Status) => Content}, 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), +response(Status, #{content := _} = Content, {Acc, RefsAcc, Module, Options}) -> + {Acc#{integer_to_binary(Status) => Content}, RefsAcc, Module, Options}; +response(Status, ?REF(StructName), {Acc, RefsAcc, Module, Options}) -> + response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module, Options}); +response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module, Options}) -> + SchemaToSpec = schema_to_spec_func(Options), + {Spec, Refs} = SchemaToSpec(RRef, Module), Content = content(Spec), - {Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module}; -response(Status, Schema, {Acc, RefsAcc, Module}) -> + {Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module, Options}; +response(Status, Schema, {Acc, RefsAcc, Module, Options}) -> case hoconsc:is_schema(Schema) of true -> Hocon = hocon_schema:field_schema(Schema, type), @@ -348,33 +349,34 @@ response(Status, Schema, {Acc, RefsAcc, Module}) -> Content = content(Spec, Examples), { Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}}, - Refs ++ RefsAcc, Module + Refs ++ RefsAcc, Module, Options }; false -> - {Props, Refs} = parse_object(Schema, Module), - Content = #{<<"content">> => content(Props)}, - {Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module} + {Props, Refs} = parse_object(Schema, Module, Options), + Init = trans_desc(#{}, Schema), + Content = Init#{<<"content">> => content(Props)}, + {Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options} end. -components(Refs) -> +components(Refs, Options) -> lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [], - components(Refs, #{}, []))). + components(Options, Refs, #{}, []))). -components([], SpecAcc, []) -> SpecAcc; -components([], SpecAcc, SubRefAcc) -> components(SubRefAcc, SpecAcc, []); -components([{Module, Field} | Refs], SpecAcc, SubRefsAcc) -> +components(_Options, [], SpecAcc, []) -> SpecAcc; +components(Options, [], SpecAcc, SubRefAcc) -> components(Options, SubRefAcc, SpecAcc, []); +components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) -> Props = hocon_schema_fields(Module, Field), Namespace = namespace(Module), - {Object, SubRefs} = parse_object(Props, Module), + {Object, SubRefs} = parse_object(Props, Module, Options), 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 -components([{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) -> +components(Options, [{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) -> Props = hocon_schema_fields(Module, Field), {[Param], SubRefs} = parameters(Props, Module), Namespace = namespace(Module), NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param}, - components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc). + components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc). hocon_schema_fields(Module, StructName) -> 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(X) -> X. -parse_object(PropList = [_ | _], Module) when is_list(PropList) -> +parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) -> {Props, Required, Refs} = lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) -> NameBin = to_bin(Name), @@ -555,7 +557,8 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) -> 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), + SchemaToSpec = schema_to_spec_func(Options), + {Prop, Refs1} = SchemaToSpec(HoconType, Module), NewRequiredAcc = case is_required(Hocon) of true -> [NameBin | RequiredAcc]; @@ -563,7 +566,7 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) -> end, {[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc}; false -> - {SubObject, SubRefs} = parse_object(Hocon, Module), + {SubObject, SubRefs} = parse_object(Hocon, Module, Options), {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc} end end, {[], [], []}, PropList), @@ -572,10 +575,10 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) -> [] -> {Object, Refs}; _ -> {maps:put(required, Required, Object), Refs} end; -parse_object(Other, Module) -> +parse_object(Other, Module, Options) -> erlang:throw({error, #{msg => <<"Object only supports not empty proplists">>, - args => Other, module => Module}}). + args => Other, module => Module, options => Options}}). is_required(Hocon) -> 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) -> Ref = #{<<"$ref">> => ?TO_COMPONENTS_PARAM(Mod, StructName)}, {[Ref | Acc], [{Mod, StructName, parameter} | RefsAcc]}. + +schema_to_spec_func(Options) -> + maps:get(schema_to_spec_func, Options, fun hocon_schema_to_spec/2). diff --git a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl index c47c91d9c..1ae2e2e1f 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl @@ -50,8 +50,8 @@ t_ref(_Config) -> LocalPath = "/test/in/ref/local", Path = "/test/in/ref", 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, LocalPath), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, LocalPath, #{}), ?assertEqual(test, OperationId), Params = maps:get(parameters, maps:get(post, Spec)), ?assertEqual(Expect, Params), @@ -64,7 +64,7 @@ t_public_ref(_Config) -> #{<<"$ref">> => <<"#/components/parameters/public.page">>}, #{<<"$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), Params = maps:get(parameters, maps:get(post, Spec)), ?assertEqual(Expect, Params), @@ -80,7 +80,7 @@ t_public_ref(_Config) -> #{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>, example => 1,in => query,name => page, schema => #{default => 1,example => 100,type => integer}}}], - ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs)), + ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs,#{})), ok. t_in_mix(_Config) -> @@ -110,7 +110,7 @@ t_in_mix(_Config) -> t_without_in(_Config) -> ?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. t_require(_Config) -> @@ -133,10 +133,10 @@ t_nullable(_Config) -> t_method(_Config) -> PathOk = "/method/ok", 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))), ?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. t_in_path_trans(_Config) -> @@ -242,7 +242,7 @@ assert_all_filters_equal(Spec, Filter) -> Spec). 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), Params = maps:get(parameters, maps:get(post, Spec)), ?assertEqual(ExpectParams, Params), diff --git a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl index 84cac6229..1c155fde9 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl @@ -151,7 +151,7 @@ t_nest_ref(_Config) -> t_none_ref(_Config) -> Path = "/ref/none", ?assertThrow({error, #{mfa := {?MODULE, schema, [Path]}}}, - emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path)), + emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{})), ok. t_sub_fields(_Config) -> @@ -472,11 +472,11 @@ t_object_trans_error(_Config) -> ok. 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(ExpectSpec, Spec), ?assertEqual(ExpectRefs, Refs), - {Spec, emqx_dashboard_swagger:components(Refs)}. + {Spec, emqx_dashboard_swagger:components(Refs, #{})}. filter(ApiSpec, Path) -> diff --git a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl index be27b954e..ca6ea97cc 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl @@ -69,7 +69,7 @@ t_error(_Config) -> {<<"message">>, #{description => <<"Details description of the error.">>, 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), Response = maps:get(responses, maps:get(get, Spec)), ?assertEqual(Error400, maps:get(<<"400">>, Response)), @@ -197,7 +197,7 @@ t_complicated_type(_Config) -> {<<"fix_integer">>, #{default => 100, enum => [100], example => 100,type => integer}} ], <<"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), Response = maps:get(responses, maps:get(post, Spec)), ?assertEqual(Object, maps:get(<<"200">>, Response)), @@ -379,14 +379,14 @@ schema("/fields/sub") -> to_schema(hoconsc:ref(sub_fields)). 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), 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)}. + {Spec, emqx_dashboard_swagger:components(Refs, #{})}. to_schema(Object) -> #{ diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 75496b685..827f6e913 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -19,7 +19,7 @@ -include_lib("hocon/include/hoconsc.hrl"). -behaviour(minirest_api). --export([api_spec/0]). +-export([api_spec/0, namespace/0]). -export([paths/0, schema/1, fields/1]). -export([config/3, config_reset/3, configs/3, get_full_config/0]). @@ -29,15 +29,31 @@ -define(PREFIX, "/configs/"). -define(PREFIX_RESET, "/configs_reset/"). -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() -> emqx_dashboard_swagger:spec(?MODULE). +namespace() -> "configuration". + paths() -> ["/configs", "/configs_reset/:rootname"] ++ lists:map(fun({Name, _Type}) -> ?PREFIX ++ to_list(Name) end, config_list(?EXCLUDES)). + schema("/configs") -> #{ 'operationId' => configs, @@ -189,7 +205,7 @@ conf_path_from_querystr(Req) -> config_list(Exclude) -> 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(Atom) when is_atom(Atom) -> atom_to_list(Atom).