feat(swagger): define mgmt config_api by hocon schema (#5814)

* feat(swagger): define mgmt config_api by hocon schema

* fix: enum can't defined by integer, use union.

* fix: hocon schema union to enum
This commit is contained in:
zhongwencool 2021-10-13 14:04:43 +08:00 committed by GitHub
parent 3df33da9ac
commit d80f20aca3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 288 additions and 157 deletions

View File

@ -34,10 +34,12 @@ fields("auto_subscribe") ->
fields("topic") -> fields("topic") ->
[ {topic, sc(binary(), #{})} [ {topic, sc(binary(), #{})}
, {qos, sc(typerefl:union([0, 1, 2]), #{default => 0})} , {qos, sc(hoconsc:union([typerefl:integer(0), typerefl:integer(1), typerefl:integer(2)]),
, {rh, sc(typerefl:union([0, 1, 2]), #{default => 0})} #{default => 0})}
, {rap, sc(typerefl:union([0, 1]), #{default => 0})} , {rh, sc(hoconsc:union([typerefl:integer(0), typerefl:integer(1), typerefl:integer(2)]),
, {nl, sc(typerefl:union([0, 1]), #{default => 0})} #{default => 0})}
, {rap, sc(hoconsc:union([typerefl:integer(0), typerefl:integer(1)]), #{default => 0})}
, {nl, sc(hoconsc:union([typerefl:integer(0), typerefl:integer(1)]), #{default => 0})}
]. ].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -79,11 +79,7 @@ fields(config) ->
] ++ emqx_connector_schema_lib:ssl_fields(). ] ++ emqx_connector_schema_lib:ssl_fields().
method() -> method() ->
hoconsc:union([ typerefl:atom(post) hoconsc:enum([post, put, get, delete]).
, typerefl:atom(put)
, typerefl:atom(get)
, typerefl:atom(delete)
]).
validations() -> validations() ->
[ {check_ssl_opts, fun check_ssl_opts/1} ]. [ {check_ssl_opts, fun check_ssl_opts/1} ].

View File

@ -300,10 +300,13 @@ components([{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) ->
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param}, NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param},
components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc). components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc).
%% Semantic error at components.schemas.xxx:xx:xx
%% Component names can only contain the characters A-Z a-z 0-9 - . _
%% So replace ':' by '-'.
namespace(Module) -> namespace(Module) ->
case hocon_schema:namespace(Module) of case hocon_schema:namespace(Module) of
undefined -> Module; undefined -> Module;
NameSpace -> NameSpace NameSpace -> re:replace(to_bin(NameSpace), ":","-",[global])
end. end.
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) -> hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
@ -312,13 +315,20 @@ hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
hocon_schema_to_spec(?REF(StructName), LocalModule) -> hocon_schema_to_spec(?REF(StructName), LocalModule) ->
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
[{LocalModule, StructName}]}; [{LocalModule, StructName}]};
hocon_schema_to_spec(Type, _LocalModule) when ?IS_TYPEREFL(Type) -> hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
{typename_to_spec(typerefl:name(Type)), []}; {typename_to_spec(typerefl:name(Type), LocalModule), []};
hocon_schema_to_spec(?ARRAY(Item), LocalModule) -> hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
{Schema, Refs} = hocon_schema_to_spec(Item, LocalModule), {Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
{#{type => array, items => Schema}, Refs}; {#{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) -> hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
{#{type => string, enum => Items}, []}; {#{type => string, enum => 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) -> hocon_schema_to_spec(?UNION(Types), LocalModule) ->
{OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) -> {OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule), {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
@ -328,33 +338,93 @@ hocon_schema_to_spec(?UNION(Types), LocalModule) ->
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) -> hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
{#{type => string, enum => [Atom]}, []}. {#{type => string, enum => [Atom]}, []}.
typename_to_spec("boolean()") -> #{type => boolean, example => true}; %% todo: Find a way to fetch enum value from user_id_type().
typename_to_spec("binary()") -> #{type => string, example =><<"binary example">>}; typename_to_spec("user_id_type()", _Mod) -> #{type => string, enum => [clientid, username]};
typename_to_spec("float()") -> #{type =>number, example =>3.14159}; typename_to_spec("term()", _Mod) -> #{type => string, example => "term"};
typename_to_spec("integer()") -> #{type =>integer, example =>100}; typename_to_spec("boolean()", _Mod) -> #{type => boolean, example => true};
typename_to_spec("number()") -> #{type =>number, example =>42}; typename_to_spec("binary()", _Mod) -> #{type => string, example => <<"binary-example">>};
typename_to_spec("string()") -> #{type =>string, example =><<"string example">>}; typename_to_spec("float()", _Mod) -> #{type => number, example => 3.14159};
typename_to_spec("atom()") -> #{type =>string, example =>atom}; typename_to_spec("integer()", _Mod) -> #{type => integer, example => 100};
typename_to_spec("duration()") -> #{type =>string, example =><<"12m">>}; typename_to_spec("non_neg_integer()", _Mod) -> #{type => integer, minimum => 1, example => 100};
typename_to_spec("duration_s()") -> #{type =>string, example =><<"1h">>}; typename_to_spec("number()", _Mod) -> #{type => number, example => 42};
typename_to_spec("duration_ms()") -> #{type =>string, example =><<"32s">>}; typename_to_spec("string()", _Mod) -> #{type => string, example => <<"string-example">>};
typename_to_spec("percent()") -> #{type =>number, example =><<"12%">>}; typename_to_spec("atom()", _Mod) -> #{type => string, example => atom};
typename_to_spec("file()") -> #{type =>string, example =><<"/path/to/file">>}; typename_to_spec("duration()", _Mod) -> #{type => string, example => <<"12m">>};
typename_to_spec("ip_port()") -> #{type => string, example =><<"127.0.0.1:80">>}; typename_to_spec("duration_s()", _Mod) -> #{type => string, example => <<"1h">>};
typename_to_spec(Name) -> typename_to_spec("duration_ms()", _Mod) -> #{type => string, example => <<"32s">>};
typename_to_spec("percent()", _Mod) -> #{type => number, example => <<"12%">>};
typename_to_spec("file()", _Mod) -> #{type => string, example => <<"/path/to/file">>};
typename_to_spec("ip_port()", _Mod) -> #{type => string, example => <<"127.0.0.1:80">>};
typename_to_spec("url()", _Mod) -> #{type => string, example => <<"http://127.0.0.1">>};
typename_to_spec("server()", Mod) -> typename_to_spec("ip_port()", Mod);
typename_to_spec("connect_timeout()", Mod) -> typename_to_spec("timeout()", Mod);
typename_to_spec("timeout()", _Mod) -> #{<<"oneOf">> => [#{type => string, example => infinity},
#{type => integer, example => 100}], example => infinity};
typename_to_spec("bytesize()", _Mod) -> #{type => string, example => <<"32MB">>};
typename_to_spec("wordsize()", _Mod) -> #{type => string, example => <<"1024KB">>};
typename_to_spec("map()", _Mod) -> #{type => string, example => <<>>};
typename_to_spec("comma_separated_list()", _Mod) -> #{type => string, example => <<"item1,item2">>};
typename_to_spec("comma_separated_atoms()", _Mod) -> #{type => string, example => <<"item1,item2">>};
typename_to_spec("pool_type()", _Mod) -> #{type => string, enum => [random, hash], example => hash};
typename_to_spec("log_level()", _Mod) ->
#{type => string, enum => [debug, info, notice, warning, error, critical, alert, emergency, all]};
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),
Spec3 =:= nomatch andalso
throw({error, #{msg => <<"Unsupport Type">>, type => Name, module => Mod}}),
Spec3.
range(Name) ->
case string:split(Name, "..") of case string:split(Name, "..") of
[MinStr, MaxStr] -> %% 1..10 [MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
{Min, []} = string:to_integer(MinStr), Schema = #{type => integer},
{Max, []} = string:to_integer(MaxStr), Schema1 = add_integer_prop(Schema, minimum, MinStr),
#{type => integer, example => Min, minimum => Min, maximum => Max}; add_integer_prop(Schema1, maximum, MaxStr);
_ -> %% Module:Type(). _ -> nomatch
case string:split(Name, ":") of
[_Module, Type] -> typename_to_spec(Type);
_ -> throw({error, #{msg => <<"Unsupport Type">>, type => Name}})
end
end. end.
to_bin(List) when is_list(List) -> list_to_binary(List); %% 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 => integer, enum => [Int], example => 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, example => Int};
{Int, []} -> Schema#{Key => Int}
end.
to_bin([Atom | _] = List) when is_atom(Atom) -> iolist_to_binary(io_lib:format("~p", [List]));
to_bin(List) when is_list(List) -> unicode:characters_to_binary(List);
to_bin(B) when is_boolean(B) -> B; to_bin(B) when is_boolean(B) -> B;
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.

View File

@ -112,15 +112,15 @@ t_without_in(_Config) ->
t_require(_Config) -> t_require(_Config) ->
ExpectSpec = [#{ ExpectSpec = [#{
in => query,name => userid, required => false, in => query,name => userid, required => false,
schema => #{example => <<"binary example">>, type => string}}], schema => #{example => <<"binary-example">>, type => string}}],
validate("/required/false", ExpectSpec), validate("/required/false", ExpectSpec),
ok. ok.
t_nullable(_Config) -> t_nullable(_Config) ->
NullableFalse = [#{in => query,name => userid, required => true, NullableFalse = [#{in => query,name => userid, required => true,
schema => #{example => <<"binary example">>, type => string}}], schema => #{example => <<"binary-example">>, type => string}}],
NullableTrue = [#{in => query,name => userid, NullableTrue = [#{in => query,name => userid,
schema => #{example => <<"binary example">>, type => string, schema => #{example => <<"binary-example">>, type => string,
nullable => true}}], nullable => true}}],
validate("/nullable/false", NullableFalse), validate("/nullable/false", NullableFalse),
validate("/nullable/true", NullableTrue), validate("/nullable/true", NullableTrue),

View File

@ -121,7 +121,7 @@ t_nest_ref(_Config) ->
#{<<"emqx_swagger_requestBody_SUITE.good_ref">> => #{<<"properties">> => [ #{<<"emqx_swagger_requestBody_SUITE.good_ref">> => #{<<"properties">> => [
{<<"webhook-host">>, #{default => <<"127.0.0.1:80">>, example => <<"127.0.0.1:80">>,type => string}}, {<<"webhook-host">>, #{default => <<"127.0.0.1:80">>, example => <<"127.0.0.1:80">>,type => string}},
{<<"log_dir">>, #{example => <<"var/log/emqx">>,type => string}}, {<<"log_dir">>, #{example => <<"var/log/emqx">>,type => string}},
{<<"tag">>, #{description => <<"tag">>, example => <<"binary example">>,type => string}}], {<<"tag">>, #{description => <<"tag">>, example => <<"binary-example">>,type => string}}],
<<"type">> => object}}]), <<"type">> => object}}]),
{_, Components} = validate("/ref/nest/ref", Spec, Refs), {_, Components} = validate("/ref/nest/ref", Spec, Refs),
?assertEqual(ExpectComponents, Components), ?assertEqual(ExpectComponents, Components),

View File

@ -13,7 +13,7 @@
-export([all/0, suite/0, groups/0]). -export([all/0, suite/0, groups/0]).
-export([paths/0, api_spec/0, schema/1, fields/1]). -export([paths/0, api_spec/0, schema/1, fields/1]).
-export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1, t_error/1, -export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1, t_error/1,
t_raw_local_ref/1, t_raw_remote_ref/1, t_hocon_schema_function/1, t_raw_local_ref/1, t_raw_remote_ref/1, t_hocon_schema_function/1, t_complicated_type/1,
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1, t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]). t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]).
@ -21,7 +21,7 @@ all() -> [{group, spec}].
suite() -> [{timetrap, {minutes, 1}}]. suite() -> [{timetrap, {minutes, 1}}].
groups() -> [ groups() -> [
{spec, [parallel], [ {spec, [parallel], [
t_api_spec, t_simple_binary, t_object, t_nest_object, t_error, t_api_spec, t_simple_binary, t_object, t_nest_object, t_error, t_complicated_type,
t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function, t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref, t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]} t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
@ -156,6 +156,38 @@ t_nest_ref(_Config) ->
validate(Path, Object, ExpectRefs), validate(Path, Object, ExpectRefs),
ok. ok.
t_complicated_type(_Config) ->
Path = "/ref/complicated_type",
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{<<"properties">> =>
[
{<<"no_neg_integer">>, #{example => 100, minimum => 1, type => integer}},
{<<"url">>, #{example => <<"http://127.0.0.1">>, type => string}},
{<<"server">>, #{example => <<"127.0.0.1:80">>, type => string}},
{<<"connect_timeout">>, #{example => infinity, <<"oneOf">> => [
#{example => infinity, type => string},
#{example => 100, type => integer}]}},
{<<"pool_type">>, #{enum => [random, hash], example => hash, type => string}},
{<<"timeout">>, #{example => infinity,
<<"oneOf">> =>
[#{example => infinity, type => string}, #{example => 100, type => integer}]}},
{<<"bytesize">>, #{example => <<"32MB">>, type => string}},
{<<"wordsize">>, #{example => <<"1024KB">>, type => string}},
{<<"maps">>, #{example => <<>>, type => string}},
{<<"comma_separated_list">>, #{example => <<"item1,item2">>, type => string}},
{<<"comma_separated_atoms">>, #{example => <<"item1,item2">>, type => string}},
{<<"log_level">>,
#{enum => [debug, info, notice, warning, error, critical, alert, emergency, all], type => string}},
{<<"fix_integer">>, #{default => 100, enum => [100], example => 100,type => integer}}
],
<<"type">> => object}}}},
{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)),
?assertEqual([], Refs),
ok.
t_ref_array_with_key(_Config) -> t_ref_array_with_key(_Config) ->
Path = "/ref/array/with/key", Path = "/ref/array/with/key",
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{ Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
@ -210,8 +242,8 @@ t_hocon_schema_function(_Config) ->
[#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref2">>}, [#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref2">>},
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}]}, type => array}}, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}]}, type => array}},
{<<"default_username">>, {<<"default_username">>,
#{default => <<"admin">>, example => <<"string example">>, type => string}}, #{default => <<"admin">>, example => <<"string-example">>, type => string}},
{<<"default_password">>, #{default => <<"public">>, example => <<"string example">>, type => string}}, {<<"default_password">>, #{default => <<"public">>, example => <<"string-example">>, type => string}},
{<<"sample_interval">>, #{default => <<"10s">>, example => <<"1h">>, type => string}}, {<<"sample_interval">>, #{default => <<"10s">>, example => <<"1h">>, type => string}},
{<<"token_expired_time">>, #{default => <<"30m">>, example => <<"12m">>, type => string}}], {<<"token_expired_time">>, #{default => <<"30m">>, example => <<"12m">>, type => string}}],
<<"type">> => object}}], <<"type">> => object}}],
@ -290,6 +322,27 @@ schema("/error") ->
400 => emqx_dashboard_swagger:error_codes(['Bad1', 'Bad2'], <<"Bad request desc">>), 400 => emqx_dashboard_swagger:error_codes(['Bad1', 'Bad2'], <<"Bad request desc">>),
404 => emqx_dashboard_swagger:error_codes(['Not-Found']) 404 => emqx_dashboard_swagger:error_codes(['Not-Found'])
}} }}
};
schema("/ref/complicated_type") ->
#{
operationId => test,
post => #{responses => #{
200 => [
{no_neg_integer, hoconsc:mk(non_neg_integer(), #{})},
{url, hoconsc:mk(emqx_connector_http:url(), #{})},
{server, hoconsc:mk(emqx_connector_redis:server(), #{})},
{connect_timeout, hoconsc:mk(emqx_connector_http:connect_timeout(), #{})},
{pool_type, hoconsc:mk(emqx_connector_http:pool_type(), #{})},
{timeout, hoconsc:mk(timeout(), #{})},
{bytesize, hoconsc:mk(emqx_schema:bytesize(), #{})},
{wordsize, hoconsc:mk(emqx_schema:wordsize(), #{})},
{maps, hoconsc:mk(map(), #{})},
{comma_separated_list, hoconsc:mk(emqx_schema:comma_separated_list(), #{})},
{comma_separated_atoms, hoconsc:mk(emqx_schema:comma_separated_atoms(), #{})},
{log_level, hoconsc:mk(emqx_machine_schema:log_level(), #{})},
{fix_integer, hoconsc:mk(typerefl:integer(100), #{})}
]
}}
}. }.
validate(Path, ExpectObject, ExpectRefs) -> validate(Path, ExpectObject, ExpectRefs) ->

View File

@ -40,13 +40,13 @@ roots() -> [exhook].
fields(exhook) -> fields(exhook) ->
[ {request_failed_action, [ {request_failed_action,
sc(union([deny, ignore]), sc(hoconsc:enum([deny, ignore]),
#{default => deny})} #{default => deny})}
, {request_timeout, , {request_timeout,
sc(duration(), sc(duration(),
#{default => "5s"})} #{default => "5s"})}
, {auto_reconnect, , {auto_reconnect,
sc(union([false, duration()]), sc(hoconsc:union([false, duration()]),
#{ default => "60s" #{ default => "60s"
})} })}
, {servers, , {servers,

View File

@ -83,7 +83,7 @@ request_parameters() ->
request_properties() -> request_properties() ->
properties([ {token, string, "message token, can be empty"} properties([ {token, string, "message token, can be empty"}
, {method, string, "request method type", ["get", "put", "post", "delete"]} , {method, string, "request method type", ["get", "put", "post", "delete"]}
, {timeout, string, "timespan for response", "10s"} , {timeout, string, "timespan for response"}
, {content_type, string, "payload type", , {content_type, string, "payload type",
[<<"text/plain">>, <<"application/json">>, <<"application/octet-stream">>]} [<<"text/plain">>, <<"application/json">>, <<"application/octet-stream">>]}
, {payload, string, "payload"}]). , {payload, string, "payload"}]).

View File

@ -232,7 +232,7 @@ gateway_common_options() ->
common_listener_opts() -> common_listener_opts() ->
[ {enable, sc(boolean(), true)} [ {enable, sc(boolean(), true)}
, {bind, sc(union(ip_port(), integer()))} , {bind, sc(hoconsc:union([ip_port(), integer()]))}
, {max_connections, sc(integer(), 1024)} , {max_connections, sc(integer(), 1024)}
, {max_conn_rate, sc(integer())} , {max_conn_rate, sc(integer())}
, {authentication, authentication()} , {authentication, authentication()}

View File

@ -16,97 +16,106 @@
-module(emqx_mgmt_api_configs). -module(emqx_mgmt_api_configs).
-include_lib("hocon/include/hoconsc.hrl").
-behaviour(minirest_api). -behaviour(minirest_api).
-import(emqx_mgmt_util, [ schema/1
, schema/2
, error_schema/2
]).
-export([api_spec/0]). -export([api_spec/0]).
-export([paths/0, schema/1, fields/1]).
-export([ config/3 -export([config/3, config_reset/3, configs/3, get_full_config/0]).
, config_reset/3
]).
-export([get_conf_schema/2, gen_schema/1]). -export([get_conf_schema/2, gen_schema/1]).
-define(PARAM_CONF_PATH, [#{ -define(PREFIX, "/configs/").
name => conf_path, -define(PREFIX_RESET, "/configs_reset/").
in => query,
description => <<"The config path separated by '.' character">>,
required => false,
schema => #{type => string, default => <<".">>}
}]).
-define(PREFIX, "/configs").
-define(PREFIX_RESET, "/configs_reset").
-define(MAX_DEPTH, 1).
-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(CORE_CONFS, [
%% from emqx_machine_schema
log, rpc,
%% from emqx_schema
zones, mqtt, flapping_detect, force_shutdown, force_gc, conn_congestion, rate_limit, quota,
broker, alarm, sysmon,
%% from other apps
emqx_dashboard, emqx_management]).
api_spec() -> api_spec() ->
{config_apis() ++ [config_reset_api()], []}. emqx_dashboard_swagger:spec(?MODULE).
config_apis() -> paths() ->
[config_api(ConfPath, Schema) || {ConfPath, Schema} <- ["/configs", "/configs_reset/:rootname"] ++
get_conf_schema(emqx:get_config([]), ?MAX_DEPTH), is_core_conf(ConfPath)]. lists:map(fun({Name, _Type}) -> ?PREFIX ++ to_list(Name) end, config_list(?EXCLUDES)).
config_api(ConfPath, Schema) -> schema("/configs") ->
Path = path_join(ConfPath), #{
Descr = fun(Str) -> operationId => configs,
list_to_binary([Str, " ", path_join(ConfPath, ".")])
end,
Metadata = #{
get => #{ get => #{
description => Descr("Get configs for"), tags => [conf],
description => <<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
parameters => [
{node, hoconsc:mk(typerefl:atom(),
#{in => query, required => false, example => <<"emqx@127.0.0.1">>,
desc => <<"Node's name: If you do not fill in the fields, this node will be used by default.">>})}],
responses => #{ responses => #{
<<"200">> => schema(Schema, <<"Get configs successfully">>), 200 => config_list([])
<<"404">> => emqx_mgmt_util:error_schema(<<"Config not found">>, ['NOT_FOUND'])
}
},
put => #{
description => Descr("Update configs for"),
'requestBody' => schema(Schema),
responses => #{
<<"200">> => schema(Schema, <<"Update configs successfully">>),
<<"400">> => error_schema(<<"Update configs failed">>, ['UPDATE_FAILED'])
} }
} }
}, };
{?PREFIX ++ "/" ++ Path, Metadata, config}. schema("/configs_reset/:rootname") ->
Paths = lists:map(fun({Path, _}) -> Path end, config_list(?EXCLUDES)),
config_reset_api() -> #{
Metadata = #{ operationId => config_reset,
post => #{ post => #{
tags => [configs], tags => [conf],
description => <<"Reset the config entry specified by the query string parameter `conf_path`.<br/> description => <<"Reset the config entry specified by the query string parameter `conf_path`.<br/>
- For a config entry that has default value, this resets it to the default value; - For a config entry that has default value, this resets it to the default value;
- For a config entry that has no default value, an error 400 will be returned">>, - For a config entry that has no default value, an error 400 will be returned">>,
parameters => ?PARAM_CONF_PATH,
responses => #{
%% We only return "200" rather than the new configs that has been changed, as %% We only return "200" rather than the new configs that has been changed, as
%% the schema of the changed configs is depends on the request parameter %% the schema of the changed configs is depends on the request parameter
%% `conf_path`, it cannot be defined here. %% `conf_path`, it cannot be defined here.
<<"200">> => schema(<<"Reset configs successfully">>), parameters => [
<<"400">> => error_schema(<<"It's not able to reset the config">>, ['INVALID_OPERATION']) {rootname, hoconsc:mk(hoconsc:enum(Paths), #{in => path, example => <<"authorization">>})},
{conf_path, hoconsc:mk(typerefl:binary(),
#{in => query, required => false, example => <<"cache.enable">>,
desc => <<"The config path separated by '.' character">>})}],
responses => #{
200 => <<"Rest config successfully">>,
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
} }
} }
};
schema(Path) ->
{Root, Schema} = find_schema(Path),
#{
operationId => config,
get => #{
tags => [conf],
description => iolist_to_binary([<<"Get the sub-configurations under *">>, Root, <<"*">>]),
responses => #{
200 => Schema,
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
}
}, },
{?PREFIX_RESET, Metadata, config_reset}. put => #{
tags => [conf],
description => iolist_to_binary([<<"Update the sub-configurations under *">>, Root, <<"*">>]),
requestBody => Schema,
responses => #{
200 => Schema,
400 => emqx_dashboard_swagger:error_codes(['UPDATE_FAILED'])
}
}
}.
find_schema(Path) ->
[_, _Prefix, Root | _] = string:split(Path, "/", all),
Configs = config_list(?EXCLUDES),
case lists:keyfind(Root, 1, Configs) of
{Root, Schema} -> {Root, Schema};
false ->
RootAtom = list_to_existing_atom(Root),
{Root, element(2, lists:keyfind(RootAtom, 1, Configs))}
end.
%% we load all configs from emqx_machine_schema, some of them are defined as local ref
%% we need redirect to emqx_machine_schema.
%% such as hoconsc:ref("node") to hoconsc:ref(emqx_machine_schema, "node")
fields(Field) -> emqx_machine_schema:fields(Field).
%%%============================================================================================== %%%==============================================================================================
%% parameters trans %% HTTP API Callbacks
config(get, _Params, Req) -> config(get, _Params, Req) ->
Path = conf_path(Req), Path = conf_path(Req),
case emqx_map_lib:deep_find(Path, get_full_config()) of case emqx_map_lib:deep_find(Path, get_full_config()) of
@ -118,19 +127,33 @@ config(get, _Params, Req) ->
config(put, #{body := Body}, Req) -> config(put, #{body := Body}, Req) ->
Path = conf_path(Req), Path = conf_path(Req),
{ok, #{raw_config := RawConf}} = emqx:update_config(Path, Body, case emqx:update_config(Path, Body, #{rawconf_with_defaults => true}) of
#{rawconf_with_defaults => true}), {ok, #{raw_config := RawConf}} ->
{200, emqx_map_lib:jsonable_map(RawConf)}. {200, emqx_map_lib:jsonable_map(RawConf)};
{error, Reason} ->
{400, #{code => 'UPDATE_FAILED', message => ?ERR_MSG(Reason)}}
end.
config_reset(post, _Params, Req) -> config_reset(post, _Params, Req) ->
%% reset the config specified by the query string param 'conf_path' %% reset the config specified by the query string param 'conf_path'
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req), Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
case emqx:reset_config(Path, #{}) of case emqx:reset_config(Path, #{}) of
{ok, _} -> {200}; {ok, _} -> {200};
{error, no_default_value} ->
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
{error, Reason} -> {error, Reason} ->
{400, ?ERR_MSG(Reason)} {400, #{code => 'REST_FAILED', message => ?ERR_MSG(Reason)}}
end. end.
configs(get, Params, _Req) ->
Node = maps:get(node, Params, node()),
Res = rpc:call(Node, ?MODULE, get_full_config, [[]]),
{200, Res}.
conf_path_reset(Req) ->
<<"/api/v5", ?PREFIX_RESET, Path/binary>> = cowboy_req:path(Req),
string:lexemes(Path, "/ ").
get_full_config() -> get_full_config() ->
emqx_map_lib:jsonable_map( emqx_map_lib:jsonable_map(
emqx_config:fill_defaults(emqx:get_raw_config([]))). emqx_config:fill_defaults(emqx:get_raw_config([]))).
@ -141,14 +164,17 @@ conf_path_from_querystr(Req) ->
Path -> string:lexemes(Path, ". ") Path -> string:lexemes(Path, ". ")
end. end.
config_list(Exclude) ->
Roots = emqx_machine_schema:roots(),
lists:foldl(fun(Key, Acc) -> lists:delete(Key, Acc) end, Roots, Exclude).
to_list(L) when is_list(L) -> L;
to_list(Atom) when is_atom(Atom) -> atom_to_list(Atom).
conf_path(Req) -> conf_path(Req) ->
<<"/api/v5", ?PREFIX, Path/binary>> = cowboy_req:path(Req), <<"/api/v5", ?PREFIX, Path/binary>> = cowboy_req:path(Req),
string:lexemes(Path, "/ "). string:lexemes(Path, "/ ").
conf_path_reset(Req) ->
<<"/api/v5", ?PREFIX_RESET, Path/binary>> = cowboy_req:path(Req),
string:lexemes(Path, "/ ").
get_conf_schema(Conf, MaxDepth) -> get_conf_schema(Conf, MaxDepth) ->
get_conf_schema([], maps:to_list(Conf), [], MaxDepth). get_conf_schema([], maps:to_list(Conf), [], MaxDepth).
@ -189,17 +215,3 @@ gen_schema(_Conf) ->
with_default_value(Type, Value) -> with_default_value(Type, Value) ->
Type#{example => emqx_map_lib:binary_string(Value)}. Type#{example => emqx_map_lib:binary_string(Value)}.
path_join(Path) ->
path_join(Path, "/").
path_join([P], _Sp) -> str(P);
path_join([P | Path], Sp) ->
str(P) ++ Sp ++ path_join(Path, Sp).
is_core_conf(Path) ->
lists:member(hd(Path), ?CORE_CONFS).
str(S) when is_list(S) -> S;
str(S) when is_binary(S) -> binary_to_list(S);
str(S) when is_atom(S) -> atom_to_list(S).

View File

@ -28,14 +28,12 @@ namespace() -> modules.
roots() -> roots() ->
["delayed", ["delayed",
"recon",
"telemetry", "telemetry",
"event_message", "event_message",
array("rewrite"), array("rewrite"),
array("topic_metrics")]. array("topic_metrics")].
fields(Name) when Name =:= "recon"; fields("telemetry") ->
Name =:= "telemetry" ->
[ {enable, hoconsc:mk(boolean(), #{default => false})} [ {enable, hoconsc:mk(boolean(), #{default => false})}
]; ];