Merge pull request #7590 from zhongwencool/doc-i18n

feat: desc/label support i18n
This commit is contained in:
zhongwencool 2022-04-15 17:43:16 +08:00 committed by GitHub
commit ce915f0bbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1110 additions and 609 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.eunit .eunit
*.conf.all
test-data/ test-data/
deps deps
!deps/.placeholder !deps/.placeholder

View File

@ -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)

View File

@ -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"}}}

View File

@ -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]),

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,12 @@
emqx_dashboard_schema {
protocol {
desc {
en: "Protocol Name"
zh: "协议名"
}
label: {
en: "Protocol"
zh: "协议"
}
}
}

View File

@ -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.

View File

@ -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).

View File

@ -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.

View File

@ -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.

View File

@ -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) ->

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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">>,

View File

@ -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},

View File

@ -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",

View File

@ -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.

View File

@ -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
View File

@ -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)."
} }

View File

@ -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.

View File

@ -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"),

View File

@ -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"}}}

View File

@ -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) ->

60
scripts/merge-i18n.escript Executable file
View File

@ -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.