Merge pull request #7590 from zhongwencool/doc-i18n
feat: desc/label support i18n
This commit is contained in:
commit
ce915f0bbd
|
@ -1,4 +1,5 @@
|
||||||
.eunit
|
.eunit
|
||||||
|
*.conf.all
|
||||||
test-data/
|
test-data/
|
||||||
deps
|
deps
|
||||||
!deps/.placeholder
|
!deps/.placeholder
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -61,7 +61,7 @@ get-dashboard:
|
||||||
@$(SCRIPTS)/get-dashboard.sh
|
@$(SCRIPTS)/get-dashboard.sh
|
||||||
|
|
||||||
.PHONY: eunit
|
.PHONY: eunit
|
||||||
eunit: $(REBAR)
|
eunit: $(REBAR) conf-segs
|
||||||
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c
|
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c
|
||||||
|
|
||||||
.PHONY: proper
|
.PHONY: proper
|
||||||
|
@ -218,6 +218,7 @@ $(foreach zt,$(ALL_DOCKERS),$(eval $(call gen-docker-target,$(zt))))
|
||||||
.PHONY:
|
.PHONY:
|
||||||
conf-segs:
|
conf-segs:
|
||||||
@scripts/merge-config.escript
|
@scripts/merge-config.escript
|
||||||
|
@scripts/merge-i18n.escript
|
||||||
|
|
||||||
## elixir target is to create release packages using Elixir's Mix
|
## elixir target is to create release packages using Elixir's Mix
|
||||||
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)
|
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
|
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
|
||||||
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
|
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
|
||||||
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
|
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
|
||||||
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.6"}}},
|
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.7"}}},
|
||||||
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
|
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
|
||||||
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
|
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
|
||||||
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
|
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
|
||||||
|
|
|
@ -31,9 +31,11 @@
|
||||||
|
|
||||||
-define(TOPICS, [?TOPIC_C, ?TOPIC_U, ?TOPIC_H, ?TOPIC_P, ?TOPIC_A, ?TOPIC_S]).
|
-define(TOPICS, [?TOPIC_C, ?TOPIC_U, ?TOPIC_H, ?TOPIC_P, ?TOPIC_A, ?TOPIC_S]).
|
||||||
|
|
||||||
-define(ENSURE_TOPICS , [<<"/c/auto_sub_c">>
|
-define(ENSURE_TOPICS, [
|
||||||
, <<"/u/auto_sub_u">>
|
<<"/c/auto_sub_c">>,
|
||||||
, ?TOPIC_S]).
|
<<"/u/auto_sub_u">>,
|
||||||
|
?TOPIC_S
|
||||||
|
]).
|
||||||
|
|
||||||
-define(CLIENT_ID, <<"auto_sub_c">>).
|
-define(CLIENT_ID, <<"auto_sub_c">>).
|
||||||
-define(CLIENT_USERNAME, <<"auto_sub_u">>).
|
-define(CLIENT_USERNAME, <<"auto_sub_u">>).
|
||||||
|
@ -45,10 +47,12 @@ init_per_suite(Config) ->
|
||||||
mria:start(),
|
mria:start(),
|
||||||
application:stop(?APP),
|
application:stop(?APP),
|
||||||
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
|
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
|
||||||
meck:expect(emqx_schema, fields, fun("auto_subscribe") ->
|
meck:expect(emqx_schema, fields, fun
|
||||||
|
("auto_subscribe") ->
|
||||||
meck:passthrough(["auto_subscribe"]) ++
|
meck:passthrough(["auto_subscribe"]) ++
|
||||||
emqx_auto_subscribe_schema:fields("auto_subscribe");
|
emqx_auto_subscribe_schema:fields("auto_subscribe");
|
||||||
(F) -> meck:passthrough([F])
|
(F) ->
|
||||||
|
meck:passthrough([F])
|
||||||
end),
|
end),
|
||||||
|
|
||||||
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
||||||
|
@ -58,47 +62,43 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
application:load(emqx_dashboard),
|
application:load(emqx_dashboard),
|
||||||
application:load(?APP),
|
application:load(?APP),
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_auto_subscribe_schema,
|
ok = emqx_common_test_helpers:load_config(
|
||||||
<<"auto_subscribe {
|
emqx_auto_subscribe_schema,
|
||||||
topics = [
|
<<"auto_subscribe {\n"
|
||||||
{
|
" topics = [\n"
|
||||||
topic = \"/c/${clientid}\"
|
" {\n"
|
||||||
},
|
" topic = \"/c/${clientid}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/u/${username}\"
|
" {\n"
|
||||||
},
|
" topic = \"/u/${username}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/h/${host}\"
|
" {\n"
|
||||||
},
|
" topic = \"/h/${host}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/p/${port}\"
|
" {\n"
|
||||||
},
|
" topic = \"/p/${port}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"
|
" {\n"
|
||||||
},
|
" topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"\n"
|
||||||
{
|
" },\n"
|
||||||
topic = \"/topic/simple\"
|
" {\n"
|
||||||
qos = 1
|
" topic = \"/topic/simple\"\n"
|
||||||
rh = 0
|
" qos = 1\n"
|
||||||
rap = 0
|
" rh = 0\n"
|
||||||
nl = 0
|
" rap = 0\n"
|
||||||
}
|
" nl = 0\n"
|
||||||
]
|
" }\n"
|
||||||
}">>),
|
" ]\n"
|
||||||
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard, ?APP],
|
" }">>
|
||||||
fun set_special_configs/1),
|
),
|
||||||
|
emqx_common_test_helpers:start_apps(
|
||||||
|
[emqx_conf, emqx_dashboard, ?APP],
|
||||||
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([emqx_dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
@ -148,7 +148,6 @@ t_update(_) ->
|
||||||
?assertEqual(1, erlang:length(GETResponseMap)),
|
?assertEqual(1, erlang:length(GETResponseMap)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
||||||
check_subs(Count) ->
|
check_subs(Count) ->
|
||||||
Subs = ets:tab2list(emqx_suboption),
|
Subs = ets:tab2list(emqx_suboption),
|
||||||
ct:pal("---> ~p ~p ~n", [Subs, Count]),
|
ct:pal("---> ~p ~p ~n", [Subs, Count]),
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
-export([update/3, update/4]).
|
-export([update/3, update/4]).
|
||||||
-export([remove/2, remove/3]).
|
-export([remove/2, remove/3]).
|
||||||
-export([reset/2, reset/3]).
|
-export([reset/2, reset/3]).
|
||||||
-export([dump_schema/1, dump_schema/2]).
|
-export([dump_schema/1, dump_schema/3]).
|
||||||
-export([schema_module/0]).
|
-export([schema_module/0]).
|
||||||
|
|
||||||
%% for rpc
|
%% for rpc
|
||||||
|
@ -80,15 +80,22 @@ get_node_and_config(KeyPath) ->
|
||||||
{node(), emqx:get_config(KeyPath, config_not_found)}.
|
{node(), emqx:get_config(KeyPath, config_not_found)}.
|
||||||
|
|
||||||
%% @doc Update all value of key path in cluster-override.conf or local-override.conf.
|
%% @doc Update all value of key path in cluster-override.conf or local-override.conf.
|
||||||
-spec update(emqx_map_lib:config_key_path(), emqx_config:update_request(),
|
-spec update(
|
||||||
emqx_config:update_opts()) ->
|
emqx_map_lib:config_key_path(),
|
||||||
|
emqx_config:update_request(),
|
||||||
|
emqx_config:update_opts()
|
||||||
|
) ->
|
||||||
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
||||||
update(KeyPath, UpdateReq, Opts) ->
|
update(KeyPath, UpdateReq, Opts) ->
|
||||||
check_cluster_rpc_result(emqx_conf_proto_v1:update(KeyPath, UpdateReq, Opts)).
|
check_cluster_rpc_result(emqx_conf_proto_v1:update(KeyPath, UpdateReq, Opts)).
|
||||||
|
|
||||||
%% @doc Update the specified node's key path in local-override.conf.
|
%% @doc Update the specified node's key path in local-override.conf.
|
||||||
-spec update(node(), emqx_map_lib:config_key_path(), emqx_config:update_request(),
|
-spec update(
|
||||||
emqx_config:update_opts()) ->
|
node(),
|
||||||
|
emqx_map_lib:config_key_path(),
|
||||||
|
emqx_config:update_request(),
|
||||||
|
emqx_config:update_opts()
|
||||||
|
) ->
|
||||||
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()} | emqx_rpc:badrpc().
|
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()} | emqx_rpc:badrpc().
|
||||||
update(Node, KeyPath, UpdateReq, Opts0) when Node =:= node() ->
|
update(Node, KeyPath, UpdateReq, Opts0) when Node =:= node() ->
|
||||||
emqx:update_config(KeyPath, UpdateReq, Opts0#{override_to => local});
|
emqx:update_config(KeyPath, UpdateReq, Opts0#{override_to => local});
|
||||||
|
@ -126,25 +133,41 @@ 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) ->
|
||||||
dump_schema(Dir, emqx_conf_schema).
|
I18nFile = emqx:etc_file("i18n.conf"),
|
||||||
|
dump_schema(Dir, emqx_conf_schema, I18nFile).
|
||||||
|
|
||||||
dump_schema(Dir, SchemaModule) ->
|
dump_schema(Dir, SchemaModule, I18nFile) ->
|
||||||
SchemaMdFile = filename:join([Dir, "config.md"]),
|
lists:foreach(
|
||||||
io:format(user, "===< Generating: ~s~n", [SchemaMdFile ]),
|
fun(Lang) ->
|
||||||
ok = gen_doc(SchemaMdFile, SchemaModule),
|
gen_config_md(Dir, I18nFile, SchemaModule, Lang),
|
||||||
|
gen_hot_conf_schema_json(Dir, I18nFile, Lang)
|
||||||
|
end,
|
||||||
|
[en, zh]
|
||||||
|
),
|
||||||
|
gen_schema_json(Dir, I18nFile, SchemaModule).
|
||||||
|
|
||||||
%% for scripts/spellcheck.
|
%% for scripts/spellcheck.
|
||||||
|
gen_schema_json(Dir, I18nFile, SchemaModule) ->
|
||||||
SchemaJsonFile = filename:join([Dir, "schema.json"]),
|
SchemaJsonFile = filename:join([Dir, "schema.json"]),
|
||||||
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
|
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
|
||||||
JsonMap = hocon_schema_json:gen(SchemaModule),
|
Opts = #{desc_file => I18nFile, lang => "en"},
|
||||||
|
JsonMap = hocon_schema_json:gen(SchemaModule, Opts),
|
||||||
IoData = jsx:encode(JsonMap, [space, {indent, 4}]),
|
IoData = jsx:encode(JsonMap, [space, {indent, 4}]),
|
||||||
ok = file:write_file(SchemaJsonFile, IoData),
|
ok = file:write_file(SchemaJsonFile, IoData).
|
||||||
|
|
||||||
%% hot-update configuration schema
|
gen_hot_conf_schema_json(Dir, I18nFile, Lang) ->
|
||||||
HotConfigSchemaFile = filename:join([Dir, "hot-config-schema.json"]),
|
emqx_dashboard:init_i18n(I18nFile, Lang),
|
||||||
|
JsonFile = "hot-config-schema-" ++ atom_to_list(Lang) ++ ".json",
|
||||||
|
HotConfigSchemaFile = filename:join([Dir, JsonFile]),
|
||||||
io:format(user, "===< Generating: ~s~n", [HotConfigSchemaFile]),
|
io:format(user, "===< Generating: ~s~n", [HotConfigSchemaFile]),
|
||||||
ok = gen_hot_conf_schema(HotConfigSchemaFile),
|
ok = gen_hot_conf_schema(HotConfigSchemaFile),
|
||||||
ok.
|
emqx_dashboard:clear_i18n().
|
||||||
|
|
||||||
|
gen_config_md(Dir, I18nFile, SchemaModule, Lang0) ->
|
||||||
|
Lang = atom_to_list(Lang0),
|
||||||
|
SchemaMdFile = filename:join([Dir, "config-" ++ Lang ++ ".md"]),
|
||||||
|
io:format(user, "===< Generating: ~s~n", [SchemaMdFile]),
|
||||||
|
ok = gen_doc(SchemaMdFile, SchemaModule, I18nFile, Lang).
|
||||||
|
|
||||||
%% @doc return the root schema module.
|
%% @doc return the root schema module.
|
||||||
-spec schema_module() -> module().
|
-spec schema_module() -> module().
|
||||||
|
@ -158,63 +181,96 @@ schema_module() ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec gen_doc(file:name_all(), module()) -> ok.
|
-spec gen_doc(file:name_all(), module(), file:name_all(), string()) -> ok.
|
||||||
gen_doc(File, SchemaModule) ->
|
gen_doc(File, SchemaModule, I18nFile, Lang) ->
|
||||||
Version = emqx_release:version(),
|
Version = emqx_release:version(),
|
||||||
Title = "# " ++ emqx_release:description() ++ " " ++ Version ++ " Configuration",
|
Title = "# " ++ emqx_release:description() ++ " " ++ Version ++ " Configuration",
|
||||||
BodyFile = filename:join([code:lib_dir(emqx_conf), "etc", "emqx_conf.md"]),
|
BodyFile = filename:join([code:lib_dir(emqx_conf), "etc", "emqx_conf.md"]),
|
||||||
{ok, Body} = file:read_file(BodyFile),
|
{ok, Body} = file:read_file(BodyFile),
|
||||||
Doc = hocon_schema_md:gen(SchemaModule, #{title => Title, body => Body}),
|
Opts = #{title => Title, body => Body, desc_file => I18nFile, lang => Lang},
|
||||||
|
Doc = hocon_schema_md:gen(SchemaModule, Opts),
|
||||||
file:write_file(File, Doc).
|
file:write_file(File, Doc).
|
||||||
|
|
||||||
check_cluster_rpc_result(Result) ->
|
check_cluster_rpc_result(Result) ->
|
||||||
case Result of
|
case Result of
|
||||||
{ok, _TnxId, Res} -> Res;
|
{ok, _TnxId, Res} ->
|
||||||
|
Res;
|
||||||
{retry, TnxId, Res, Nodes} ->
|
{retry, TnxId, Res, Nodes} ->
|
||||||
%% The init MFA return ok, but other nodes failed.
|
%% The init MFA return ok, but other nodes failed.
|
||||||
%% We return ok and alert an alarm.
|
%% We return ok and alert an alarm.
|
||||||
?SLOG(error, #{msg => "failed_to_update_config_in_cluster", nodes => Nodes,
|
?SLOG(error, #{
|
||||||
tnx_id => TnxId}),
|
msg => "failed_to_update_config_in_cluster",
|
||||||
|
nodes => Nodes,
|
||||||
|
tnx_id => TnxId
|
||||||
|
}),
|
||||||
Res;
|
Res;
|
||||||
{error, Error} -> %% all MFA return not ok or {ok, term()}.
|
%% all MFA return not ok or {ok, term()}.
|
||||||
|
{error, Error} ->
|
||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Only gen hot_conf schema, not all configuration fields.
|
%% Only gen hot_conf schema, not all configuration fields.
|
||||||
gen_hot_conf_schema(File) ->
|
gen_hot_conf_schema(File) ->
|
||||||
{ApiSpec0, Components0} = emqx_dashboard_swagger:spec(emqx_mgmt_api_configs,
|
{ApiSpec0, Components0} = emqx_dashboard_swagger:spec(
|
||||||
#{schema_converter => fun hocon_schema_to_spec/2}),
|
emqx_mgmt_api_configs,
|
||||||
ApiSpec = lists:foldl(fun({Path, Spec, _, _}, Acc) ->
|
#{schema_converter => fun hocon_schema_to_spec/2}
|
||||||
NewSpec = maps:fold(fun(Method, #{responses := Responses}, SubAcc) ->
|
),
|
||||||
|
ApiSpec = lists:foldl(
|
||||||
|
fun({Path, Spec, _, _}, Acc) ->
|
||||||
|
NewSpec = maps:fold(
|
||||||
|
fun(Method, #{responses := Responses}, SubAcc) ->
|
||||||
case Responses of
|
case Responses of
|
||||||
#{<<"200">> :=
|
#{
|
||||||
#{<<"content">> := #{<<"application/json">> := #{<<"schema">> := Schema}}}} ->
|
<<"200">> :=
|
||||||
|
#{
|
||||||
|
<<"content">> := #{
|
||||||
|
<<"application/json">> := #{<<"schema">> := Schema}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ->
|
||||||
SubAcc#{Method => Schema};
|
SubAcc#{Method => Schema};
|
||||||
_ -> SubAcc
|
_ ->
|
||||||
|
SubAcc
|
||||||
end
|
end
|
||||||
end, #{}, Spec),
|
end,
|
||||||
Acc#{list_to_atom(Path) => NewSpec} end, #{}, ApiSpec0),
|
#{},
|
||||||
|
Spec
|
||||||
|
),
|
||||||
|
Acc#{list_to_atom(Path) => NewSpec}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
ApiSpec0
|
||||||
|
),
|
||||||
Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
|
Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
|
||||||
IoData = jsx:encode(#{
|
IoData = jsx:encode(
|
||||||
|
#{
|
||||||
info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
|
info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
|
||||||
paths => ApiSpec,
|
paths => ApiSpec,
|
||||||
components => #{schemas => Components}
|
components => #{schemas => Components}
|
||||||
}, [space, {indent, 4}]),
|
},
|
||||||
|
[space, {indent, 4}]
|
||||||
|
),
|
||||||
file:write_file(File, IoData).
|
file:write_file(File, IoData).
|
||||||
|
|
||||||
-define(INIT_SCHEMA, #{fields => #{}, translations => #{},
|
-define(INIT_SCHEMA, #{
|
||||||
validations => [], namespace => undefined}).
|
fields => #{},
|
||||||
|
translations => #{},
|
||||||
|
validations => [],
|
||||||
|
namespace => undefined
|
||||||
|
}).
|
||||||
|
|
||||||
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
-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/">>,
|
-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
|
||||||
?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)])).
|
iolist_to_binary([
|
||||||
|
<<"#/components/schemas/">>,
|
||||||
|
?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)
|
||||||
|
])
|
||||||
|
).
|
||||||
|
|
||||||
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
|
||||||
[{Module, StructName}]};
|
|
||||||
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), LocalModule), []};
|
{typename_to_spec(typerefl:name(Type), LocalModule), []};
|
||||||
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
||||||
|
@ -226,50 +282,97 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
||||||
{#{type => enum, symbols => Items}, []};
|
{#{type => enum, symbols => Items}, []};
|
||||||
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
||||||
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
||||||
{#{<<"type">> => object,
|
{
|
||||||
<<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
|
#{
|
||||||
SubRefs};
|
<<"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),
|
||||||
{[Schema | Acc], SubRefs ++ RefsAcc}
|
{[Schema | Acc], SubRefs ++ RefsAcc}
|
||||||
end, {[], []}, Types),
|
end,
|
||||||
|
{[], []},
|
||||||
|
Types
|
||||||
|
),
|
||||||
{#{<<"oneOf">> => OneOf}, Refs};
|
{#{<<"oneOf">> => OneOf}, Refs};
|
||||||
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
|
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
|
||||||
{#{type => enum, symbols => [Atom]}, []}.
|
{#{type => enum, symbols => [Atom]}, []}.
|
||||||
|
|
||||||
typename_to_spec("user_id_type()", _Mod) -> #{type => enum, symbols => [clientid, username]};
|
typename_to_spec("user_id_type()", _Mod) ->
|
||||||
typename_to_spec("term()", _Mod) -> #{type => string};
|
#{type => enum, symbols => [clientid, username]};
|
||||||
typename_to_spec("boolean()", _Mod) -> #{type => boolean};
|
typename_to_spec("term()", _Mod) ->
|
||||||
typename_to_spec("binary()", _Mod) -> #{type => string};
|
#{type => string};
|
||||||
typename_to_spec("float()", _Mod) -> #{type => number};
|
typename_to_spec("boolean()", _Mod) ->
|
||||||
typename_to_spec("integer()", _Mod) -> #{type => number};
|
#{type => boolean};
|
||||||
typename_to_spec("non_neg_integer()", _Mod) -> #{type => number, minimum => 1};
|
typename_to_spec("binary()", _Mod) ->
|
||||||
typename_to_spec("number()", _Mod) -> #{type => number};
|
#{type => string};
|
||||||
typename_to_spec("string()", _Mod) -> #{type => string};
|
typename_to_spec("float()", _Mod) ->
|
||||||
typename_to_spec("atom()", _Mod) -> #{type => string};
|
#{type => number};
|
||||||
|
typename_to_spec("integer()", _Mod) ->
|
||||||
typename_to_spec("duration()", _Mod) -> #{type => duration};
|
#{type => number};
|
||||||
typename_to_spec("duration_s()", _Mod) -> #{type => duration};
|
typename_to_spec("non_neg_integer()", _Mod) ->
|
||||||
typename_to_spec("duration_ms()", _Mod) -> #{type => duration};
|
#{type => number, minimum => 1};
|
||||||
typename_to_spec("percent()", _Mod) -> #{type => percent};
|
typename_to_spec("number()", _Mod) ->
|
||||||
typename_to_spec("file()", _Mod) -> #{type => string};
|
#{type => number};
|
||||||
typename_to_spec("ip_port()", _Mod) -> #{type => ip_port};
|
typename_to_spec("string()", _Mod) ->
|
||||||
typename_to_spec("url()", _Mod) -> #{type => url};
|
#{type => string};
|
||||||
typename_to_spec("bytesize()", _Mod) -> #{type => 'byteSize'};
|
typename_to_spec("atom()", _Mod) ->
|
||||||
typename_to_spec("wordsize()", _Mod) -> #{type => 'byteSize'};
|
#{type => string};
|
||||||
typename_to_spec("qos()", _Mod) -> #{type => enum, symbols => [0, 1, 2]};
|
typename_to_spec("duration()", _Mod) ->
|
||||||
typename_to_spec("comma_separated_list()", _Mod) -> #{type => comma_separated_string};
|
#{type => duration};
|
||||||
typename_to_spec("comma_separated_atoms()", _Mod) -> #{type => comma_separated_string};
|
typename_to_spec("duration_s()", _Mod) ->
|
||||||
typename_to_spec("pool_type()", _Mod) -> #{type => enum, symbols => [random, hash]};
|
#{type => duration};
|
||||||
|
typename_to_spec("duration_ms()", _Mod) ->
|
||||||
|
#{type => duration};
|
||||||
|
typename_to_spec("percent()", _Mod) ->
|
||||||
|
#{type => percent};
|
||||||
|
typename_to_spec("file()", _Mod) ->
|
||||||
|
#{type => string};
|
||||||
|
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) ->
|
typename_to_spec("log_level()", _Mod) ->
|
||||||
#{type => enum, symbols => [debug, info, notice, warning, error,
|
#{
|
||||||
critical, alert, emergency, all]};
|
type => enum,
|
||||||
typename_to_spec("rate()", _Mod) -> #{type => string};
|
symbols => [
|
||||||
typename_to_spec("capacity()", _Mod) -> #{type => string};
|
debug,
|
||||||
typename_to_spec("burst_rate()", _Mod) -> #{type => string};
|
info,
|
||||||
typename_to_spec("failure_strategy()", _Mod) -> #{type => enum, symbols => [force, drop, throw]};
|
notice,
|
||||||
typename_to_spec("initial()", _Mod) -> #{type => string};
|
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) ->
|
typename_to_spec(Name, Mod) ->
|
||||||
Spec = range(Name),
|
Spec = range(Name),
|
||||||
Spec1 = remote_module_type(Spec, Name, Mod),
|
Spec1 = remote_module_type(Spec, Name, Mod),
|
||||||
|
@ -282,11 +385,13 @@ default_type(Type) -> Type.
|
||||||
|
|
||||||
range(Name) ->
|
range(Name) ->
|
||||||
case string:split(Name, "..") of
|
case string:split(Name, "..") of
|
||||||
[MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
|
%% 1..10 1..inf -inf..10
|
||||||
|
[MinStr, MaxStr] ->
|
||||||
Schema = #{type => number},
|
Schema = #{type => number},
|
||||||
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
||||||
add_integer_prop(Schema1, maximum, MaxStr);
|
add_integer_prop(Schema1, maximum, MaxStr);
|
||||||
_ -> nomatch
|
_ ->
|
||||||
|
nomatch
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Module:Type
|
%% Module:Type
|
||||||
|
@ -295,21 +400,25 @@ remote_module_type(nomatch, Name, Mod) ->
|
||||||
[_Module, Type] -> typename_to_spec(Type, Mod);
|
[_Module, Type] -> typename_to_spec(Type, Mod);
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
remote_module_type(Spec, _Name, _Mod) -> Spec.
|
remote_module_type(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% [string()] or [integer()] or [xxx].
|
%% [string()] or [integer()] or [xxx].
|
||||||
typerefl_array(nomatch, Name, Mod) ->
|
typerefl_array(nomatch, Name, Mod) ->
|
||||||
case string:trim(Name, leading, "[") of
|
case string:trim(Name, leading, "[") of
|
||||||
Name -> nomatch;
|
Name ->
|
||||||
|
nomatch;
|
||||||
Name1 ->
|
Name1 ->
|
||||||
case string:trim(Name1, trailing, "]") of
|
case string:trim(Name1, trailing, "]") of
|
||||||
Name1 -> notmatch;
|
Name1 ->
|
||||||
|
notmatch;
|
||||||
Name2 ->
|
Name2 ->
|
||||||
Schema = typename_to_spec(Name2, Mod),
|
Schema = typename_to_spec(Name2, Mod),
|
||||||
#{type => array, items => Schema}
|
#{type => array, items => Schema}
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
typerefl_array(Spec, _Name, _Mod) -> Spec.
|
typerefl_array(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% integer(1)
|
%% integer(1)
|
||||||
integer(nomatch, Name) ->
|
integer(nomatch, Name) ->
|
||||||
|
@ -317,7 +426,8 @@ integer(nomatch, Name) ->
|
||||||
{Int, []} -> #{type => enum, symbols => [Int], default => Int};
|
{Int, []} -> #{type => enum, symbols => [Int], default => Int};
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
integer(Spec, _Name) -> Spec.
|
integer(Spec, _Name) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
add_integer_prop(Schema, Key, Value) ->
|
add_integer_prop(Schema, Key, Value) ->
|
||||||
case string:to_integer(Value) of
|
case string:to_integer(Value) of
|
||||||
|
@ -333,4 +443,5 @@ to_bin(List) when is_list(List) ->
|
||||||
end;
|
end;
|
||||||
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
|
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.
|
||||||
|
|
|
@ -9,5 +9,6 @@
|
||||||
doc_gen_test() ->
|
doc_gen_test() ->
|
||||||
Dir = "tmp",
|
Dir = "tmp",
|
||||||
ok = filelib:ensure_dir(filename:join("tmp", foo)),
|
ok = filelib:ensure_dir(filename:join("tmp", foo)),
|
||||||
_ = emqx_conf:dump_schema(Dir),
|
I18nFile = filename:join(["_build", "test", "lib", "emqx_dashboard", "etc", "i18n.conf.all"]),
|
||||||
|
_ = emqx_conf:dump_schema(Dir, emqx_conf_schema, I18nFile),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
emqx_dashboard_schema {
|
||||||
|
protocol {
|
||||||
|
desc {
|
||||||
|
en: "Protocol Name"
|
||||||
|
zh: "协议名"
|
||||||
|
}
|
||||||
|
label: {
|
||||||
|
en: "Protocol"
|
||||||
|
zh: "协议"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,11 +18,19 @@
|
||||||
|
|
||||||
-define(APP, ?MODULE).
|
-define(APP, ?MODULE).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
start_listeners/0,
|
||||||
|
start_listeners/1,
|
||||||
|
stop_listeners/1,
|
||||||
|
stop_listeners/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ start_listeners/0
|
-export([
|
||||||
, start_listeners/1
|
init_i18n/2,
|
||||||
, stop_listeners/1
|
init_i18n/0,
|
||||||
, stop_listeners/0]).
|
get_i18n/0,
|
||||||
|
clear_i18n/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% Authorization
|
%% Authorization
|
||||||
-export([authorize/1]).
|
-export([authorize/1]).
|
||||||
|
@ -48,6 +56,7 @@ stop_listeners() ->
|
||||||
|
|
||||||
start_listeners(Listeners) ->
|
start_listeners(Listeners) ->
|
||||||
{ok, _} = application:ensure_all_started(minirest),
|
{ok, _} = application:ensure_all_started(minirest),
|
||||||
|
init_i18n(),
|
||||||
Authorization = {?MODULE, authorize},
|
Authorization = {?MODULE, authorize},
|
||||||
GlobalSpec = #{
|
GlobalSpec = #{
|
||||||
openapi => "3.0.0",
|
openapi => "3.0.0",
|
||||||
|
@ -58,11 +67,14 @@ start_listeners(Listeners) ->
|
||||||
'securitySchemes' => #{
|
'securitySchemes' => #{
|
||||||
'basicAuth' => #{type => http, scheme => basic},
|
'basicAuth' => #{type => http, scheme => basic},
|
||||||
'bearerAuth' => #{type => http, scheme => bearer}
|
'bearerAuth' => #{type => http, scheme => bearer}
|
||||||
}}},
|
}
|
||||||
Dispatch = [ {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
|
}
|
||||||
, {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}
|
},
|
||||||
, {?BASE_PATH ++ "/[...]", emqx_dashboard_bad_api, []}
|
Dispatch = [
|
||||||
, {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
|
{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}},
|
||||||
|
{"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}},
|
||||||
|
{?BASE_PATH ++ "/[...]", emqx_dashboard_bad_api, []},
|
||||||
|
{'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
|
||||||
],
|
],
|
||||||
BaseMinirest = #{
|
BaseMinirest = #{
|
||||||
base_path => ?BASE_PATH,
|
base_path => ?BASE_PATH,
|
||||||
|
@ -74,51 +86,85 @@ start_listeners(Listeners) ->
|
||||||
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
|
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
|
||||||
},
|
},
|
||||||
Res =
|
Res =
|
||||||
lists:foldl(fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
|
lists:foldl(
|
||||||
|
fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
|
||||||
Minirest = BaseMinirest#{protocol => Protocol},
|
Minirest = BaseMinirest#{protocol => Protocol},
|
||||||
case minirest:start(Name, RanchOptions, Minirest) of
|
case minirest:start(Name, RanchOptions, Minirest) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
?ULOG("Start listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Bind)]),
|
?ULOG("Start listener ~ts on ~ts successfully.~n", [
|
||||||
|
Name, emqx_listeners:format_addr(Bind)
|
||||||
|
]),
|
||||||
Acc;
|
Acc;
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
%% Don't record the reason because minirest already does(too much logs noise).
|
%% Don't record the reason because minirest already does(too much logs noise).
|
||||||
[Name | Acc]
|
[Name | Acc]
|
||||||
end
|
end
|
||||||
end, [], listeners(Listeners)),
|
end,
|
||||||
|
[],
|
||||||
|
listeners(Listeners)
|
||||||
|
),
|
||||||
|
clear_i18n(),
|
||||||
case Res of
|
case Res of
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
_ -> {error, Res}
|
_ -> {error, Res}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
stop_listeners(Listeners) ->
|
stop_listeners(Listeners) ->
|
||||||
[begin
|
[
|
||||||
|
begin
|
||||||
case minirest:stop(Name) of
|
case minirest:stop(Name) of
|
||||||
ok ->
|
ok ->
|
||||||
?ULOG("Stop listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Port)]);
|
?ULOG("Stop listener ~ts on ~ts successfully.~n", [
|
||||||
|
Name, emqx_listeners:format_addr(Port)
|
||||||
|
]);
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
|
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
|
||||||
end
|
end
|
||||||
end || {Name, _, Port, _} <- listeners(Listeners)],
|
end
|
||||||
|
|| {Name, _, Port, _} <- listeners(Listeners)
|
||||||
|
],
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
get_i18n() ->
|
||||||
|
application:get_env(emqx_dashboard, i18n).
|
||||||
|
|
||||||
|
init_i18n(File, Lang) ->
|
||||||
|
Cache = hocon_schema:new_cache(File),
|
||||||
|
application:set_env(emqx_dashboard, i18n, #{lang => atom_to_binary(Lang), cache => Cache}).
|
||||||
|
|
||||||
|
clear_i18n() ->
|
||||||
|
case application:get_env(emqx_dashboard, i18n) of
|
||||||
|
{ok, #{cache := Cache}} ->
|
||||||
|
hocon_schema:delete_cache(Cache),
|
||||||
|
application:unset_env(emqx_dashboard, i18n);
|
||||||
|
undefined ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% internal
|
%% internal
|
||||||
|
|
||||||
apps() ->
|
apps() ->
|
||||||
[App || {App, _, _} <- application:loaded_applications(),
|
[
|
||||||
|
App
|
||||||
|
|| {App, _, _} <- application:loaded_applications(),
|
||||||
case re:run(atom_to_list(App), "^emqx") of
|
case re:run(atom_to_list(App), "^emqx") of
|
||||||
{match, [{0, 4}]} -> true;
|
{match, [{0, 4}]} -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
end].
|
end
|
||||||
|
].
|
||||||
|
|
||||||
listeners(Listeners) ->
|
listeners(Listeners) ->
|
||||||
[begin
|
[
|
||||||
|
begin
|
||||||
Protocol = maps:get(protocol, ListenerOption0, http),
|
Protocol = maps:get(protocol, ListenerOption0, http),
|
||||||
{ListenerOption, Bind} = ip_port(ListenerOption0),
|
{ListenerOption, Bind} = ip_port(ListenerOption0),
|
||||||
Name = listener_name(Protocol, ListenerOption),
|
Name = listener_name(Protocol, ListenerOption),
|
||||||
RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
|
RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
|
||||||
{Name, Protocol, Bind, RanchOptions}
|
{Name, Protocol, Bind, RanchOptions}
|
||||||
end || ListenerOption0 <- Listeners].
|
end
|
||||||
|
|| ListenerOption0 <- Listeners
|
||||||
|
].
|
||||||
|
|
||||||
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
|
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
|
||||||
|
|
||||||
|
@ -126,20 +172,26 @@ ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
|
||||||
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
|
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
|
||||||
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
|
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
|
||||||
|
|
||||||
|
init_i18n() ->
|
||||||
|
File = i18n_file(),
|
||||||
|
Lang = emqx_conf:get([dashboard, i18n_lang], en),
|
||||||
|
init_i18n(File, Lang).
|
||||||
|
|
||||||
ranch_opts(RanchOptions) ->
|
ranch_opts(RanchOptions) ->
|
||||||
Keys = [ {ack_timeout, handshake_timeout}
|
Keys = [
|
||||||
, connection_type
|
{ack_timeout, handshake_timeout},
|
||||||
, max_connections
|
connection_type,
|
||||||
, num_acceptors
|
max_connections,
|
||||||
, shutdown
|
num_acceptors,
|
||||||
, socket],
|
shutdown,
|
||||||
|
socket
|
||||||
|
],
|
||||||
{S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys),
|
{S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys),
|
||||||
R#{socket_opts => maps:fold(fun key_only/3, [], S)}.
|
R#{socket_opts => maps:fold(fun key_only/3, [], S)}.
|
||||||
|
|
||||||
|
|
||||||
key_take(Key, {All, R}) ->
|
key_take(Key, {All, R}) ->
|
||||||
{K, KX} = case Key of
|
{K, KX} =
|
||||||
|
case Key of
|
||||||
{K1, K2} -> {K1, K2};
|
{K1, K2} -> {K1, K2};
|
||||||
_ -> {Key, Key}
|
_ -> {Key, Key}
|
||||||
end,
|
end,
|
||||||
|
@ -155,15 +207,17 @@ key_only(_K, false, S) -> S;
|
||||||
key_only(K, V, S) -> [{K, V} | S].
|
key_only(K, V, S) -> [{K, V} | S].
|
||||||
|
|
||||||
listener_name(Protocol, #{port := Port, ip := IP}) ->
|
listener_name(Protocol, #{port := Port, ip := IP}) ->
|
||||||
Name = "dashboard:"
|
Name =
|
||||||
++ atom_to_list(Protocol) ++ ":"
|
"dashboard:" ++
|
||||||
++ inet:ntoa(IP) ++ ":"
|
atom_to_list(Protocol) ++ ":" ++
|
||||||
++ integer_to_list(Port),
|
inet:ntoa(IP) ++ ":" ++
|
||||||
|
integer_to_list(Port),
|
||||||
list_to_atom(Name);
|
list_to_atom(Name);
|
||||||
listener_name(Protocol, #{port := Port}) ->
|
listener_name(Protocol, #{port := Port}) ->
|
||||||
Name = "dashboard:"
|
Name =
|
||||||
++ atom_to_list(Protocol) ++ ":"
|
"dashboard:" ++
|
||||||
++ integer_to_list(Port),
|
atom_to_list(Protocol) ++ ":" ++
|
||||||
|
integer_to_list(Port),
|
||||||
list_to_atom(Name).
|
list_to_atom(Name).
|
||||||
|
|
||||||
authorize(Req) ->
|
authorize(Req) ->
|
||||||
|
@ -180,11 +234,13 @@ authorize(Req) ->
|
||||||
{error, <<"not_allowed">>} ->
|
{error, <<"not_allowed">>} ->
|
||||||
return_unauthorized(
|
return_unauthorized(
|
||||||
?WRONG_USERNAME_OR_PWD,
|
?WRONG_USERNAME_OR_PWD,
|
||||||
<<"Check username/password">>);
|
<<"Check username/password">>
|
||||||
|
);
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
return_unauthorized(
|
return_unauthorized(
|
||||||
?WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET,
|
?WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET,
|
||||||
<<"Check username/password or api_key/api_secret">>)
|
<<"Check username/password or api_key/api_secret">>
|
||||||
|
)
|
||||||
end;
|
end;
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
return_unauthorized(<<"WORNG_USERNAME_OR_PWD">>, <<"Check username/password">>)
|
return_unauthorized(<<"WORNG_USERNAME_OR_PWD">>, <<"Check username/password">>)
|
||||||
|
@ -199,12 +255,22 @@ authorize(Req) ->
|
||||||
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
|
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
return_unauthorized(<<"AUTHORIZATION_HEADER_ERROR">>,
|
return_unauthorized(
|
||||||
<<"Support authorization: basic/bearer ">>)
|
<<"AUTHORIZATION_HEADER_ERROR">>,
|
||||||
|
<<"Support authorization: basic/bearer ">>
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
return_unauthorized(Code, Message) ->
|
return_unauthorized(Code, Message) ->
|
||||||
{401, #{<<"WWW-Authenticate">> =>
|
{401,
|
||||||
<<"Basic Realm=\"minirest-server\"">>},
|
#{
|
||||||
#{code => Code, message => Message}
|
<<"WWW-Authenticate">> =>
|
||||||
}.
|
<<"Basic Realm=\"minirest-server\"">>
|
||||||
|
},
|
||||||
|
#{code => Code, message => Message}}.
|
||||||
|
|
||||||
|
i18n_file() ->
|
||||||
|
case application:get_env(emqx_dashboard, i18n_file) of
|
||||||
|
undefined -> emqx:etc_file("i18n.conf");
|
||||||
|
{ok, File} -> File
|
||||||
|
end.
|
||||||
|
|
|
@ -15,87 +15,130 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_dashboard_schema).
|
-module(emqx_dashboard_schema).
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
||||||
-export([ roots/0
|
-export([
|
||||||
, fields/1
|
roots/0,
|
||||||
, namespace/0
|
fields/1,
|
||||||
, desc/1
|
namespace/0,
|
||||||
|
desc/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
namespace() -> <<"dashboard">>.
|
namespace() -> <<"dashboard">>.
|
||||||
roots() -> ["dashboard"].
|
roots() -> ["dashboard"].
|
||||||
|
|
||||||
fields("dashboard") ->
|
fields("dashboard") ->
|
||||||
[ {listeners,
|
[
|
||||||
sc(hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"),
|
{listeners,
|
||||||
hoconsc:ref(?MODULE, "https")])),
|
sc(
|
||||||
#{ desc =>
|
hoconsc:array(
|
||||||
"HTTP(s) listeners are identified by their protocol type and are
|
hoconsc:union([
|
||||||
used to serve dashboard UI and restful HTTP API.<br>
|
hoconsc:ref(?MODULE, "http"),
|
||||||
Listeners must have a unique combination of port number and IP address.<br>
|
hoconsc:ref(?MODULE, "https")
|
||||||
For example, an HTTP listener can listen on all configured IP addresses
|
])
|
||||||
on a given port for a machine by specifying the IP address 0.0.0.0.<br>
|
),
|
||||||
Alternatively, the HTTP listener can specify a unique IP address for each listener,
|
#{
|
||||||
but use the same port."})}
|
desc =>
|
||||||
, {default_username, fun default_username/1}
|
"HTTP(s) listeners are identified by their protocol type and are\n"
|
||||||
, {default_password, fun default_password/1}
|
"used to serve dashboard UI and restful HTTP API.<br>\n"
|
||||||
, {sample_interval, sc(emqx_schema:duration_s(),
|
"Listeners must have a unique combination of port number and IP address.<br>\n"
|
||||||
#{ default => "10s"
|
"For example, an HTTP listener can listen on all configured IP addresses\n"
|
||||||
, desc => "How often to update metrics displayed in the dashboard.<br/>"
|
"on a given port for a machine by specifying the IP address 0.0.0.0.<br>\n"
|
||||||
|
"Alternatively, the HTTP listener can specify a unique IP address for each listener,\n"
|
||||||
|
"but use the same port."
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{default_username, fun default_username/1},
|
||||||
|
{default_password, fun default_password/1},
|
||||||
|
{sample_interval,
|
||||||
|
sc(
|
||||||
|
emqx_schema:duration_s(),
|
||||||
|
#{
|
||||||
|
default => "10s",
|
||||||
|
desc =>
|
||||||
|
"How often to update metrics displayed in the dashboard.<br/>"
|
||||||
"Note: `sample_interval` should be a divisor of 60."
|
"Note: `sample_interval` should be a divisor of 60."
|
||||||
})}
|
}
|
||||||
, {token_expired_time, sc(emqx_schema:duration(),
|
)},
|
||||||
#{ default => "30m"
|
{token_expired_time,
|
||||||
, desc => "JWT token expiration time."
|
sc(
|
||||||
})}
|
emqx_schema:duration(),
|
||||||
, {cors, fun cors/1}
|
#{
|
||||||
|
default => "30m",
|
||||||
|
desc => "JWT token expiration time."
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{cors, fun cors/1},
|
||||||
|
{i18n_lang, fun i18n_lang/1}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("http") ->
|
fields("http") ->
|
||||||
[ {"protocol", sc(
|
[
|
||||||
|
{"protocol",
|
||||||
|
sc(
|
||||||
hoconsc:enum([http, https]),
|
hoconsc:enum([http, https]),
|
||||||
#{ desc => "HTTP/HTTPS protocol."
|
#{
|
||||||
, required => true
|
desc => ?DESC("protocol"),
|
||||||
, default => http
|
required => true,
|
||||||
})}
|
default => http
|
||||||
, {"bind", fun bind/1}
|
}
|
||||||
, {"num_acceptors", sc(
|
)},
|
||||||
|
{"bind", fun bind/1},
|
||||||
|
{"num_acceptors",
|
||||||
|
sc(
|
||||||
integer(),
|
integer(),
|
||||||
#{ default => 4
|
#{
|
||||||
, desc => "Socket acceptor pool size for TCP protocols."
|
default => 4,
|
||||||
})}
|
desc => "Socket acceptor pool size for TCP protocols."
|
||||||
, {"max_connections",
|
}
|
||||||
sc(integer(),
|
)},
|
||||||
#{ default => 512
|
{"max_connections",
|
||||||
, desc => "Maximum number of simultaneous connections."
|
sc(
|
||||||
})}
|
integer(),
|
||||||
, {"backlog",
|
#{
|
||||||
sc(integer(),
|
default => 512,
|
||||||
#{ default => 1024
|
desc => "Maximum number of simultaneous connections."
|
||||||
, desc => "Defines the maximum length that the queue of pending connections can grow to."
|
}
|
||||||
})}
|
)},
|
||||||
, {"send_timeout",
|
{"backlog",
|
||||||
sc(emqx_schema:duration(),
|
sc(
|
||||||
#{ default => "5s"
|
integer(),
|
||||||
, desc => "Send timeout for the socket."
|
#{
|
||||||
})}
|
default => 1024,
|
||||||
, {"inet6",
|
desc =>
|
||||||
sc(boolean(),
|
"Defines the maximum length that the queue of pending connections can grow to."
|
||||||
#{ default => false
|
}
|
||||||
, desc => "Sets up the listener for IPv6."
|
)},
|
||||||
})}
|
{"send_timeout",
|
||||||
, {"ipv6_v6only",
|
sc(
|
||||||
sc(boolean(),
|
emqx_schema:duration(),
|
||||||
#{ default => false
|
#{
|
||||||
, desc => "Disable IPv4-to-IPv6 mapping for the listener."
|
default => "5s",
|
||||||
})}
|
desc => "Send timeout for the socket."
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{"inet6",
|
||||||
|
sc(
|
||||||
|
boolean(),
|
||||||
|
#{
|
||||||
|
default => false,
|
||||||
|
desc => "Sets up the listener for IPv6."
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{"ipv6_v6only",
|
||||||
|
sc(
|
||||||
|
boolean(),
|
||||||
|
#{
|
||||||
|
default => false,
|
||||||
|
desc => "Disable IPv4-to-IPv6 mapping for the listener."
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("https") ->
|
fields("https") ->
|
||||||
fields("http") ++
|
fields("http") ++
|
||||||
proplists:delete("fail_if_no_peer_cert",
|
proplists:delete(
|
||||||
emqx_schema:server_ssl_opts_schema(#{}, true)).
|
"fail_if_no_peer_cert",
|
||||||
|
emqx_schema:server_ssl_opts_schema(#{}, true)
|
||||||
|
).
|
||||||
|
|
||||||
desc("dashboard") ->
|
desc("dashboard") ->
|
||||||
"Configuration for EMQX dashboard.";
|
"Configuration for EMQX dashboard.";
|
||||||
|
@ -119,23 +162,42 @@ default_username(desc) -> "The default username of the automatically created das
|
||||||
default_username('readOnly') -> true;
|
default_username('readOnly') -> true;
|
||||||
default_username(_) -> undefined.
|
default_username(_) -> undefined.
|
||||||
|
|
||||||
default_password(type) -> string();
|
default_password(type) ->
|
||||||
default_password(default) -> "public";
|
string();
|
||||||
default_password(required) -> true;
|
default_password(default) ->
|
||||||
default_password('readOnly') -> true;
|
"public";
|
||||||
default_password(sensitive) -> true;
|
default_password(required) ->
|
||||||
default_password(desc) -> """
|
true;
|
||||||
The initial default password for dashboard 'admin' user.
|
default_password('readOnly') ->
|
||||||
For safety, it should be changed as soon as possible.""";
|
true;
|
||||||
default_password(_) -> undefined.
|
default_password(sensitive) ->
|
||||||
|
true;
|
||||||
|
default_password(desc) ->
|
||||||
|
""
|
||||||
|
"\n"
|
||||||
|
"The initial default password for dashboard 'admin' user.\n"
|
||||||
|
"For safety, it should be changed as soon as possible."
|
||||||
|
"";
|
||||||
|
default_password(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
cors(type) -> boolean();
|
cors(type) ->
|
||||||
cors(default) -> false;
|
boolean();
|
||||||
cors(required) -> false;
|
cors(default) ->
|
||||||
|
false;
|
||||||
|
cors(required) ->
|
||||||
|
false;
|
||||||
cors(desc) ->
|
cors(desc) ->
|
||||||
"Support Cross-Origin Resource Sharing (CORS).
|
"Support Cross-Origin Resource Sharing (CORS).\n"
|
||||||
Allows a server to indicate any origins (domain, scheme, or port) other than
|
"Allows a server to indicate any origins (domain, scheme, or port) other than\n"
|
||||||
its own from which a browser should permit loading resources.";
|
"its own from which a browser should permit loading resources.";
|
||||||
cors(_) -> undefined.
|
cors(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
i18n_lang(type) -> ?ENUM([en, zh]);
|
||||||
|
i18n_lang(default) -> zh;
|
||||||
|
i18n_lang('readOnly') -> true;
|
||||||
|
i18n_lang(desc) -> "Internationalization language support.";
|
||||||
|
i18n_lang(_) -> undefined.
|
||||||
|
|
||||||
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
||||||
|
|
|
@ -28,98 +28,133 @@
|
||||||
-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/3
|
-export([
|
||||||
, components/2
|
parse_spec_ref/3,
|
||||||
|
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, readOnly,
|
-define(DEFAULT_FIELDS, [
|
||||||
explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
|
example,
|
||||||
|
allowReserved,
|
||||||
|
style,
|
||||||
|
format,
|
||||||
|
readOnly,
|
||||||
|
explode,
|
||||||
|
maxLength,
|
||||||
|
allowEmptyValue,
|
||||||
|
deprecated,
|
||||||
|
minimum,
|
||||||
|
maximum
|
||||||
|
]).
|
||||||
|
|
||||||
-define(INIT_SCHEMA, #{fields => #{}, translations => #{},
|
-define(INIT_SCHEMA, #{
|
||||||
validations => [], namespace => undefined}).
|
fields => #{},
|
||||||
|
translations => #{},
|
||||||
|
validations => [],
|
||||||
|
namespace => undefined
|
||||||
|
}).
|
||||||
|
|
||||||
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
-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/">>,
|
-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
|
||||||
?TO_REF(namespace(_M_), _F_)])).
|
iolist_to_binary([
|
||||||
-define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>,
|
<<"#/components/schemas/">>,
|
||||||
?TO_REF(namespace(_M_), _F_)])).
|
?TO_REF(namespace(_M_), _F_)
|
||||||
|
])
|
||||||
|
).
|
||||||
|
-define(TO_COMPONENTS_PARAM(_M_, _F_),
|
||||||
|
iolist_to_binary([
|
||||||
|
<<"#/components/parameters/">>,
|
||||||
|
?TO_REF(namespace(_M_), _F_)
|
||||||
|
])
|
||||||
|
).
|
||||||
|
|
||||||
-define(MAX_ROW_LIMIT, 1000).
|
-define(MAX_ROW_LIMIT, 1000).
|
||||||
-define(DEFAULT_ROW, 100).
|
-define(DEFAULT_ROW, 100).
|
||||||
|
|
||||||
-type(request() :: #{bindings => map(), query_string => map(), body => map()}).
|
-type request() :: #{bindings => map(), query_string => map(), body => map()}.
|
||||||
-type(request_meta() :: #{module => module(), path => string(), method => atom()}).
|
-type request_meta() :: #{module => module(), path => string(), method => atom()}.
|
||||||
|
|
||||||
-type(filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}).
|
-type filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}.
|
||||||
-type(filter() :: fun((request(), request_meta()) -> filter_result())).
|
-type filter() :: fun((request(), request_meta()) -> filter_result()).
|
||||||
|
|
||||||
-type(spec_opts() :: #{check_schema => boolean() | filter(),
|
-type spec_opts() :: #{
|
||||||
|
check_schema => boolean() | filter(),
|
||||||
translate_body => boolean(),
|
translate_body => boolean(),
|
||||||
schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map())
|
schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map())
|
||||||
}).
|
}.
|
||||||
|
|
||||||
-type(route_path() :: string() | binary()).
|
-type route_path() :: string() | binary().
|
||||||
-type(route_methods() :: map()).
|
-type route_methods() :: map().
|
||||||
-type(route_handler() :: atom()).
|
-type route_handler() :: atom().
|
||||||
-type(route_options() :: #{filter => filter() | undefined}).
|
-type route_options() :: #{filter => filter() | undefined}.
|
||||||
|
|
||||||
-type(api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}).
|
-type api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}.
|
||||||
-type(api_spec_component() :: map()).
|
-type api_spec_component() :: map().
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
%% @equiv spec(Module, #{check_schema => false})
|
%% @equiv spec(Module, #{check_schema => false})
|
||||||
-spec(spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}).
|
-spec spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}.
|
||||||
spec(Module) -> spec(Module, #{check_schema => false}).
|
spec(Module) -> spec(Module, #{check_schema => false}).
|
||||||
|
|
||||||
-spec(spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}).
|
-spec spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}.
|
||||||
spec(Module, Options) ->
|
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, Options),
|
{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],
|
{
|
||||||
Refs ++ AllRefsAcc}
|
[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
|
||||||
end, {[], []}, Paths),
|
Refs ++ AllRefsAcc
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
{[], []},
|
||||||
|
Paths
|
||||||
|
),
|
||||||
{ApiSpec, components(lists:usort(AllRefs), Options)}.
|
{ApiSpec, components(lists:usort(AllRefs), Options)}.
|
||||||
|
|
||||||
-spec(namespace() -> hocon_schema:name()).
|
-spec namespace() -> hocon_schema:name().
|
||||||
namespace() -> "public".
|
namespace() -> "public".
|
||||||
|
|
||||||
-spec(fields(hocon_schema:name()) -> hocon_schema:fields()).
|
-spec fields(hocon_schema:name()) -> hocon_schema:fields().
|
||||||
fields(page) ->
|
fields(page) ->
|
||||||
Desc = <<"Page number of the results to fetch.">>,
|
Desc = <<"Page number of the results to fetch.">>,
|
||||||
Meta = #{in => query, desc => Desc, default => 1, example => 1},
|
Meta = #{in => query, desc => Desc, default => 1, example => 1},
|
||||||
[{page, hoconsc:mk(integer(), Meta)}];
|
[{page, hoconsc:mk(integer(), Meta)}];
|
||||||
fields(limit) ->
|
fields(limit) ->
|
||||||
Desc = iolist_to_binary([<<"Results per page(max ">>,
|
Desc = iolist_to_binary([
|
||||||
integer_to_binary(?MAX_ROW_LIMIT), <<")">>]),
|
<<"Results per page(max ">>,
|
||||||
|
integer_to_binary(?MAX_ROW_LIMIT),
|
||||||
|
<<")">>
|
||||||
|
]),
|
||||||
Meta = #{in => query, desc => Desc, default => ?DEFAULT_ROW, example => 50},
|
Meta = #{in => query, desc => Desc, default => ?DEFAULT_ROW, example => 50},
|
||||||
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
|
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
|
||||||
|
|
||||||
-spec(schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map()).
|
-spec schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map().
|
||||||
schema_with_example(Type, Example) ->
|
schema_with_example(Type, Example) ->
|
||||||
hoconsc:mk(Type, #{examples => #{<<"example">> => Example}}).
|
hoconsc:mk(Type, #{examples => #{<<"example">> => Example}}).
|
||||||
|
|
||||||
-spec(schema_with_examples(hocon_schema:type(), map()) -> hocon_schema:field_schema_map()).
|
-spec schema_with_examples(hocon_schema:type(), map()) -> hocon_schema:field_schema_map().
|
||||||
schema_with_examples(Type, Examples) ->
|
schema_with_examples(Type, Examples) ->
|
||||||
hoconsc:mk(Type, #{examples => #{<<"examples">> => Examples}}).
|
hoconsc:mk(Type, #{examples => #{<<"examples">> => Examples}}).
|
||||||
|
|
||||||
-spec(error_codes(list(atom())) -> hocon_schema:fields()).
|
-spec error_codes(list(atom())) -> hocon_schema:fields().
|
||||||
error_codes(Codes) ->
|
error_codes(Codes) ->
|
||||||
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
|
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
|
||||||
|
|
||||||
-spec(error_codes(nonempty_list(atom()), binary()) -> hocon_schema:fields()).
|
-spec error_codes(nonempty_list(atom()), binary()) -> hocon_schema:fields().
|
||||||
error_codes(Codes = [_ | _], MsgExample) ->
|
error_codes(Codes = [_ | _], MsgExample) ->
|
||||||
[
|
[
|
||||||
{code, hoconsc:mk(hoconsc:enum(Codes))},
|
{code, hoconsc:mk(hoconsc:enum(Codes))},
|
||||||
{message, hoconsc:mk(string(), #{
|
{message,
|
||||||
|
hoconsc:mk(string(), #{
|
||||||
desc => <<"Details description of the error.">>,
|
desc => <<"Details description of the error.">>,
|
||||||
example => MsgExample
|
example => MsgExample
|
||||||
})}
|
})}
|
||||||
|
@ -143,9 +178,12 @@ translate_req(Request, #{module := Module, path := Path, method := Method}, Chec
|
||||||
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
||||||
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
|
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
|
||||||
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
||||||
catch throw:{_, ValidErrors} ->
|
catch
|
||||||
Msg = [io_lib:format("~ts : ~p", [Key, Reason]) ||
|
throw:{_, ValidErrors} ->
|
||||||
{validation_error, #{path := Key, reason := Reason}} <- ValidErrors],
|
Msg = [
|
||||||
|
io_lib:format("~ts : ~p", [Key, Reason])
|
||||||
|
|| {validation_error, #{path := Key, reason := Reason}} <- ValidErrors
|
||||||
|
],
|
||||||
{400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))}
|
{400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -169,30 +207,51 @@ parse_spec_ref(Module, Path, Options) ->
|
||||||
Schema =
|
Schema =
|
||||||
try
|
try
|
||||||
erlang:apply(Module, schema, [Path])
|
erlang:apply(Module, schema, [Path])
|
||||||
catch error: Reason -> %% better error message
|
%% better error message
|
||||||
|
catch
|
||||||
|
error:Reason ->
|
||||||
throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
|
throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
|
||||||
end,
|
end,
|
||||||
{Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) ->
|
{Specs, Refs} = maps:fold(
|
||||||
(not lists:member(Method, ?METHODS))
|
fun(Method, Meta, {Acc, RefsAcc}) ->
|
||||||
andalso throw({error, #{module => Module, path => Path, method => Method}}),
|
(not lists:member(Method, ?METHODS)) andalso
|
||||||
|
throw({error, #{module => Module, path => Path, method => Method}}),
|
||||||
{Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
|
{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)
|
||||||
|
),
|
||||||
{maps:get('operationId', Schema), Specs, Refs}.
|
{maps:get('operationId', Schema), Specs, Refs}.
|
||||||
|
|
||||||
check_parameters(Request, Spec, Module) ->
|
check_parameters(Request, Spec, Module) ->
|
||||||
#{bindings := Bindings, query_string := QueryStr} = Request,
|
#{bindings := Bindings, query_string := QueryStr} = Request,
|
||||||
BindingsBin = maps:fold(fun(Key, Value, Acc) ->
|
BindingsBin = maps:fold(
|
||||||
|
fun(Key, Value, Acc) ->
|
||||||
Acc#{atom_to_binary(Key) => Value}
|
Acc#{atom_to_binary(Key) => Value}
|
||||||
end, #{}, Bindings),
|
end,
|
||||||
|
#{},
|
||||||
|
Bindings
|
||||||
|
),
|
||||||
check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
|
check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
|
||||||
|
|
||||||
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
||||||
check_parameter([?R_REF(LocalMod, Fields) | Spec],
|
check_parameter(
|
||||||
Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
[?R_REF(LocalMod, Fields) | Spec],
|
||||||
check_parameter([?R_REF(Module, Fields) | Spec],
|
Bindings,
|
||||||
Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
QueryStr,
|
||||||
|
LocalMod,
|
||||||
|
BindingsAcc,
|
||||||
|
QueryStrAcc
|
||||||
|
);
|
||||||
|
check_parameter(
|
||||||
|
[?R_REF(Module, Fields) | Spec],
|
||||||
|
Bindings,
|
||||||
|
QueryStr,
|
||||||
|
LocalMod,
|
||||||
|
BindingsAcc,
|
||||||
|
QueryStrAcc
|
||||||
|
) ->
|
||||||
Params = apply(Module, fields, [Fields]),
|
Params = apply(Module, fields, [Fields]),
|
||||||
check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
||||||
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) ->
|
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) ->
|
||||||
|
@ -231,11 +290,14 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
||||||
%% ]}
|
%% ]}
|
||||||
%% ]
|
%% ]
|
||||||
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
|
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
|
||||||
lists:foldl(fun({Name, Type}, Acc) ->
|
lists:foldl(
|
||||||
|
fun({Name, Type}, Acc) ->
|
||||||
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
||||||
maps:merge(Acc, CheckFun(Schema, Body, #{}))
|
maps:merge(Acc, CheckFun(Schema, Body, #{}))
|
||||||
end, #{}, Spec);
|
end,
|
||||||
|
#{},
|
||||||
|
Spec
|
||||||
|
);
|
||||||
%% requestBody => #{content => #{ 'application/octet-stream' =>
|
%% requestBody => #{content => #{ 'application/octet-stream' =>
|
||||||
%% #{schema => #{ type => string, format => binary}}}
|
%% #{schema => #{ type => string, format => binary}}}
|
||||||
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
|
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
|
||||||
|
@ -244,7 +306,7 @@ check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false)when is_map(
|
||||||
%% tags, description, summary, security, deprecated
|
%% tags, description, summary, security, deprecated
|
||||||
meta_to_spec(Meta, Module, Options) ->
|
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, Options),
|
||||||
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
|
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
|
||||||
{
|
{
|
||||||
generate_method_desc(to_spec(Meta, Params, RequestBody, Responses)),
|
generate_method_desc(to_spec(Meta, Params, RequestBody, Responses)),
|
||||||
|
@ -268,10 +330,13 @@ generate_method_desc(Spec) ->
|
||||||
|
|
||||||
parameters(Params, Module) ->
|
parameters(Params, Module) ->
|
||||||
{SpecList, AllRefs} =
|
{SpecList, AllRefs} =
|
||||||
lists:foldl(fun(Param, {Acc, RefsAcc}) ->
|
lists:foldl(
|
||||||
|
fun(Param, {Acc, RefsAcc}) ->
|
||||||
case Param of
|
case Param of
|
||||||
?REF(StructName) -> to_ref(Module, StructName, Acc, RefsAcc);
|
?REF(StructName) ->
|
||||||
?R_REF(RModule, StructName) -> to_ref(RModule, StructName, Acc, RefsAcc);
|
to_ref(Module, StructName, Acc, RefsAcc);
|
||||||
|
?R_REF(RModule, StructName) ->
|
||||||
|
to_ref(RModule, StructName, Acc, RefsAcc);
|
||||||
{Name, Type} ->
|
{Name, Type} ->
|
||||||
In = hocon_schema:field_schema(Type, in),
|
In = hocon_schema:field_schema(Type, in),
|
||||||
In =:= undefined andalso
|
In =:= undefined andalso
|
||||||
|
@ -281,67 +346,99 @@ parameters(Params, Module) ->
|
||||||
HoconType = hocon_schema:field_schema(Type, type),
|
HoconType = hocon_schema:field_schema(Type, type),
|
||||||
Meta = init_meta(Default),
|
Meta = init_meta(Default),
|
||||||
{ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
|
{ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
|
||||||
Spec0 = init_prop([required | ?DEFAULT_FIELDS],
|
Spec0 = init_prop(
|
||||||
#{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type),
|
[required | ?DEFAULT_FIELDS],
|
||||||
|
#{schema => maps:merge(ParamType, Meta), name => Name, in => In},
|
||||||
|
Type
|
||||||
|
),
|
||||||
Spec1 = trans_required(Spec0, Required, In),
|
Spec1 = trans_required(Spec0, Required, In),
|
||||||
Spec2 = trans_desc(Spec1, Type),
|
Spec2 = trans_description(Spec1, Type),
|
||||||
{[Spec2 | Acc], Refs ++ RefsAcc}
|
{[Spec2 | Acc], Refs ++ RefsAcc}
|
||||||
end
|
end
|
||||||
end, {[], []}, Params),
|
end,
|
||||||
|
{[], []},
|
||||||
|
Params
|
||||||
|
),
|
||||||
{lists:reverse(SpecList), AllRefs}.
|
{lists:reverse(SpecList), AllRefs}.
|
||||||
|
|
||||||
init_meta(undefined) -> #{};
|
init_meta(undefined) -> #{};
|
||||||
init_meta(Default) -> #{default => Default}.
|
init_meta(Default) -> #{default => Default}.
|
||||||
|
|
||||||
init_prop(Keys, Init, Type) ->
|
init_prop(Keys, Init, Type) ->
|
||||||
lists:foldl(fun(Key, Acc) ->
|
lists:foldl(
|
||||||
|
fun(Key, Acc) ->
|
||||||
case hocon_schema:field_schema(Type, Key) of
|
case hocon_schema:field_schema(Type, Key) of
|
||||||
undefined -> Acc;
|
undefined -> Acc;
|
||||||
Schema -> Acc#{Key => to_bin(Schema)}
|
Schema -> Acc#{Key => to_bin(Schema)}
|
||||||
end
|
end
|
||||||
end, Init, Keys).
|
end,
|
||||||
|
Init,
|
||||||
|
Keys
|
||||||
|
).
|
||||||
|
|
||||||
trans_required(Spec, true, _) -> Spec#{required => true};
|
trans_required(Spec, true, _) -> Spec#{required => true};
|
||||||
trans_required(Spec, _, path) -> Spec#{required => true};
|
trans_required(Spec, _, path) -> Spec#{required => true};
|
||||||
trans_required(Spec, _, _) -> Spec.
|
trans_required(Spec, _, _) -> Spec.
|
||||||
|
|
||||||
trans_desc(Init, Hocon, Func, Name) ->
|
trans_desc(Init, Hocon, Func, Name) ->
|
||||||
Spec0 = trans_desc(Init, Hocon),
|
Spec0 = trans_description(Init, Hocon),
|
||||||
case Func =:= fun hocon_schema_to_spec/2 of
|
case Func =:= fun hocon_schema_to_spec/2 of
|
||||||
true -> Spec0;
|
true ->
|
||||||
|
Spec0;
|
||||||
false ->
|
false ->
|
||||||
Spec1 = Spec0#{label => Name},
|
Spec1 = trans_label(Spec0, Hocon, Name),
|
||||||
case Spec1 of
|
case Spec1 of
|
||||||
#{description := _} -> Spec1;
|
#{description := _} -> Spec1;
|
||||||
_ -> Spec1#{description => <<Name/binary, " Description">>}
|
_ -> Spec1#{description => <<Name/binary, " Description">>}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
trans_desc(Spec, Hocon) ->
|
trans_description(Spec, Hocon) ->
|
||||||
case hocon_schema:field_schema(Hocon, desc) of
|
case trans_desc(<<"desc">>, Hocon, undefined) of
|
||||||
undefined ->
|
undefined -> Spec;
|
||||||
case hocon_schema:field_schema(Hocon, description) of
|
Value -> Spec#{description => Value}
|
||||||
undefined ->
|
|
||||||
Spec;
|
|
||||||
Desc ->
|
|
||||||
Spec#{description => to_bin(Desc)}
|
|
||||||
end;
|
|
||||||
Desc -> Spec#{description => to_bin(Desc)}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
request_body(#{content := _} = Content, _Module) -> {Content, []};
|
trans_label(Spec, Hocon, Default) ->
|
||||||
request_body([], _Module) -> {[], []};
|
Label = trans_desc(<<"label">>, Hocon, Default),
|
||||||
request_body(Schema, Module) ->
|
Spec#{label => Label}.
|
||||||
|
|
||||||
|
trans_desc(Key, Hocon, Default) ->
|
||||||
|
case resolve_desc(Key, desc_struct(Hocon)) of
|
||||||
|
undefined -> Default;
|
||||||
|
Value -> to_bin(Value)
|
||||||
|
end.
|
||||||
|
|
||||||
|
desc_struct(Hocon) ->
|
||||||
|
case hocon_schema:field_schema(Hocon, desc) of
|
||||||
|
undefined -> hocon_schema:field_schema(Hocon, description);
|
||||||
|
Struct -> Struct
|
||||||
|
end.
|
||||||
|
|
||||||
|
resolve_desc(_Key, Bin) when is_binary(Bin) -> Bin;
|
||||||
|
resolve_desc(Key, Struct) ->
|
||||||
|
{ok, #{cache := Cache, lang := Lang}} = emqx_dashboard:get_i18n(),
|
||||||
|
Desc = hocon_schema:resolve_schema(Struct, Cache),
|
||||||
|
case is_map(Desc) of
|
||||||
|
true -> emqx_map_lib:deep_get([Key, Lang], Desc, undefined);
|
||||||
|
false -> Desc
|
||||||
|
end.
|
||||||
|
|
||||||
|
request_body(#{content := _} = Content, _Module, _Options) ->
|
||||||
|
{Content, []};
|
||||||
|
request_body([], _Module, _Options) ->
|
||||||
|
{[], []};
|
||||||
|
request_body(Schema, Module, Options) ->
|
||||||
{{Props, Refs}, Examples} =
|
{{Props, Refs}, Examples} =
|
||||||
case hoconsc:is_schema(Schema) of
|
case hoconsc:is_schema(Schema) of
|
||||||
true ->
|
true ->
|
||||||
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, Options), undefined}
|
||||||
end,
|
end,
|
||||||
{#{<<"content">> => content(Props, Examples)},
|
{#{<<"content">> => content(Props, Examples)}, Refs}.
|
||||||
Refs}.
|
|
||||||
|
|
||||||
responses(Responses, Module, Options) ->
|
responses(Responses, Module, Options) ->
|
||||||
{Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses),
|
{Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses),
|
||||||
|
@ -359,33 +456,49 @@ response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module, Options}) ->
|
||||||
SchemaToSpec = schema_converter(Options),
|
SchemaToSpec = schema_converter(Options),
|
||||||
{Spec, Refs} = SchemaToSpec(RRef, Module),
|
{Spec, Refs} = SchemaToSpec(RRef, Module),
|
||||||
Content = content(Spec),
|
Content = content(Spec),
|
||||||
{Acc#{integer_to_binary(Status) =>
|
{
|
||||||
#{<<"content">> => Content}}, Refs ++ RefsAcc, Module, Options};
|
Acc#{
|
||||||
|
integer_to_binary(Status) =>
|
||||||
|
#{<<"content">> => Content}
|
||||||
|
},
|
||||||
|
Refs ++ RefsAcc,
|
||||||
|
Module,
|
||||||
|
Options
|
||||||
|
};
|
||||||
response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
|
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),
|
||||||
Examples = hocon_schema:field_schema(Schema, examples),
|
Examples = hocon_schema:field_schema(Schema, examples),
|
||||||
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
|
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
|
||||||
Init = trans_desc(#{}, Schema),
|
Init = trans_description(#{}, Schema),
|
||||||
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, Options
|
Refs ++ RefsAcc,
|
||||||
|
Module,
|
||||||
|
Options
|
||||||
};
|
};
|
||||||
false ->
|
false ->
|
||||||
{Props, Refs} = parse_object(Schema, Module, Options),
|
{Props, Refs} = parse_object(Schema, Module, Options),
|
||||||
Init = trans_desc(#{}, Schema),
|
Init = trans_description(#{}, Schema),
|
||||||
Content = Init#{<<"content">> => content(Props)},
|
Content = Init#{<<"content">> => content(Props)},
|
||||||
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
|
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
components(Refs, Options) ->
|
components(Refs, Options) ->
|
||||||
lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [],
|
lists:sort(
|
||||||
components(Options, Refs, #{}, []))).
|
maps:fold(
|
||||||
|
fun(K, V, Acc) -> [#{K => V} | Acc] end,
|
||||||
|
[],
|
||||||
|
components(Options, Refs, #{}, [])
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
components(_Options, [], SpecAcc, []) -> SpecAcc;
|
components(_Options, [], SpecAcc, []) ->
|
||||||
components(Options, [], SpecAcc, SubRefAcc) -> components(Options, SubRefAcc, SpecAcc, []);
|
SpecAcc;
|
||||||
|
components(Options, [], SpecAcc, SubRefAcc) ->
|
||||||
|
components(Options, SubRefAcc, SpecAcc, []);
|
||||||
components(Options, [{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),
|
||||||
|
@ -404,7 +517,9 @@ hocon_schema_fields(Module, StructName) ->
|
||||||
case apply(Module, fields, [StructName]) of
|
case apply(Module, fields, [StructName]) of
|
||||||
#{fields := Fields, desc := _} ->
|
#{fields := Fields, desc := _} ->
|
||||||
%% evil here, as it's match hocon_schema's internal representation
|
%% evil here, as it's match hocon_schema's internal representation
|
||||||
Fields; %% TODO: make use of desc ?
|
|
||||||
|
%% TODO: make use of desc ?
|
||||||
|
Fields;
|
||||||
Other ->
|
Other ->
|
||||||
Other
|
Other
|
||||||
end.
|
end.
|
||||||
|
@ -419,11 +534,9 @@ namespace(Module) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
|
||||||
[{Module, StructName}]};
|
|
||||||
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), LocalModule), []};
|
{typename_to_spec(typerefl:name(Type), LocalModule), []};
|
||||||
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
||||||
|
@ -435,57 +548,99 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
||||||
{#{type => string, enum => Items}, []};
|
{#{type => string, enum => Items}, []};
|
||||||
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
||||||
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
|
||||||
{#{<<"type">> => object,
|
{
|
||||||
<<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
|
#{
|
||||||
SubRefs};
|
<<"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),
|
||||||
{[Schema | Acc], SubRefs ++ RefsAcc}
|
{[Schema | Acc], SubRefs ++ RefsAcc}
|
||||||
end, {[], []}, Types),
|
end,
|
||||||
|
{[], []},
|
||||||
|
Types
|
||||||
|
),
|
||||||
{#{<<"oneOf">> => OneOf}, Refs};
|
{#{<<"oneOf">> => OneOf}, Refs};
|
||||||
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]}, []}.
|
||||||
|
|
||||||
%% todo: Find a way to fetch enum value from user_id_type().
|
%% todo: Find a way to fetch enum value from user_id_type().
|
||||||
typename_to_spec("user_id_type()", _Mod) -> #{type => string, enum => [clientid, username]};
|
typename_to_spec("user_id_type()", _Mod) ->
|
||||||
typename_to_spec("term()", _Mod) -> #{type => string, example => "any"};
|
#{type => string, enum => [clientid, username]};
|
||||||
typename_to_spec("boolean()", _Mod) -> #{type => boolean, example => true};
|
typename_to_spec("term()", _Mod) ->
|
||||||
typename_to_spec("binary()", _Mod) -> #{type => string, example => <<"binary-example">>};
|
#{type => string, example => "any"};
|
||||||
typename_to_spec("float()", _Mod) -> #{type => number, example => 3.14159};
|
typename_to_spec("boolean()", _Mod) ->
|
||||||
typename_to_spec("integer()", _Mod) -> #{type => integer, example => 100};
|
#{type => boolean, example => true};
|
||||||
typename_to_spec("non_neg_integer()", _Mod) -> #{type => integer, minimum => 1, example => 100};
|
typename_to_spec("binary()", _Mod) ->
|
||||||
typename_to_spec("number()", _Mod) -> #{type => number, example => 42};
|
#{type => string, example => <<"binary-example">>};
|
||||||
typename_to_spec("string()", _Mod) -> #{type => string, example => <<"string-example">>};
|
typename_to_spec("float()", _Mod) ->
|
||||||
typename_to_spec("atom()", _Mod) -> #{type => string, example => atom};
|
#{type => number, example => 3.14159};
|
||||||
|
typename_to_spec("integer()", _Mod) ->
|
||||||
|
#{type => integer, example => 100};
|
||||||
|
typename_to_spec("non_neg_integer()", _Mod) ->
|
||||||
|
#{type => integer, minimum => 1, example => 100};
|
||||||
|
typename_to_spec("number()", _Mod) ->
|
||||||
|
#{type => number, example => 42};
|
||||||
|
typename_to_spec("string()", _Mod) ->
|
||||||
|
#{type => string, example => <<"string-example">>};
|
||||||
|
typename_to_spec("atom()", _Mod) ->
|
||||||
|
#{type => string, example => atom};
|
||||||
typename_to_spec("epoch_second()", _Mod) ->
|
typename_to_spec("epoch_second()", _Mod) ->
|
||||||
#{<<"oneOf">> => [
|
#{
|
||||||
|
<<"oneOf">> => [
|
||||||
#{type => integer, example => 1640995200, description => <<"epoch-second">>},
|
#{type => integer, example => 1640995200, description => <<"epoch-second">>},
|
||||||
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
|
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
typename_to_spec("epoch_millisecond()", _Mod) ->
|
typename_to_spec("epoch_millisecond()", _Mod) ->
|
||||||
#{<<"oneOf">> => [
|
#{
|
||||||
|
<<"oneOf">> => [
|
||||||
#{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
|
#{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
|
||||||
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
|
#{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
typename_to_spec("duration()", _Mod) -> #{type => string, example => <<"12m">>};
|
typename_to_spec("duration()", _Mod) ->
|
||||||
typename_to_spec("duration_s()", _Mod) -> #{type => string, example => <<"1h">>};
|
#{type => string, example => <<"12m">>};
|
||||||
typename_to_spec("duration_ms()", _Mod) -> #{type => string, example => <<"32s">>};
|
typename_to_spec("duration_s()", _Mod) ->
|
||||||
typename_to_spec("percent()", _Mod) -> #{type => number, example => <<"12%">>};
|
#{type => string, example => <<"1h">>};
|
||||||
typename_to_spec("file()", _Mod) -> #{type => string, example => <<"/path/to/file">>};
|
typename_to_spec("duration_ms()", _Mod) ->
|
||||||
typename_to_spec("ip_port()", _Mod) -> #{type => string, example => <<"127.0.0.1:80">>};
|
#{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("ip_ports()", _Mod) ->
|
typename_to_spec("ip_ports()", _Mod) ->
|
||||||
#{type => string, example => <<"127.0.0.1:80, 127.0.0.2:80">>};
|
#{type => string, example => <<"127.0.0.1:80, 127.0.0.2:80">>};
|
||||||
typename_to_spec("url()", _Mod) -> #{type => string, example => <<"http://127.0.0.1">>};
|
typename_to_spec("url()", _Mod) ->
|
||||||
typename_to_spec("connect_timeout()", Mod) -> typename_to_spec("timeout()", Mod);
|
#{type => string, example => <<"http://127.0.0.1">>};
|
||||||
typename_to_spec("timeout()", _Mod) -> #{<<"oneOf">> => [#{type => string, example => infinity},
|
typename_to_spec("connect_timeout()", Mod) ->
|
||||||
#{type => integer, example => 100}], example => infinity};
|
typename_to_spec("timeout()", Mod);
|
||||||
typename_to_spec("bytesize()", _Mod) -> #{type => string, example => <<"32MB">>};
|
typename_to_spec("timeout()", _Mod) ->
|
||||||
typename_to_spec("wordsize()", _Mod) -> #{type => string, example => <<"1024KB">>};
|
#{
|
||||||
typename_to_spec("map()", _Mod) -> #{type => object, example => #{}};
|
<<"oneOf">> => [
|
||||||
typename_to_spec("#{" ++ _, Mod) -> typename_to_spec("map()", Mod);
|
#{type => string, example => infinity},
|
||||||
typename_to_spec("qos()", _Mod) -> #{type => string, enum => [0, 1, 2], example => 0};
|
#{type => integer, example => 100}
|
||||||
typename_to_spec("{binary(), binary()}", _Mod) -> #{type => object, example => #{}};
|
],
|
||||||
|
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 => object, example => #{}};
|
||||||
|
typename_to_spec("#{" ++ _, Mod) ->
|
||||||
|
typename_to_spec("map()", Mod);
|
||||||
|
typename_to_spec("qos()", _Mod) ->
|
||||||
|
#{type => string, enum => [0, 1, 2], example => 0};
|
||||||
|
typename_to_spec("{binary(), binary()}", _Mod) ->
|
||||||
|
#{type => object, example => #{}};
|
||||||
typename_to_spec("comma_separated_list()", _Mod) ->
|
typename_to_spec("comma_separated_list()", _Mod) ->
|
||||||
#{type => string, example => <<"item1,item2">>};
|
#{type => string, example => <<"item1,item2">>};
|
||||||
typename_to_spec("comma_separated_atoms()", _Mod) ->
|
typename_to_spec("comma_separated_atoms()", _Mod) ->
|
||||||
|
@ -493,7 +648,8 @@ typename_to_spec("comma_separated_atoms()", _Mod) ->
|
||||||
typename_to_spec("pool_type()", _Mod) ->
|
typename_to_spec("pool_type()", _Mod) ->
|
||||||
#{type => string, enum => [random, hash], example => hash};
|
#{type => string, enum => [random, hash], example => hash};
|
||||||
typename_to_spec("log_level()", _Mod) ->
|
typename_to_spec("log_level()", _Mod) ->
|
||||||
#{ type => string,
|
#{
|
||||||
|
type => string,
|
||||||
enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
|
enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
|
||||||
};
|
};
|
||||||
typename_to_spec("rate()", _Mod) ->
|
typename_to_spec("rate()", _Mod) ->
|
||||||
|
@ -520,11 +676,13 @@ typename_to_spec(Name, Mod) ->
|
||||||
|
|
||||||
range(Name) ->
|
range(Name) ->
|
||||||
case string:split(Name, "..") of
|
case string:split(Name, "..") of
|
||||||
[MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
|
%% 1..10 1..inf -inf..10
|
||||||
|
[MinStr, MaxStr] ->
|
||||||
Schema = #{type => integer},
|
Schema = #{type => integer},
|
||||||
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
Schema1 = add_integer_prop(Schema, minimum, MinStr),
|
||||||
add_integer_prop(Schema1, maximum, MaxStr);
|
add_integer_prop(Schema1, maximum, MaxStr);
|
||||||
_ -> nomatch
|
_ ->
|
||||||
|
nomatch
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Module:Type
|
%% Module:Type
|
||||||
|
@ -533,21 +691,25 @@ remote_module_type(nomatch, Name, Mod) ->
|
||||||
[_Module, Type] -> typename_to_spec(Type, Mod);
|
[_Module, Type] -> typename_to_spec(Type, Mod);
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
remote_module_type(Spec, _Name, _Mod) -> Spec.
|
remote_module_type(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% [string()] or [integer()] or [xxx].
|
%% [string()] or [integer()] or [xxx].
|
||||||
typerefl_array(nomatch, Name, Mod) ->
|
typerefl_array(nomatch, Name, Mod) ->
|
||||||
case string:trim(Name, leading, "[") of
|
case string:trim(Name, leading, "[") of
|
||||||
Name -> nomatch;
|
Name ->
|
||||||
|
nomatch;
|
||||||
Name1 ->
|
Name1 ->
|
||||||
case string:trim(Name1, trailing, "]") of
|
case string:trim(Name1, trailing, "]") of
|
||||||
Name1 -> notmatch;
|
Name1 ->
|
||||||
|
notmatch;
|
||||||
Name2 ->
|
Name2 ->
|
||||||
Schema = typename_to_spec(Name2, Mod),
|
Schema = typename_to_spec(Name2, Mod),
|
||||||
#{type => array, items => Schema}
|
#{type => array, items => Schema}
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
typerefl_array(Spec, _Name, _Mod) -> Spec.
|
typerefl_array(Spec, _Name, _Mod) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
%% integer(1)
|
%% integer(1)
|
||||||
integer(nomatch, Name) ->
|
integer(nomatch, Name) ->
|
||||||
|
@ -555,7 +717,8 @@ integer(nomatch, Name) ->
|
||||||
{Int, []} -> #{type => integer, enum => [Int], example => Int, default => Int};
|
{Int, []} -> #{type => integer, enum => [Int], example => Int, default => Int};
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end;
|
end;
|
||||||
integer(Spec, _Name) -> Spec.
|
integer(Spec, _Name) ->
|
||||||
|
Spec.
|
||||||
|
|
||||||
add_integer_prop(Schema, Key, Value) ->
|
add_integer_prop(Schema, Key, Value) ->
|
||||||
case string:to_integer(Value) of
|
case string:to_integer(Value) of
|
||||||
|
@ -571,11 +734,13 @@ to_bin(List) when is_list(List) ->
|
||||||
end;
|
end;
|
||||||
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
|
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, Options) 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),
|
||||||
case hoconsc:is_schema(Hocon) of
|
case hoconsc:is_schema(Hocon) of
|
||||||
true ->
|
true ->
|
||||||
|
@ -589,21 +754,33 @@ parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) ->
|
||||||
true -> [NameBin | RequiredAcc];
|
true -> [NameBin | RequiredAcc];
|
||||||
false -> RequiredAcc
|
false -> RequiredAcc
|
||||||
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, Options),
|
{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
|
||||||
|
),
|
||||||
Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)},
|
Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)},
|
||||||
case Required of
|
case Required of
|
||||||
[] -> {Object, Refs};
|
[] -> {Object, Refs};
|
||||||
_ -> {maps:put(required, Required, Object), Refs}
|
_ -> {maps:put(required, Required, Object), Refs}
|
||||||
end;
|
end;
|
||||||
parse_object(Other, Module, Options) ->
|
parse_object(Other, Module, Options) ->
|
||||||
erlang:throw({error,
|
erlang:throw(
|
||||||
#{msg => <<"Object only supports not empty proplists">>,
|
{error, #{
|
||||||
args => Other, module => Module, options => Options}}).
|
msg => <<"Object only supports not empty proplists">>,
|
||||||
|
args => Other,
|
||||||
|
module => Module,
|
||||||
|
options => Options
|
||||||
|
}}
|
||||||
|
).
|
||||||
|
|
||||||
is_required(Hocon) ->
|
is_required(Hocon) ->
|
||||||
hocon_schema:field_schema(Hocon, required) =:= true.
|
hocon_schema:field_schema(Hocon, required) =:= true.
|
||||||
|
|
|
@ -81,12 +81,7 @@ set_special_configs(emqx_management) ->
|
||||||
emqx_config:put([emqx_management], Config),
|
emqx_config:put([emqx_management], Config),
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Listeners = [#{protocol => http, port => 18083}],
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
Config = #{listeners => Listeners,
|
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>
|
|
||||||
},
|
|
||||||
emqx_config:put([dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -35,9 +35,16 @@ set_default_config(DefaultUsername) ->
|
||||||
Config = #{listeners => [#{protocol => http,
|
Config = #{listeners => [#{protocol => http,
|
||||||
port => 18083}],
|
port => 18083}],
|
||||||
default_username => DefaultUsername,
|
default_username => DefaultUsername,
|
||||||
default_password => <<"public">>
|
default_password => <<"public">>,
|
||||||
|
i18n_lang => en
|
||||||
},
|
},
|
||||||
emqx_config:put([dashboard], Config),
|
emqx_config:put([dashboard], Config),
|
||||||
|
I18nFile = filename:join([
|
||||||
|
filename:dirname(code:priv_dir(emqx_dashboard)),
|
||||||
|
"etc",
|
||||||
|
"i18n.conf.all"
|
||||||
|
]),
|
||||||
|
application:set_env(emqx_dashboard, i18n_file, I18nFile),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
request(Method, Url) ->
|
request(Method, Url) ->
|
||||||
|
|
|
@ -35,15 +35,7 @@ init_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([emqx_dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -35,15 +35,7 @@ init_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([emqx_dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -40,15 +40,7 @@ end_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
||||||
|
-export([init_per_suite/1, end_per_suite/1]).
|
||||||
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
|
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
|
||||||
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
||||||
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
|
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
|
||||||
|
@ -26,6 +27,27 @@ groups() -> [
|
||||||
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
mria:start(),
|
||||||
|
application:load(emqx_dashboard),
|
||||||
|
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
|
||||||
|
emqx_dashboard:init_i18n(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
end_per_suite(Config) ->
|
||||||
|
end_suite(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_suite() ->
|
||||||
|
application:unload(emqx_management),
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_dashboard]).
|
||||||
|
|
||||||
t_in_path(_Config) ->
|
t_in_path(_Config) ->
|
||||||
Expect =
|
Expect =
|
||||||
[#{description => <<"Indicates which sorts of issues to return">>,
|
[#{description => <<"Indicates which sorts of issues to return">>,
|
||||||
|
|
|
@ -3,41 +3,36 @@
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
%% API
|
-compile(nowarn_export_all).
|
||||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
-compile(export_all).
|
||||||
-export([t_object/1, t_nest_object/1, t_api_spec/1,
|
|
||||||
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
|
|
||||||
t_ref_array_with_key/1, t_ref_array_without_key/1, t_sub_fields/1
|
|
||||||
]).
|
|
||||||
-export([
|
|
||||||
t_object_trans/1, t_object_notrans/1, t_nest_object_trans/1, t_local_ref_trans/1,
|
|
||||||
t_remote_ref_trans/1, t_nest_ref_trans/1,
|
|
||||||
t_ref_array_with_key_trans/1, t_ref_array_without_key_trans/1,
|
|
||||||
t_ref_trans_error/1, t_object_trans_error/1
|
|
||||||
]).
|
|
||||||
-export([all/0, suite/0, groups/0]).
|
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-import(hoconsc, [mk/2]).
|
-import(hoconsc, [mk/2]).
|
||||||
|
|
||||||
all() -> [{group, spec}, {group, validation}].
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
init_per_suite(Config) ->
|
||||||
groups() -> [
|
mria:start(),
|
||||||
{spec, [parallel], [
|
application:load(emqx_dashboard),
|
||||||
t_api_spec, t_object, t_nest_object,
|
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
|
||||||
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
|
emqx_dashboard:init_i18n(),
|
||||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]},
|
Config.
|
||||||
{validation, [parallel],
|
|
||||||
[
|
set_special_configs(emqx_dashboard) ->
|
||||||
t_object_trans, t_object_notrans, t_local_ref_trans, t_remote_ref_trans,
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
t_ref_array_with_key_trans, t_ref_array_without_key_trans, t_nest_ref_trans,
|
ok;
|
||||||
t_ref_trans_error, t_object_trans_error
|
set_special_configs(_) ->
|
||||||
%% t_nest_object_trans,
|
ok.
|
||||||
]}
|
|
||||||
].
|
end_per_suite(Config) ->
|
||||||
|
end_suite(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_suite() ->
|
||||||
|
application:unload(emqx_management),
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_dashboard]).
|
||||||
|
|
||||||
t_object(_Config) ->
|
t_object(_Config) ->
|
||||||
Spec = #{
|
Spec = #{
|
||||||
|
@ -281,7 +276,7 @@ t_object_notrans(_Config) ->
|
||||||
?assertEqual(Body, ActualBody),
|
?assertEqual(Body, ActualBody),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_nest_object_trans(_Config) ->
|
todo_t_nest_object_check(_Config) ->
|
||||||
Path = "/nest/object",
|
Path = "/nest/object",
|
||||||
Body = #{
|
Body = #{
|
||||||
<<"timeout">> => "10m",
|
<<"timeout">> => "10m",
|
||||||
|
@ -306,7 +301,7 @@ t_nest_object_trans(_Config) ->
|
||||||
body => #{<<"per_page">> => 10,
|
body => #{<<"per_page">> => 10,
|
||||||
<<"timeout">> => 600}
|
<<"timeout">> => 600}
|
||||||
},
|
},
|
||||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
{ok, NewRequest} = check_requestBody(Path, Body),
|
||||||
?assertEqual(Expect, NewRequest),
|
?assertEqual(Expect, NewRequest),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -487,6 +482,10 @@ trans_requestBody(Path, Body) ->
|
||||||
trans_requestBody(Path, Body,
|
trans_requestBody(Path, Body,
|
||||||
fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2).
|
fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2).
|
||||||
|
|
||||||
|
check_requestBody(Path, Body) ->
|
||||||
|
trans_requestBody(Path, Body,
|
||||||
|
fun emqx_dashboard_swagger:filter_check_request/2).
|
||||||
|
|
||||||
trans_requestBody(Path, Body, Filter) ->
|
trans_requestBody(Path, Body, Filter) ->
|
||||||
Meta = #{module => ?MODULE, method => post, path => Path},
|
Meta = #{module => ?MODULE, method => post, path => Path},
|
||||||
Request = #{bindings => #{}, query_string => #{}, body => Body},
|
Request = #{bindings => #{}, query_string => #{}, body => Body},
|
||||||
|
|
|
@ -10,22 +10,31 @@
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-import(hoconsc, [mk/2]).
|
-import(hoconsc, [mk/2]).
|
||||||
|
|
||||||
-export([all/0, suite/0, groups/0]).
|
-compile(nowarn_export_all).
|
||||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
-compile(export_all).
|
||||||
-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_complicated_type/1,
|
|
||||||
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1, t_sub_fields/1,
|
|
||||||
t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]).
|
|
||||||
|
|
||||||
all() -> [{group, spec}].
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
|
||||||
groups() -> [
|
init_per_suite(Config) ->
|
||||||
{spec, [parallel], [
|
mria:start(),
|
||||||
t_api_spec, t_simple_binary, t_object, t_nest_object, t_error, t_complicated_type,
|
application:load(emqx_dashboard),
|
||||||
t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
|
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
|
||||||
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref, t_sub_fields,
|
emqx_dashboard:init_i18n(),
|
||||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
|
Config.
|
||||||
].
|
|
||||||
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
end_per_suite(Config) ->
|
||||||
|
end_suite(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_suite() ->
|
||||||
|
application:unload(emqx_management),
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_dashboard]).
|
||||||
|
|
||||||
t_simple_binary(_config) ->
|
t_simple_binary(_config) ->
|
||||||
Path = "/simple/bin",
|
Path = "/simple/bin",
|
||||||
|
|
|
@ -39,15 +39,7 @@ end_suite(Apps) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
Config = #{
|
emqx_dashboard_api_test_helpers:set_default_config(),
|
||||||
default_username => <<"admin">>,
|
|
||||||
default_password => <<"public">>,
|
|
||||||
listeners => [#{
|
|
||||||
protocol => http,
|
|
||||||
port => 18083
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
emqx_config:put([dashboard], Config),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_App) ->
|
set_special_configs(_App) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
[ {emqx, {path, "../emqx"}},
|
[ {emqx, {path, "../emqx"}},
|
||||||
%% FIXME: tag this as v3.1.3
|
%% FIXME: tag this as v3.1.3
|
||||||
{prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}},
|
{prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}},
|
||||||
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.6"}}}
|
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.7"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
|
|
3
build
3
build
|
@ -82,7 +82,8 @@ make_doc() {
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
erl -noshell -pa $libs_dir1 $libs_dir2 $libs_dir3 -eval \
|
erl -noshell -pa $libs_dir1 $libs_dir2 $libs_dir3 -eval \
|
||||||
"Dir = filename:join(['_build', '${PROFILE}', lib, emqx_dashboard, priv, www, static]), \
|
"Dir = filename:join(['_build', '${PROFILE}', lib, emqx_dashboard, priv, www, static]), \
|
||||||
ok = emqx_conf:dump_schema(Dir, $SCHEMA_MODULE), \
|
I18nFile = filename:join(['_build', '${PROFILE}', lib, emqx_dashboard, etc, 'i18n.conf.all']), \
|
||||||
|
ok = emqx_conf:dump_schema(Dir, $SCHEMA_MODULE, I18nFile), \
|
||||||
halt(0)."
|
halt(0)."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,5 +9,6 @@
|
||||||
doc_gen_test() ->
|
doc_gen_test() ->
|
||||||
Dir = "tmp",
|
Dir = "tmp",
|
||||||
ok = filelib:ensure_dir(filename:join("tmp", foo)),
|
ok = filelib:ensure_dir(filename:join("tmp", foo)),
|
||||||
_ = emqx_conf:dump_schema(Dir, emqx_enterprise_conf_schema),
|
I18nFile = filename:join(["_build", "test", "lib", "emqx_dashboard", "etc", "i18n.conf.all"]),
|
||||||
|
_ = emqx_conf:dump_schema(Dir, emqx_enterprise_conf_schema, I18nFile),
|
||||||
ok.
|
ok.
|
||||||
|
|
9
mix.exs
9
mix.exs
|
@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
# in conflict by emqtt and hocon
|
# in conflict by emqtt and hocon
|
||||||
{:getopt, "1.0.2", override: true},
|
{:getopt, "1.0.2", override: true},
|
||||||
{:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "0.18.0", override: true},
|
{:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "0.18.0", override: true},
|
||||||
{:hocon, github: "emqx/hocon", tag: "0.26.6", override: true},
|
{:hocon, github: "emqx/hocon", tag: "0.26.7", override: true},
|
||||||
{:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.4.1", override: true},
|
{:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.4.1", override: true},
|
||||||
{:esasl, github: "emqx/esasl", tag: "0.2.0"},
|
{:esasl, github: "emqx/esasl", tag: "0.2.0"},
|
||||||
{:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},
|
{:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},
|
||||||
|
@ -342,6 +342,13 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
Path.join(etc, "certs")
|
Path.join(etc, "certs")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# required by emqx_dashboard
|
||||||
|
Mix.Generator.copy_file(
|
||||||
|
"apps/emqx_dashboard/etc/i18n.conf.all",
|
||||||
|
Path.join(etc, "i18n.conf"),
|
||||||
|
force: overwrite?
|
||||||
|
)
|
||||||
|
|
||||||
# this is required by the produced escript / nodetool
|
# this is required by the produced escript / nodetool
|
||||||
Mix.Generator.copy_file(
|
Mix.Generator.copy_file(
|
||||||
Path.join(release.version_path, "start_clean.boot"),
|
Path.join(release.version_path, "start_clean.boot"),
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
, {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.2"}}}
|
, {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.2"}}}
|
||||||
, {getopt, "1.0.2"}
|
, {getopt, "1.0.2"}
|
||||||
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
|
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
|
||||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.6"}}}
|
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.7"}}}
|
||||||
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.1"}}}
|
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.1"}}}
|
||||||
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
|
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
|
||||||
, {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}
|
, {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}
|
||||||
|
|
|
@ -450,10 +450,12 @@ emqx_etc_overlay_common() ->
|
||||||
|
|
||||||
emqx_etc_overlay_per_edition(ce) ->
|
emqx_etc_overlay_per_edition(ce) ->
|
||||||
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
|
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
|
||||||
|
, {"{{base_dir}}/lib/emqx_dashboard/etc/i18n.conf.all", "etc/i18n.conf"}
|
||||||
];
|
];
|
||||||
emqx_etc_overlay_per_edition(ee) ->
|
emqx_etc_overlay_per_edition(ee) ->
|
||||||
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx_enterprise.conf.all", "etc/emqx_enterprise.conf"}
|
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx_enterprise.conf.all", "etc/emqx_enterprise.conf"}
|
||||||
, {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
|
, {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
|
||||||
|
, {"{{base_dir}}/lib/emqx_dashboard/etc/i18n.conf.all", "etc/i18n.conf"}
|
||||||
].
|
].
|
||||||
|
|
||||||
get_vsn(Profile) ->
|
get_vsn(Profile) ->
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env escript
|
||||||
|
|
||||||
|
-mode(compile).
|
||||||
|
|
||||||
|
main(_) ->
|
||||||
|
{ok, BaseConf} = file:read_file("apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf"),
|
||||||
|
|
||||||
|
Cfgs = get_all_cfgs("apps/"),
|
||||||
|
Conf = [merge(BaseConf, Cfgs),
|
||||||
|
io_lib:nl()
|
||||||
|
],
|
||||||
|
ok = file:write_file("apps/emqx_dashboard/etc/i18n.conf.all", Conf).
|
||||||
|
|
||||||
|
merge(BaseConf, Cfgs) ->
|
||||||
|
lists:foldl(
|
||||||
|
fun(CfgFile, Acc) ->
|
||||||
|
case filelib:is_regular(CfgFile) of
|
||||||
|
true ->
|
||||||
|
{ok, Bin1} = file:read_file(CfgFile),
|
||||||
|
[Acc, io_lib:nl(), Bin1];
|
||||||
|
false -> Acc
|
||||||
|
end
|
||||||
|
end, BaseConf, Cfgs).
|
||||||
|
|
||||||
|
get_all_cfgs(Root) ->
|
||||||
|
Apps = filelib:wildcard("*", Root) -- ["emqx_machine", "emqx_dashboard"],
|
||||||
|
Dirs = [filename:join([Root, App]) || App <- Apps],
|
||||||
|
lists:foldl(fun get_cfgs/2, [], Dirs).
|
||||||
|
|
||||||
|
get_all_cfgs(Dir, Cfgs) ->
|
||||||
|
Fun = fun(E, Acc) ->
|
||||||
|
Path = filename:join([Dir, E]),
|
||||||
|
get_cfgs(Path, Acc)
|
||||||
|
end,
|
||||||
|
lists:foldl(Fun, Cfgs, filelib:wildcard("*", Dir)).
|
||||||
|
|
||||||
|
get_cfgs(Dir, Cfgs) ->
|
||||||
|
case filelib:is_dir(Dir) of
|
||||||
|
false ->
|
||||||
|
Cfgs;
|
||||||
|
_ ->
|
||||||
|
Files = filelib:wildcard("*", Dir),
|
||||||
|
case lists:member("i18n", Files) of
|
||||||
|
false ->
|
||||||
|
try_enter_child(Dir, Files, Cfgs);
|
||||||
|
true ->
|
||||||
|
EtcDir = filename:join([Dir, "i18n"]),
|
||||||
|
Confs = filelib:wildcard("*.conf", EtcDir),
|
||||||
|
NewCfgs = [filename:join([EtcDir, Name]) || Name <- Confs],
|
||||||
|
try_enter_child(Dir, Files, NewCfgs ++ Cfgs)
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
try_enter_child(Dir, Files, Cfgs) ->
|
||||||
|
case lists:member("src", Files) of
|
||||||
|
false ->
|
||||||
|
Cfgs;
|
||||||
|
true ->
|
||||||
|
get_all_cfgs(filename:join([Dir, "src"]), Cfgs)
|
||||||
|
end.
|
Loading…
Reference in New Issue