diff --git a/.gitignore b/.gitignore
index eda7d5652..4c689ce9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.eunit
+*.conf.all
test-data/
deps
!deps/.placeholder
diff --git a/Makefile b/Makefile
index 96eb3ad6e..50e8f0ed6 100644
--- a/Makefile
+++ b/Makefile
@@ -61,7 +61,7 @@ get-dashboard:
@$(SCRIPTS)/get-dashboard.sh
.PHONY: eunit
-eunit: $(REBAR)
+eunit: $(REBAR) conf-segs
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c
.PHONY: proper
@@ -218,6 +218,7 @@ $(foreach zt,$(ALL_DOCKERS),$(eval $(call gen-docker-target,$(zt))))
.PHONY:
conf-segs:
@scripts/merge-config.escript
+ @scripts/merge-i18n.escript
## elixir target is to create release packages using Elixir's Mix
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)
diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config
index 51418dce2..4221bd80d 100644
--- a/apps/emqx/rebar.config
+++ b/apps/emqx/rebar.config
@@ -30,7 +30,7 @@
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
{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"}}},
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
diff --git a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl
index 58ca56d7d..5d7ebf57e 100644
--- a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl
+++ b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl
@@ -31,9 +31,11 @@
-define(TOPICS, [?TOPIC_C, ?TOPIC_U, ?TOPIC_H, ?TOPIC_P, ?TOPIC_A, ?TOPIC_S]).
--define(ENSURE_TOPICS , [<<"/c/auto_sub_c">>
- , <<"/u/auto_sub_u">>
- , ?TOPIC_S]).
+-define(ENSURE_TOPICS, [
+ <<"/c/auto_sub_c">>,
+ <<"/u/auto_sub_u">>,
+ ?TOPIC_S
+]).
-define(CLIENT_ID, <<"auto_sub_c">>).
-define(CLIENT_USERNAME, <<"auto_sub_u">>).
@@ -45,60 +47,58 @@ init_per_suite(Config) ->
mria:start(),
application:stop(?APP),
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
- meck:expect(emqx_schema, fields, fun("auto_subscribe") ->
- meck:passthrough(["auto_subscribe"]) ++
- emqx_auto_subscribe_schema:fields("auto_subscribe");
- (F) -> meck:passthrough([F])
- end),
+ meck:expect(emqx_schema, fields, fun
+ ("auto_subscribe") ->
+ meck:passthrough(["auto_subscribe"]) ++
+ emqx_auto_subscribe_schema:fields("auto_subscribe");
+ (F) ->
+ meck:passthrough([F])
+ end),
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end),
- meck:expect(emqx_resource, remove, fun(_) -> ok end ),
+ meck:expect(emqx_resource, remove, fun(_) -> ok end),
application:load(emqx_dashboard),
application:load(?APP),
- ok = emqx_common_test_helpers:load_config(emqx_auto_subscribe_schema,
- <<"auto_subscribe {
- topics = [
- {
- topic = \"/c/${clientid}\"
- },
- {
- topic = \"/u/${username}\"
- },
- {
- topic = \"/h/${host}\"
- },
- {
- topic = \"/p/${port}\"
- },
- {
- topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"
- },
- {
- topic = \"/topic/simple\"
- qos = 1
- rh = 0
- rap = 0
- nl = 0
- }
- ]
- }">>),
- emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard, ?APP],
- fun set_special_configs/1),
+ ok = emqx_common_test_helpers:load_config(
+ emqx_auto_subscribe_schema,
+ <<"auto_subscribe {\n"
+ " topics = [\n"
+ " {\n"
+ " topic = \"/c/${clientid}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/u/${username}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/h/${host}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/p/${port}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/topic/simple\"\n"
+ " qos = 1\n"
+ " rh = 0\n"
+ " rap = 0\n"
+ " nl = 0\n"
+ " }\n"
+ " ]\n"
+ " }">>
+ ),
+ emqx_common_test_helpers:start_apps(
+ [emqx_conf, emqx_dashboard, ?APP],
+ fun set_special_configs/1
+ ),
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([emqx_dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
@@ -106,10 +106,10 @@ set_special_configs(_) ->
topic_config(T) ->
#{
topic => T,
- qos => 0,
- rh => 0,
- rap => 0,
- nl => 0
+ qos => 0,
+ rh => 0,
+ rap => 0,
+ nl => 0
}.
end_per_suite(_) ->
@@ -148,7 +148,6 @@ t_update(_) ->
?assertEqual(1, erlang:length(GETResponseMap)),
ok.
-
check_subs(Count) ->
Subs = ets:tab2list(emqx_suboption),
ct:pal("---> ~p ~p ~n", [Subs, Count]),
diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl
index 372875d25..f8eabbbd1 100644
--- a/apps/emqx_conf/src/emqx_conf.erl
+++ b/apps/emqx_conf/src/emqx_conf.erl
@@ -25,7 +25,7 @@
-export([update/3, update/4]).
-export([remove/2, remove/3]).
-export([reset/2, reset/3]).
--export([dump_schema/1, dump_schema/2]).
+-export([dump_schema/1, dump_schema/3]).
-export([schema_module/0]).
%% for rpc
@@ -80,15 +80,22 @@ get_node_and_config(KeyPath) ->
{node(), emqx:get_config(KeyPath, config_not_found)}.
%% @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(),
- emqx_config:update_opts()) ->
+-spec update(
+ emqx_map_lib:config_key_path(),
+ emqx_config:update_request(),
+ emqx_config:update_opts()
+) ->
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
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.
--spec update(node(), emqx_map_lib:config_key_path(), emqx_config:update_request(),
- emqx_config:update_opts()) ->
+-spec update(
+ 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().
update(Node, KeyPath, UpdateReq, Opts0) when Node =:= node() ->
emqx:update_config(KeyPath, UpdateReq, Opts0#{override_to => local});
@@ -126,25 +133,41 @@ reset(Node, KeyPath, Opts) ->
%% @doc Called from build script.
-spec dump_schema(file:name_all()) -> ok.
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) ->
- SchemaMdFile = filename:join([Dir, "config.md"]),
- io:format(user, "===< Generating: ~s~n", [SchemaMdFile ]),
- ok = gen_doc(SchemaMdFile, SchemaModule),
+dump_schema(Dir, SchemaModule, I18nFile) ->
+ lists:foreach(
+ fun(Lang) ->
+ 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"]),
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}]),
- ok = file:write_file(SchemaJsonFile, IoData),
+ ok = file:write_file(SchemaJsonFile, IoData).
- %% hot-update configuration schema
- HotConfigSchemaFile = filename:join([Dir, "hot-config-schema.json"]),
+gen_hot_conf_schema_json(Dir, I18nFile, Lang) ->
+ 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]),
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.
-spec schema_module() -> module().
@@ -158,63 +181,96 @@ schema_module() ->
%% Internal functions
%%--------------------------------------------------------------------
--spec gen_doc(file:name_all(), module()) -> ok.
-gen_doc(File, SchemaModule) ->
+-spec gen_doc(file:name_all(), module(), file:name_all(), string()) -> ok.
+gen_doc(File, SchemaModule, I18nFile, Lang) ->
Version = emqx_release:version(),
Title = "# " ++ emqx_release:description() ++ " " ++ Version ++ " Configuration",
BodyFile = filename:join([code:lib_dir(emqx_conf), "etc", "emqx_conf.md"]),
{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).
check_cluster_rpc_result(Result) ->
case Result of
- {ok, _TnxId, Res} -> Res;
+ {ok, _TnxId, Res} ->
+ Res;
{retry, TnxId, Res, Nodes} ->
%% The init MFA return ok, but other nodes failed.
%% We return ok and alert an alarm.
- ?SLOG(error, #{msg => "failed_to_update_config_in_cluster", nodes => Nodes,
- tnx_id => TnxId}),
+ ?SLOG(error, #{
+ msg => "failed_to_update_config_in_cluster",
+ nodes => Nodes,
+ tnx_id => TnxId
+ }),
Res;
- {error, Error} -> %% all MFA return not ok or {ok, term()}.
+ %% all MFA return not ok or {ok, term()}.
+ {error, Error} ->
Error
end.
%% Only gen hot_conf schema, not all configuration fields.
gen_hot_conf_schema(File) ->
- {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(emqx_mgmt_api_configs,
- #{schema_converter => fun hocon_schema_to_spec/2}),
- ApiSpec = lists:foldl(fun({Path, Spec, _, _}, Acc) ->
- NewSpec = maps:fold(fun(Method, #{responses := Responses}, SubAcc) ->
- case Responses of
- #{<<"200">> :=
- #{<<"content">> := #{<<"application/json">> := #{<<"schema">> := Schema}}}} ->
- SubAcc#{Method => Schema};
- _ -> SubAcc
- end
- end, #{}, Spec),
- Acc#{list_to_atom(Path) => NewSpec} end, #{}, ApiSpec0),
+ {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(
+ emqx_mgmt_api_configs,
+ #{schema_converter => fun hocon_schema_to_spec/2}
+ ),
+ ApiSpec = lists:foldl(
+ fun({Path, Spec, _, _}, Acc) ->
+ NewSpec = maps:fold(
+ fun(Method, #{responses := Responses}, SubAcc) ->
+ case Responses of
+ #{
+ <<"200">> :=
+ #{
+ <<"content">> := #{
+ <<"application/json">> := #{<<"schema">> := Schema}
+ }
+ }
+ } ->
+ SubAcc#{Method => Schema};
+ _ ->
+ SubAcc
+ end
+ end,
+ #{},
+ Spec
+ ),
+ Acc#{list_to_atom(Path) => NewSpec}
+ end,
+ #{},
+ ApiSpec0
+ ),
Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
- IoData = jsx:encode(#{
- info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
- paths => ApiSpec,
- components => #{schemas => Components}
- }, [space, {indent, 4}]),
+ IoData = jsx:encode(
+ #{
+ info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
+ paths => ApiSpec,
+ components => #{schemas => Components}
+ },
+ [space, {indent, 4}]
+ ),
file:write_file(File, IoData).
--define(INIT_SCHEMA, #{fields => #{}, translations => #{},
- validations => [], namespace => undefined}).
+-define(INIT_SCHEMA, #{
+ fields => #{},
+ translations => #{},
+ validations => [],
+ namespace => undefined
+}).
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
--define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>,
- ?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)])).
+-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
+ iolist_to_binary([
+ <<"#/components/schemas/">>,
+ ?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)
+ ])
+).
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
- [{Module, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
- [{LocalModule, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, [{LocalModule, StructName}]};
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
{typename_to_spec(typerefl:name(Type), LocalModule), []};
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
@@ -226,50 +282,97 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
{#{type => enum, symbols => Items}, []};
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {#{<<"type">> => object,
- <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
- SubRefs};
+ {
+ #{
+ <<"type">> => object,
+ <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}
+ },
+ SubRefs
+ };
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
- {OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
- {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {[Schema | Acc], SubRefs ++ RefsAcc}
- end, {[], []}, Types),
+ {OneOf, Refs} = lists:foldl(
+ fun(Type, {Acc, RefsAcc}) ->
+ {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
+ {[Schema | Acc], SubRefs ++ RefsAcc}
+ end,
+ {[], []},
+ Types
+ ),
{#{<<"oneOf">> => OneOf}, Refs};
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
{#{type => enum, symbols => [Atom]}, []}.
-typename_to_spec("user_id_type()", _Mod) -> #{type => enum, symbols => [clientid, username]};
-typename_to_spec("term()", _Mod) -> #{type => string};
-typename_to_spec("boolean()", _Mod) -> #{type => boolean};
-typename_to_spec("binary()", _Mod) -> #{type => string};
-typename_to_spec("float()", _Mod) -> #{type => number};
-typename_to_spec("integer()", _Mod) -> #{type => number};
-typename_to_spec("non_neg_integer()", _Mod) -> #{type => number, minimum => 1};
-typename_to_spec("number()", _Mod) -> #{type => number};
-typename_to_spec("string()", _Mod) -> #{type => string};
-typename_to_spec("atom()", _Mod) -> #{type => string};
-
-typename_to_spec("duration()", _Mod) -> #{type => duration};
-typename_to_spec("duration_s()", _Mod) -> #{type => duration};
-typename_to_spec("duration_ms()", _Mod) -> #{type => duration};
-typename_to_spec("percent()", _Mod) -> #{type => percent};
-typename_to_spec("file()", _Mod) -> #{type => 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("user_id_type()", _Mod) ->
+ #{type => enum, symbols => [clientid, username]};
+typename_to_spec("term()", _Mod) ->
+ #{type => string};
+typename_to_spec("boolean()", _Mod) ->
+ #{type => boolean};
+typename_to_spec("binary()", _Mod) ->
+ #{type => string};
+typename_to_spec("float()", _Mod) ->
+ #{type => number};
+typename_to_spec("integer()", _Mod) ->
+ #{type => number};
+typename_to_spec("non_neg_integer()", _Mod) ->
+ #{type => number, minimum => 1};
+typename_to_spec("number()", _Mod) ->
+ #{type => number};
+typename_to_spec("string()", _Mod) ->
+ #{type => string};
+typename_to_spec("atom()", _Mod) ->
+ #{type => string};
+typename_to_spec("duration()", _Mod) ->
+ #{type => duration};
+typename_to_spec("duration_s()", _Mod) ->
+ #{type => duration};
+typename_to_spec("duration_ms()", _Mod) ->
+ #{type => duration};
+typename_to_spec("percent()", _Mod) ->
+ #{type => percent};
+typename_to_spec("file()", _Mod) ->
+ #{type => 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) ->
- #{type => enum, symbols => [debug, info, notice, warning, error,
- critical, alert, emergency, all]};
-typename_to_spec("rate()", _Mod) -> #{type => string};
-typename_to_spec("capacity()", _Mod) -> #{type => string};
-typename_to_spec("burst_rate()", _Mod) -> #{type => string};
-typename_to_spec("failure_strategy()", _Mod) -> #{type => enum, symbols => [force, drop, throw]};
-typename_to_spec("initial()", _Mod) -> #{type => string};
+ #{
+ type => enum,
+ symbols => [
+ debug,
+ info,
+ notice,
+ warning,
+ error,
+ critical,
+ alert,
+ emergency,
+ all
+ ]
+ };
+typename_to_spec("rate()", _Mod) ->
+ #{type => string};
+typename_to_spec("capacity()", _Mod) ->
+ #{type => string};
+typename_to_spec("burst_rate()", _Mod) ->
+ #{type => string};
+typename_to_spec("failure_strategy()", _Mod) ->
+ #{type => enum, symbols => [force, drop, throw]};
+typename_to_spec("initial()", _Mod) ->
+ #{type => string};
typename_to_spec(Name, Mod) ->
Spec = range(Name),
Spec1 = remote_module_type(Spec, Name, Mod),
@@ -282,11 +385,13 @@ default_type(Type) -> Type.
range(Name) ->
case string:split(Name, "..") of
- [MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
+ %% 1..10 1..inf -inf..10
+ [MinStr, MaxStr] ->
Schema = #{type => number},
Schema1 = add_integer_prop(Schema, minimum, MinStr),
add_integer_prop(Schema1, maximum, MaxStr);
- _ -> nomatch
+ _ ->
+ nomatch
end.
%% Module:Type
@@ -295,21 +400,25 @@ remote_module_type(nomatch, Name, Mod) ->
[_Module, Type] -> typename_to_spec(Type, Mod);
_ -> nomatch
end;
-remote_module_type(Spec, _Name, _Mod) -> Spec.
+remote_module_type(Spec, _Name, _Mod) ->
+ Spec.
%% [string()] or [integer()] or [xxx].
typerefl_array(nomatch, Name, Mod) ->
case string:trim(Name, leading, "[") of
- Name -> nomatch;
+ Name ->
+ nomatch;
Name1 ->
case string:trim(Name1, trailing, "]") of
- Name1 -> notmatch;
+ Name1 ->
+ notmatch;
Name2 ->
Schema = typename_to_spec(Name2, Mod),
#{type => array, items => Schema}
end
end;
-typerefl_array(Spec, _Name, _Mod) -> Spec.
+typerefl_array(Spec, _Name, _Mod) ->
+ Spec.
%% integer(1)
integer(nomatch, Name) ->
@@ -317,12 +426,13 @@ integer(nomatch, Name) ->
{Int, []} -> #{type => enum, symbols => [Int], default => Int};
_ -> nomatch
end;
-integer(Spec, _Name) -> Spec.
+integer(Spec, _Name) ->
+ Spec.
add_integer_prop(Schema, Key, Value) ->
case string:to_integer(Value) of
{error, no_integer} -> Schema;
- {Int, []}when Key =:= minimum -> Schema#{Key => Int};
+ {Int, []} when Key =:= minimum -> Schema#{Key => Int};
{Int, []} -> Schema#{Key => Int}
end.
@@ -333,4 +443,5 @@ to_bin(List) when is_list(List) ->
end;
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
-to_bin(X) -> X.
+to_bin(X) ->
+ X.
diff --git a/apps/emqx_conf/test/emqx_conf_schema_tests.erl b/apps/emqx_conf/test/emqx_conf_schema_tests.erl
index 1d962b514..b222a49ae 100644
--- a/apps/emqx_conf/test/emqx_conf_schema_tests.erl
+++ b/apps/emqx_conf/test/emqx_conf_schema_tests.erl
@@ -9,5 +9,6 @@
doc_gen_test() ->
Dir = "tmp",
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.
diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf
new file mode 100644
index 000000000..2a588b752
--- /dev/null
+++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf
@@ -0,0 +1,12 @@
+emqx_dashboard_schema {
+ protocol {
+ desc {
+ en: "Protocol Name"
+ zh: "协议名"
+ }
+ label: {
+ en: "Protocol"
+ zh: "协议"
+ }
+ }
+}
diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl
index 513d7ff50..c0f9cddf8 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard.erl
@@ -18,11 +18,19 @@
-define(APP, ?MODULE).
+-export([
+ start_listeners/0,
+ start_listeners/1,
+ stop_listeners/1,
+ stop_listeners/0
+]).
--export([ start_listeners/0
- , start_listeners/1
- , stop_listeners/1
- , stop_listeners/0]).
+-export([
+ init_i18n/2,
+ init_i18n/0,
+ get_i18n/0,
+ clear_i18n/0
+]).
%% Authorization
-export([authorize/1]).
@@ -48,6 +56,7 @@ stop_listeners() ->
start_listeners(Listeners) ->
{ok, _} = application:ensure_all_started(minirest),
+ init_i18n(),
Authorization = {?MODULE, authorize},
GlobalSpec = #{
openapi => "3.0.0",
@@ -58,12 +67,15 @@ start_listeners(Listeners) ->
'securitySchemes' => #{
'basicAuth' => #{type => http, scheme => basic},
'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, []}
- , {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
- ],
+ }
+ }
+ },
+ 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, []},
+ {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
+ ],
BaseMinirest = #{
base_path => ?BASE_PATH,
modules => minirest_api:find_api_modules(apps()),
@@ -74,75 +86,115 @@ start_listeners(Listeners) ->
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
},
Res =
- lists:foldl(fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
- Minirest = BaseMinirest#{protocol => Protocol},
- case minirest:start(Name, RanchOptions, Minirest) of
- {ok, _} ->
- ?ULOG("Start listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Bind)]),
- Acc;
- {error, _Reason} ->
- %% Don't record the reason because minirest already does(too much logs noise).
- [Name | Acc]
- end
- end, [], listeners(Listeners)),
+ lists:foldl(
+ fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
+ Minirest = BaseMinirest#{protocol => Protocol},
+ case minirest:start(Name, RanchOptions, Minirest) of
+ {ok, _} ->
+ ?ULOG("Start listener ~ts on ~ts successfully.~n", [
+ Name, emqx_listeners:format_addr(Bind)
+ ]),
+ Acc;
+ {error, _Reason} ->
+ %% Don't record the reason because minirest already does(too much logs noise).
+ [Name | Acc]
+ end
+ end,
+ [],
+ listeners(Listeners)
+ ),
+ clear_i18n(),
case Res of
[] -> ok;
_ -> {error, Res}
end.
stop_listeners(Listeners) ->
- [begin
- case minirest:stop(Name) of
- ok ->
- ?ULOG("Stop listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Port)]);
- {error, not_found} ->
- ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
+ [
+ begin
+ case minirest:stop(Name) of
+ ok ->
+ ?ULOG("Stop listener ~ts on ~ts successfully.~n", [
+ Name, emqx_listeners:format_addr(Port)
+ ]);
+ {error, not_found} ->
+ ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
+ end
end
- end || {Name, _, Port, _} <- listeners(Listeners)],
+ || {Name, _, Port, _} <- listeners(Listeners)
+ ],
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
apps() ->
- [App || {App, _, _} <- application:loaded_applications(),
+ [
+ App
+ || {App, _, _} <- application:loaded_applications(),
case re:run(atom_to_list(App), "^emqx") of
- {match,[{0,4}]} -> true;
+ {match, [{0, 4}]} -> true;
_ -> false
- end].
+ end
+ ].
listeners(Listeners) ->
- [begin
- Protocol = maps:get(protocol, ListenerOption0, http),
- {ListenerOption, Bind} = ip_port(ListenerOption0),
- Name = listener_name(Protocol, ListenerOption),
- RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
- {Name, Protocol, Bind, RanchOptions}
- end || ListenerOption0 <- Listeners].
+ [
+ begin
+ Protocol = maps:get(protocol, ListenerOption0, http),
+ {ListenerOption, Bind} = ip_port(ListenerOption0),
+ Name = listener_name(Protocol, ListenerOption),
+ RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
+ {Name, Protocol, Bind, RanchOptions}
+ end
+ || ListenerOption0 <- Listeners
+ ].
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
-ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
+ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, 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) ->
- Keys = [ {ack_timeout, handshake_timeout}
- , connection_type
- , max_connections
- , num_acceptors
- , shutdown
- , socket],
+ Keys = [
+ {ack_timeout, handshake_timeout},
+ connection_type,
+ max_connections,
+ num_acceptors,
+ shutdown,
+ socket
+ ],
{S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys),
R#{socket_opts => maps:fold(fun key_only/3, [], S)}.
-
-key_take(Key, {All, R}) ->
- {K, KX} = case Key of
- {K1, K2} -> {K1, K2};
- _ -> {Key, Key}
- end,
+key_take(Key, {All, R}) ->
+ {K, KX} =
+ case Key of
+ {K1, K2} -> {K1, K2};
+ _ -> {Key, Key}
+ end,
case maps:get(K, All, undefined) of
undefined ->
{All, R};
@@ -150,20 +202,22 @@ key_take(Key, {All, R}) ->
{maps:remove(K, All), R#{KX => V}}
end.
-key_only(K , true , S) -> [K | S];
-key_only(_K, false, S) -> S;
-key_only(K , V , S) -> [{K, V} | S].
+key_only(K, true, S) -> [K | S];
+key_only(_K, false, S) -> S;
+key_only(K, V, S) -> [{K, V} | S].
listener_name(Protocol, #{port := Port, ip := IP}) ->
- Name = "dashboard:"
- ++ atom_to_list(Protocol) ++ ":"
- ++ inet:ntoa(IP) ++ ":"
- ++ integer_to_list(Port),
+ Name =
+ "dashboard:" ++
+ atom_to_list(Protocol) ++ ":" ++
+ inet:ntoa(IP) ++ ":" ++
+ integer_to_list(Port),
list_to_atom(Name);
listener_name(Protocol, #{port := Port}) ->
- Name = "dashboard:"
- ++ atom_to_list(Protocol) ++ ":"
- ++ integer_to_list(Port),
+ Name =
+ "dashboard:" ++
+ atom_to_list(Protocol) ++ ":" ++
+ integer_to_list(Port),
list_to_atom(Name).
authorize(Req) ->
@@ -180,11 +234,13 @@ authorize(Req) ->
{error, <<"not_allowed">>} ->
return_unauthorized(
?WRONG_USERNAME_OR_PWD,
- <<"Check username/password">>);
+ <<"Check username/password">>
+ );
{error, _} ->
return_unauthorized(
?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;
{error, _} ->
return_unauthorized(<<"WORNG_USERNAME_OR_PWD">>, <<"Check username/password">>)
@@ -199,12 +255,22 @@ authorize(Req) ->
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
end;
_ ->
- return_unauthorized(<<"AUTHORIZATION_HEADER_ERROR">>,
- <<"Support authorization: basic/bearer ">>)
+ return_unauthorized(
+ <<"AUTHORIZATION_HEADER_ERROR">>,
+ <<"Support authorization: basic/bearer ">>
+ )
end.
return_unauthorized(Code, Message) ->
- {401, #{<<"WWW-Authenticate">> =>
- <<"Basic Realm=\"minirest-server\"">>},
- #{code => Code, message => Message}
- }.
+ {401,
+ #{
+ <<"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.
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
index f518b8091..bbe198f46 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
@@ -15,87 +15,130 @@
%%--------------------------------------------------------------------
-module(emqx_dashboard_schema).
--include_lib("typerefl/include/types.hrl").
+-include_lib("hocon/include/hoconsc.hrl").
--export([ roots/0
- , fields/1
- , namespace/0
- , desc/1
- ]).
+-export([
+ roots/0,
+ fields/1,
+ namespace/0,
+ desc/1
+]).
namespace() -> <<"dashboard">>.
roots() -> ["dashboard"].
fields("dashboard") ->
- [ {listeners,
- sc(hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"),
- hoconsc:ref(?MODULE, "https")])),
- #{ desc =>
-"HTTP(s) listeners are identified by their protocol type and are
-used to serve dashboard UI and restful HTTP API.
-Listeners must have a unique combination of port number and IP address.
-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.
-Alternatively, the HTTP listener can specify a unique IP address for each listener,
-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.
"
- "Note: `sample_interval` should be a divisor of 60."
- })}
- , {token_expired_time, sc(emqx_schema:duration(),
- #{ default => "30m"
- , desc => "JWT token expiration time."
- })}
- , {cors, fun cors/1}
+ [
+ {listeners,
+ sc(
+ hoconsc:array(
+ hoconsc:union([
+ hoconsc:ref(?MODULE, "http"),
+ hoconsc:ref(?MODULE, "https")
+ ])
+ ),
+ #{
+ desc =>
+ "HTTP(s) listeners are identified by their protocol type and are\n"
+ "used to serve dashboard UI and restful HTTP API.
\n"
+ "Listeners must have a unique combination of port number and IP address.
\n"
+ "For example, an HTTP listener can listen on all configured IP addresses\n"
+ "on a given port for a machine by specifying the IP address 0.0.0.0.
\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.
"
+ "Note: `sample_interval` should be a divisor of 60."
+ }
+ )},
+ {token_expired_time,
+ sc(
+ emqx_schema:duration(),
+ #{
+ default => "30m",
+ desc => "JWT token expiration time."
+ }
+ )},
+ {cors, fun cors/1},
+ {i18n_lang, fun i18n_lang/1}
];
-
fields("http") ->
- [ {"protocol", sc(
- hoconsc:enum([http, https]),
- #{ desc => "HTTP/HTTPS protocol."
- , required => true
- , default => http
- })}
- , {"bind", fun bind/1}
- , {"num_acceptors", sc(
- integer(),
- #{ default => 4
- , desc => "Socket acceptor pool size for TCP protocols."
- })}
- , {"max_connections",
- sc(integer(),
- #{ default => 512
- , desc => "Maximum number of simultaneous connections."
- })}
- , {"backlog",
- sc(integer(),
- #{ default => 1024
- , desc => "Defines the maximum length that the queue of pending connections can grow to."
- })}
- , {"send_timeout",
- sc(emqx_schema:duration(),
- #{ 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."
- })}
+ [
+ {"protocol",
+ sc(
+ hoconsc:enum([http, https]),
+ #{
+ desc => ?DESC("protocol"),
+ required => true,
+ default => http
+ }
+ )},
+ {"bind", fun bind/1},
+ {"num_acceptors",
+ sc(
+ integer(),
+ #{
+ default => 4,
+ desc => "Socket acceptor pool size for TCP protocols."
+ }
+ )},
+ {"max_connections",
+ sc(
+ integer(),
+ #{
+ default => 512,
+ desc => "Maximum number of simultaneous connections."
+ }
+ )},
+ {"backlog",
+ sc(
+ integer(),
+ #{
+ default => 1024,
+ desc =>
+ "Defines the maximum length that the queue of pending connections can grow to."
+ }
+ )},
+ {"send_timeout",
+ sc(
+ emqx_schema:duration(),
+ #{
+ 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("http") ++
- proplists:delete("fail_if_no_peer_cert",
- emqx_schema:server_ssl_opts_schema(#{}, true)).
+ proplists:delete(
+ "fail_if_no_peer_cert",
+ emqx_schema:server_ssl_opts_schema(#{}, true)
+ ).
desc("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(_) -> undefined.
-default_password(type) -> string();
-default_password(default) -> "public";
-default_password(required) -> true;
-default_password('readOnly') -> true;
-default_password(sensitive) -> true;
-default_password(desc) -> """
-The initial default password for dashboard 'admin' user.
-For safety, it should be changed as soon as possible.""";
-default_password(_) -> undefined.
+default_password(type) ->
+ string();
+default_password(default) ->
+ "public";
+default_password(required) ->
+ true;
+default_password('readOnly') ->
+ true;
+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(default) -> false;
-cors(required) -> false;
+cors(type) ->
+ boolean();
+cors(default) ->
+ false;
+cors(required) ->
+ false;
cors(desc) ->
-"Support Cross-Origin Resource Sharing (CORS).
-Allows a server to indicate any origins (domain, scheme, or port) other than
-its own from which a browser should permit loading resources.";
-cors(_) -> undefined.
+ "Support Cross-Origin Resource Sharing (CORS).\n"
+ "Allows a server to indicate any origins (domain, scheme, or port) other than\n"
+ "its own from which a browser should permit loading resources.";
+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).
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
index a4bbe74fa..7ef374ec3 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
@@ -28,101 +28,136 @@
-export([filter_check_request/2, filter_check_request_and_translate_body/2]).
-ifdef(TEST).
--export([ parse_spec_ref/3
- , components/2
- ]).
+-export([
+ parse_spec_ref/3,
+ components/2
+]).
-endif.
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
--define(DEFAULT_FIELDS, [example, allowReserved, style, format, readOnly,
- explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
+-define(DEFAULT_FIELDS, [
+ example,
+ allowReserved,
+ style,
+ format,
+ readOnly,
+ explode,
+ maxLength,
+ allowEmptyValue,
+ deprecated,
+ minimum,
+ maximum
+]).
--define(INIT_SCHEMA, #{fields => #{}, translations => #{},
- validations => [], namespace => undefined}).
+-define(INIT_SCHEMA, #{
+ fields => #{},
+ translations => #{},
+ validations => [],
+ namespace => undefined
+}).
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
--define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>,
- ?TO_REF(namespace(_M_), _F_)])).
--define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>,
- ?TO_REF(namespace(_M_), _F_)])).
+-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
+ iolist_to_binary([
+ <<"#/components/schemas/">>,
+ ?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(DEFAULT_ROW, 100).
--type(request() :: #{bindings => map(), query_string => map(), body => map()}).
--type(request_meta() :: #{module => module(), path => string(), method => atom()}).
+-type request() :: #{bindings => map(), query_string => map(), body => map()}.
+-type request_meta() :: #{module => module(), path => string(), method => atom()}.
--type(filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}).
--type(filter() :: fun((request(), request_meta()) -> filter_result())).
+-type filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}.
+-type filter() :: fun((request(), request_meta()) -> filter_result()).
--type(spec_opts() :: #{check_schema => boolean() | filter(),
- translate_body => boolean(),
- schema_converter => fun((hocon_schema:schema(), Module::atom()) -> map())
- }).
+-type spec_opts() :: #{
+ check_schema => boolean() | filter(),
+ translate_body => boolean(),
+ schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map())
+}.
--type(route_path() :: string() | binary()).
--type(route_methods() :: map()).
--type(route_handler() :: atom()).
--type(route_options() :: #{filter => filter() | undefined}).
+-type route_path() :: string() | binary().
+-type route_methods() :: map().
+-type route_handler() :: atom().
+-type route_options() :: #{filter => filter() | undefined}.
--type(api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}).
--type(api_spec_component() :: map()).
+-type api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}.
+-type api_spec_component() :: map().
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
%% @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(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) ->
Paths = apply(Module, paths, []),
{ApiSpec, AllRefs} =
- lists:foldl(fun(Path, {AllAcc, AllRefsAcc}) ->
- {OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options),
- CheckSchema = support_check_schema(Options),
- {[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
- Refs ++ AllRefsAcc}
- end, {[], []}, Paths),
+ lists:foldl(
+ fun(Path, {AllAcc, AllRefsAcc}) ->
+ {OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options),
+ CheckSchema = support_check_schema(Options),
+ {
+ [{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
+ Refs ++ AllRefsAcc
+ }
+ end,
+ {[], []},
+ Paths
+ ),
{ApiSpec, components(lists:usort(AllRefs), Options)}.
--spec(namespace() -> hocon_schema:name()).
+-spec namespace() -> hocon_schema:name().
namespace() -> "public".
--spec(fields(hocon_schema:name()) -> hocon_schema:fields()).
+-spec fields(hocon_schema:name()) -> hocon_schema:fields().
fields(page) ->
Desc = <<"Page number of the results to fetch.">>,
Meta = #{in => query, desc => Desc, default => 1, example => 1},
[{page, hoconsc:mk(integer(), Meta)}];
fields(limit) ->
- Desc = iolist_to_binary([<<"Results per page(max ">>,
- integer_to_binary(?MAX_ROW_LIMIT), <<")">>]),
+ Desc = iolist_to_binary([
+ <<"Results per page(max ">>,
+ integer_to_binary(?MAX_ROW_LIMIT),
+ <<")">>
+ ]),
Meta = #{in => query, desc => Desc, default => ?DEFAULT_ROW, example => 50},
[{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) ->
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) ->
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 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) ->
[
{code, hoconsc:mk(hoconsc:enum(Codes))},
- {message, hoconsc:mk(string(), #{
- desc => <<"Details description of the error.">>,
- example => MsgExample
- })}
+ {message,
+ hoconsc:mk(string(), #{
+ desc => <<"Details description of the error.">>,
+ example => MsgExample
+ })}
].
%%------------------------------------------------------------------------------
@@ -143,10 +178,13 @@ translate_req(Request, #{module := Module, path := Path, method := Method}, Chec
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
- catch throw:{_, 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, ","))}
+ catch
+ throw:{_, 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, ","))}
end.
check_and_translate(Schema, Map, Opts) ->
@@ -169,30 +207,51 @@ parse_spec_ref(Module, Path, Options) ->
Schema =
try
erlang:apply(Module, schema, [Path])
- catch error: Reason -> %% better error message
- throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
+ %% better error message
+ catch
+ error:Reason ->
+ throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
end,
- {Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) ->
- (not lists:member(Method, ?METHODS))
- andalso throw({error, #{module => Module, path => Path, method => Method}}),
- {Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
- {Acc#{Method => Spec}, SubRefs ++ RefsAcc}
- end, {#{}, []},
- maps:without(['operationId'], Schema)),
+ {Specs, Refs} = maps:fold(
+ fun(Method, Meta, {Acc, RefsAcc}) ->
+ (not lists:member(Method, ?METHODS)) andalso
+ throw({error, #{module => Module, path => Path, method => Method}}),
+ {Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
+ {Acc#{Method => Spec}, SubRefs ++ RefsAcc}
+ end,
+ {#{}, []},
+ maps:without(['operationId'], Schema)
+ ),
{maps:get('operationId', Schema), Specs, Refs}.
check_parameters(Request, Spec, Module) ->
#{bindings := Bindings, query_string := QueryStr} = Request,
- BindingsBin = maps:fold(fun(Key, Value, Acc) ->
- Acc#{atom_to_binary(Key) => Value}
- end, #{}, Bindings),
+ BindingsBin = maps:fold(
+ fun(Key, Value, Acc) ->
+ Acc#{atom_to_binary(Key) => Value}
+ end,
+ #{},
+ Bindings
+ ),
check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
- check_parameter([?R_REF(LocalMod, Fields) | Spec],
- Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
-check_parameter([?R_REF(Module, Fields) | Spec],
- Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
+ check_parameter(
+ [?R_REF(LocalMod, Fields) | Spec],
+ Bindings,
+ QueryStr,
+ LocalMod,
+ BindingsAcc,
+ QueryStrAcc
+ );
+check_parameter(
+ [?R_REF(Module, Fields) | Spec],
+ Bindings,
+ QueryStr,
+ LocalMod,
+ BindingsAcc,
+ QueryStrAcc
+) ->
Params = apply(Module, fields, [Fields]),
check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) ->
@@ -209,7 +268,7 @@ check_parameter([{Name, Type} | Spec], Bindings, QueryStr, Module, BindingsAcc,
Option = #{},
NewQueryStr = hocon_tconf:check_plain(Schema, QueryStr, Option),
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
- check_parameter(Spec, Bindings, QueryStr, Module,BindingsAcc, NewQueryStrAcc)
+ check_parameter(Spec, Bindings, QueryStr, Module, BindingsAcc, NewQueryStrAcc)
end.
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
@@ -230,21 +289,24 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
%% {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
%% ]}
%% ]
-check_request_body(#{body := Body}, Spec, _Module, CheckFun, false)when is_list(Spec) ->
- lists:foldl(fun({Name, Type}, Acc) ->
- Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
- maps:merge(Acc, CheckFun(Schema, Body, #{}))
- end, #{}, Spec);
-
+check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
+ lists:foldl(
+ fun({Name, Type}, Acc) ->
+ Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
+ maps:merge(Acc, CheckFun(Schema, Body, #{}))
+ end,
+ #{},
+ Spec
+ );
%% requestBody => #{content => #{ 'application/octet-stream' =>
%% #{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) ->
Body.
%% tags, description, summary, security, deprecated
meta_to_spec(Meta, Module, Options) ->
{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),
{
generate_method_desc(to_spec(Meta, Params, RequestBody, Responses)),
@@ -268,80 +330,115 @@ generate_method_desc(Spec) ->
parameters(Params, Module) ->
{SpecList, AllRefs} =
- lists:foldl(fun(Param, {Acc, RefsAcc}) ->
- case Param of
- ?REF(StructName) -> to_ref(Module, StructName, Acc, RefsAcc);
- ?R_REF(RModule, StructName) -> to_ref(RModule, StructName, Acc, RefsAcc);
- {Name, Type} ->
- In = hocon_schema:field_schema(Type, in),
- In =:= undefined andalso
- throw({error, <<"missing in:path/query field in parameters">>}),
- Required = hocon_schema:field_schema(Type, required),
- Default = hocon_schema:field_schema(Type, default),
- HoconType = hocon_schema:field_schema(Type, type),
- Meta = init_meta(Default),
- {ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
- Spec0 = init_prop([required | ?DEFAULT_FIELDS],
- #{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type),
- Spec1 = trans_required(Spec0, Required, In),
- Spec2 = trans_desc(Spec1, Type),
- {[Spec2 | Acc], Refs ++ RefsAcc}
- end
- end, {[], []}, Params),
+ lists:foldl(
+ fun(Param, {Acc, RefsAcc}) ->
+ case Param of
+ ?REF(StructName) ->
+ to_ref(Module, StructName, Acc, RefsAcc);
+ ?R_REF(RModule, StructName) ->
+ to_ref(RModule, StructName, Acc, RefsAcc);
+ {Name, Type} ->
+ In = hocon_schema:field_schema(Type, in),
+ In =:= undefined andalso
+ throw({error, <<"missing in:path/query field in parameters">>}),
+ Required = hocon_schema:field_schema(Type, required),
+ Default = hocon_schema:field_schema(Type, default),
+ HoconType = hocon_schema:field_schema(Type, type),
+ Meta = init_meta(Default),
+ {ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
+ Spec0 = init_prop(
+ [required | ?DEFAULT_FIELDS],
+ #{schema => maps:merge(ParamType, Meta), name => Name, in => In},
+ Type
+ ),
+ Spec1 = trans_required(Spec0, Required, In),
+ Spec2 = trans_description(Spec1, Type),
+ {[Spec2 | Acc], Refs ++ RefsAcc}
+ end
+ end,
+ {[], []},
+ Params
+ ),
{lists:reverse(SpecList), AllRefs}.
init_meta(undefined) -> #{};
init_meta(Default) -> #{default => Default}.
init_prop(Keys, Init, Type) ->
- lists:foldl(fun(Key, Acc) ->
- case hocon_schema:field_schema(Type, Key) of
- undefined -> Acc;
- Schema -> Acc#{Key => to_bin(Schema)}
- end
- end, Init, Keys).
+ lists:foldl(
+ fun(Key, Acc) ->
+ case hocon_schema:field_schema(Type, Key) of
+ undefined -> Acc;
+ Schema -> Acc#{Key => to_bin(Schema)}
+ end
+ end,
+ Init,
+ Keys
+ ).
trans_required(Spec, true, _) -> Spec#{required => true};
trans_required(Spec, _, path) -> Spec#{required => true};
trans_required(Spec, _, _) -> Spec.
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
- true -> Spec0;
+ true ->
+ Spec0;
false ->
- Spec1 = Spec0#{label => Name},
+ Spec1 = trans_label(Spec0, Hocon, Name),
case Spec1 of
#{description := _} -> Spec1;
_ -> Spec1#{description => <>}
end
end.
-trans_desc(Spec, Hocon) ->
- case hocon_schema:field_schema(Hocon, desc) of
- undefined ->
- case hocon_schema:field_schema(Hocon, description) of
- undefined ->
- Spec;
- Desc ->
- Spec#{description => to_bin(Desc)}
- end;
- Desc -> Spec#{description => to_bin(Desc)}
+trans_description(Spec, Hocon) ->
+ case trans_desc(<<"desc">>, Hocon, undefined) of
+ undefined -> Spec;
+ Value -> Spec#{description => Value}
end.
-request_body(#{content := _} = Content, _Module) -> {Content, []};
-request_body([], _Module) -> {[], []};
-request_body(Schema, Module) ->
+trans_label(Spec, Hocon, Default) ->
+ Label = trans_desc(<<"label">>, Hocon, Default),
+ 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} =
case hoconsc:is_schema(Schema) of
true ->
HoconSchema = hocon_schema:field_schema(Schema, type),
SchemaExamples = hocon_schema:field_schema(Schema, examples),
{hocon_schema_to_spec(HoconSchema, Module), SchemaExamples};
- false -> {parse_object(Schema, Module, #{}), undefined}
+ false ->
+ {parse_object(Schema, Module, Options), undefined}
end,
- {#{<<"content">> => content(Props, Examples)},
- Refs}.
+ {#{<<"content">> => content(Props, Examples)}, Refs}.
responses(Responses, Module, Options) ->
{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),
{Spec, Refs} = SchemaToSpec(RRef, Module),
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}) ->
case hoconsc:is_schema(Schema) of
true ->
Hocon = hocon_schema:field_schema(Schema, type),
Examples = hocon_schema:field_schema(Schema, examples),
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
- Init = trans_desc(#{}, Schema),
+ Init = trans_description(#{}, Schema),
Content = content(Spec, Examples),
{
Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}},
- Refs ++ RefsAcc, Module, Options
+ Refs ++ RefsAcc,
+ Module,
+ Options
};
false ->
{Props, Refs} = parse_object(Schema, Module, Options),
- Init = trans_desc(#{}, Schema),
+ Init = trans_description(#{}, Schema),
Content = Init#{<<"content">> => content(Props)},
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
end.
components(Refs, Options) ->
- lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [],
- components(Options, Refs, #{}, []))).
+ lists:sort(
+ maps:fold(
+ fun(K, V, Acc) -> [#{K => V} | Acc] end,
+ [],
+ components(Options, Refs, #{}, [])
+ )
+ ).
-components(_Options, [], SpecAcc, []) -> SpecAcc;
-components(Options, [], SpecAcc, SubRefAcc) -> components(Options, SubRefAcc, SpecAcc, []);
+components(_Options, [], SpecAcc, []) ->
+ SpecAcc;
+components(Options, [], SpecAcc, SubRefAcc) ->
+ components(Options, SubRefAcc, SpecAcc, []);
components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
Props = hocon_schema_fields(Module, Field),
Namespace = namespace(Module),
@@ -404,7 +517,9 @@ hocon_schema_fields(Module, StructName) ->
case apply(Module, fields, [StructName]) of
#{fields := Fields, desc := _} ->
%% 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
end.
@@ -415,15 +530,13 @@ hocon_schema_fields(Module, StructName) ->
namespace(Module) ->
case hocon_schema:namespace(Module) of
undefined -> Module;
- NameSpace -> re:replace(to_bin(NameSpace), ":","-",[global])
+ NameSpace -> re:replace(to_bin(NameSpace), ":", "-", [global])
end.
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
- [{Module, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
- [{LocalModule, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, [{LocalModule, StructName}]};
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
{typename_to_spec(typerefl:name(Type), LocalModule), []};
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
@@ -435,57 +548,99 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
{#{type => string, enum => Items}, []};
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {#{<<"type">> => object,
- <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
- SubRefs};
+ {
+ #{
+ <<"type">> => object,
+ <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}
+ },
+ SubRefs
+ };
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
- {OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
- {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {[Schema | Acc], SubRefs ++ RefsAcc}
- end, {[], []}, Types),
+ {OneOf, Refs} = lists:foldl(
+ fun(Type, {Acc, RefsAcc}) ->
+ {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
+ {[Schema | Acc], SubRefs ++ RefsAcc}
+ end,
+ {[], []},
+ Types
+ ),
{#{<<"oneOf">> => OneOf}, Refs};
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
{#{type => string, enum => [Atom]}, []}.
%% 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("term()", _Mod) -> #{type => string, example => "any"};
-typename_to_spec("boolean()", _Mod) -> #{type => boolean, example => true};
-typename_to_spec("binary()", _Mod) -> #{type => string, example => <<"binary-example">>};
-typename_to_spec("float()", _Mod) -> #{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("user_id_type()", _Mod) ->
+ #{type => string, enum => [clientid, username]};
+typename_to_spec("term()", _Mod) ->
+ #{type => string, example => "any"};
+typename_to_spec("boolean()", _Mod) ->
+ #{type => boolean, example => true};
+typename_to_spec("binary()", _Mod) ->
+ #{type => string, example => <<"binary-example">>};
+typename_to_spec("float()", _Mod) ->
+ #{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) ->
- #{<<"oneOf">> => [
- #{type => integer, example => 1640995200, description => <<"epoch-second">>},
- #{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
- };
-typename_to_spec("epoch_millisecond()", _Mod) ->
- #{<<"oneOf">> => [
- #{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
- #{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
+ #{
+ <<"oneOf">> => [
+ #{type => integer, example => 1640995200, description => <<"epoch-second">>},
+ #{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_s()", _Mod) -> #{type => string, example => <<"1h">>};
-typename_to_spec("duration_ms()", _Mod) -> #{type => string, example => <<"32s">>};
-typename_to_spec("percent()", _Mod) -> #{type => number, example => <<"12%">>};
-typename_to_spec("file()", _Mod) -> #{type => string, example => <<"/path/to/file">>};
-typename_to_spec("ip_port()", _Mod) -> #{type => string, example => <<"127.0.0.1:80">>};
+typename_to_spec("epoch_millisecond()", _Mod) ->
+ #{
+ <<"oneOf">> => [
+ #{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
+ #{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_s()", _Mod) ->
+ #{type => string, example => <<"1h">>};
+typename_to_spec("duration_ms()", _Mod) ->
+ #{type => string, example => <<"32s">>};
+typename_to_spec("percent()", _Mod) ->
+ #{type => number, example => <<"12%">>};
+typename_to_spec("file()", _Mod) ->
+ #{type => string, example => <<"/path/to/file">>};
+typename_to_spec("ip_port()", _Mod) ->
+ #{type => string, example => <<"127.0.0.1:80">>};
typename_to_spec("ip_ports()", _Mod) ->
#{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("connect_timeout()", Mod) -> typename_to_spec("timeout()", Mod);
-typename_to_spec("timeout()", _Mod) -> #{<<"oneOf">> => [#{type => string, example => infinity},
- #{type => integer, example => 100}], example => infinity};
-typename_to_spec("bytesize()", _Mod) -> #{type => string, example => <<"32MB">>};
-typename_to_spec("wordsize()", _Mod) -> #{type => string, example => <<"1024KB">>};
-typename_to_spec("map()", _Mod) -> #{type => 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("url()", _Mod) ->
+ #{type => string, example => <<"http://127.0.0.1">>};
+typename_to_spec("connect_timeout()", Mod) ->
+ typename_to_spec("timeout()", Mod);
+typename_to_spec("timeout()", _Mod) ->
+ #{
+ <<"oneOf">> => [
+ #{type => string, example => infinity},
+ #{type => integer, example => 100}
+ ],
+ example => infinity
+ };
+typename_to_spec("bytesize()", _Mod) ->
+ #{type => string, example => <<"32MB">>};
+typename_to_spec("wordsize()", _Mod) ->
+ #{type => string, example => <<"1024KB">>};
+typename_to_spec("map()", _Mod) ->
+ #{type => 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) ->
#{type => string, example => <<"item1,item2">>};
typename_to_spec("comma_separated_atoms()", _Mod) ->
@@ -493,8 +648,9 @@ typename_to_spec("comma_separated_atoms()", _Mod) ->
typename_to_spec("pool_type()", _Mod) ->
#{type => string, enum => [random, hash], example => hash};
typename_to_spec("log_level()", _Mod) ->
- #{ type => string,
- enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
+ #{
+ type => string,
+ enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
};
typename_to_spec("rate()", _Mod) ->
#{type => string, example => <<"10M/s">>};
@@ -515,16 +671,18 @@ typename_to_spec(Name, Mod) ->
Spec2 = typerefl_array(Spec1, Name, Mod),
Spec3 = integer(Spec2, Name),
Spec3 =:= nomatch andalso
- throw({error, #{msg => <<"Unsupported Type">>, type => Name, module => Mod}}),
- Spec3.
+ throw({error, #{msg => <<"Unsupported Type">>, type => Name, module => Mod}}),
+ Spec3.
range(Name) ->
case string:split(Name, "..") of
- [MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
+ %% 1..10 1..inf -inf..10
+ [MinStr, MaxStr] ->
Schema = #{type => integer},
Schema1 = add_integer_prop(Schema, minimum, MinStr),
add_integer_prop(Schema1, maximum, MaxStr);
- _ -> nomatch
+ _ ->
+ nomatch
end.
%% Module:Type
@@ -533,21 +691,25 @@ remote_module_type(nomatch, Name, Mod) ->
[_Module, Type] -> typename_to_spec(Type, Mod);
_ -> nomatch
end;
-remote_module_type(Spec, _Name, _Mod) -> Spec.
+remote_module_type(Spec, _Name, _Mod) ->
+ Spec.
%% [string()] or [integer()] or [xxx].
typerefl_array(nomatch, Name, Mod) ->
case string:trim(Name, leading, "[") of
- Name -> nomatch;
+ Name ->
+ nomatch;
Name1 ->
case string:trim(Name1, trailing, "]") of
- Name1 -> notmatch;
+ Name1 ->
+ notmatch;
Name2 ->
Schema = typename_to_spec(Name2, Mod),
#{type => array, items => Schema}
end
end;
-typerefl_array(Spec, _Name, _Mod) -> Spec.
+typerefl_array(Spec, _Name, _Mod) ->
+ Spec.
%% integer(1)
integer(nomatch, Name) ->
@@ -555,12 +717,13 @@ integer(nomatch, Name) ->
{Int, []} -> #{type => integer, enum => [Int], example => Int, default => Int};
_ -> nomatch
end;
-integer(Spec, _Name) -> Spec.
+integer(Spec, _Name) ->
+ Spec.
add_integer_prop(Schema, Key, Value) ->
case string:to_integer(Value) of
{error, no_integer} -> Schema;
- {Int, []}when Key =:= minimum -> Schema#{Key => Int, example => Int};
+ {Int, []} when Key =:= minimum -> Schema#{Key => Int, example => Int};
{Int, []} -> Schema#{Key => Int}
end.
@@ -571,39 +734,53 @@ to_bin(List) when is_list(List) ->
end;
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
-to_bin(X) -> X.
+to_bin(X) ->
+ X.
parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) ->
{Props, Required, Refs} =
- lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
- NameBin = to_bin(Name),
- case hoconsc:is_schema(Hocon) of
- true ->
- HoconType = hocon_schema:field_schema(Hocon, type),
- Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
- SchemaToSpec = schema_converter(Options),
- Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin),
- {Prop, Refs1} = SchemaToSpec(HoconType, Module),
- NewRequiredAcc =
- case is_required(Hocon) of
- true -> [NameBin | RequiredAcc];
- false -> RequiredAcc
- end,
- {[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc};
- false ->
- {SubObject, SubRefs} = parse_object(Hocon, Module, Options),
- {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
- end
- end, {[], [], []}, PropList),
+ lists:foldl(
+ fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
+ NameBin = to_bin(Name),
+ case hoconsc:is_schema(Hocon) of
+ true ->
+ HoconType = hocon_schema:field_schema(Hocon, type),
+ Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
+ SchemaToSpec = schema_converter(Options),
+ Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin),
+ {Prop, Refs1} = SchemaToSpec(HoconType, Module),
+ NewRequiredAcc =
+ case is_required(Hocon) of
+ true -> [NameBin | RequiredAcc];
+ false -> RequiredAcc
+ end,
+ {
+ [{NameBin, maps:merge(Prop, Init)} | Acc],
+ NewRequiredAcc,
+ Refs1 ++ RefsAcc
+ };
+ false ->
+ {SubObject, SubRefs} = parse_object(Hocon, Module, Options),
+ {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
+ end
+ end,
+ {[], [], []},
+ PropList
+ ),
Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)},
case Required of
[] -> {Object, Refs};
_ -> {maps:put(required, Required, Object), Refs}
end;
parse_object(Other, Module, Options) ->
- erlang:throw({error,
- #{msg => <<"Object only supports not empty proplists">>,
- args => Other, module => Module, options => Options}}).
+ erlang:throw(
+ {error, #{
+ msg => <<"Object only supports not empty proplists">>,
+ args => Other,
+ module => Module,
+ options => Options
+ }}
+ ).
is_required(Hocon) ->
hocon_schema:field_schema(Hocon, required) =:= true.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl
index db587edbd..62bf5ab7a 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl
@@ -81,12 +81,7 @@ set_special_configs(emqx_management) ->
emqx_config:put([emqx_management], Config),
ok;
set_special_configs(emqx_dashboard) ->
- Listeners = [#{protocol => http, port => 18083}],
- Config = #{listeners => Listeners,
- default_username => <<"admin">>,
- default_password => <<"public">>
- },
- emqx_config:put([dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl
index ca40a5329..bec5f0ed9 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl
@@ -35,9 +35,16 @@ set_default_config(DefaultUsername) ->
Config = #{listeners => [#{protocol => http,
port => 18083}],
default_username => DefaultUsername,
- default_password => <<"public">>
+ default_password => <<"public">>,
+ i18n_lang => en
},
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.
request(Method, Url) ->
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl
index 341ba5691..878212c61 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl
@@ -35,15 +35,7 @@ init_per_suite(Config) ->
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([emqx_dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl
index bed79b349..ff9926336 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl
@@ -35,15 +35,7 @@ init_per_suite(Config) ->
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([emqx_dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl
index b75ca412e..b7430e586 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl
@@ -40,15 +40,7 @@ end_per_suite(Config) ->
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl
index 3b54c54c4..614b3064e 100644
--- a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl
@@ -4,6 +4,7 @@
%% API
-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_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]).
@@ -26,6 +27,27 @@ groups() -> [
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) ->
Expect =
[#{description => <<"Indicates which sorts of issues to return">>,
diff --git a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl
index bf48046c2..5501ee780 100644
--- a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl
@@ -3,41 +3,36 @@
-behaviour(minirest_api).
-behaviour(hocon_schema).
-%% API
--export([paths/0, api_spec/0, schema/1, fields/1]).
--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]).
+-compile(nowarn_export_all).
+-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2]).
-all() -> [{group, spec}, {group, validation}].
+all() -> emqx_common_test_helpers:all(?MODULE).
-suite() -> [{timetrap, {minutes, 1}}].
-groups() -> [
- {spec, [parallel], [
- t_api_spec, t_object, t_nest_object,
- t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
- t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]},
- {validation, [parallel],
- [
- t_object_trans, t_object_notrans, t_local_ref_trans, t_remote_ref_trans,
- t_ref_array_with_key_trans, t_ref_array_without_key_trans, t_nest_ref_trans,
- t_ref_trans_error, t_object_trans_error
- %% t_nest_object_trans,
- ]}
-].
+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_object(_Config) ->
Spec = #{
@@ -281,7 +276,7 @@ t_object_notrans(_Config) ->
?assertEqual(Body, ActualBody),
ok.
-t_nest_object_trans(_Config) ->
+todo_t_nest_object_check(_Config) ->
Path = "/nest/object",
Body = #{
<<"timeout">> => "10m",
@@ -306,7 +301,7 @@ t_nest_object_trans(_Config) ->
body => #{<<"per_page">> => 10,
<<"timeout">> => 600}
},
- {ok, NewRequest} = trans_requestBody(Path, Body),
+ {ok, NewRequest} = check_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
@@ -487,6 +482,10 @@ trans_requestBody(Path, Body) ->
trans_requestBody(Path, Body,
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) ->
Meta = #{module => ?MODULE, method => post, path => Path},
Request = #{bindings => #{}, query_string => #{}, body => Body},
diff --git a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl
index 3b68c8eff..1444370fb 100644
--- a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl
@@ -10,22 +10,31 @@
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2]).
--export([all/0, suite/0, groups/0]).
--export([paths/0, api_spec/0, schema/1, fields/1]).
--export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1, t_error/1,
- 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]).
+-compile(nowarn_export_all).
+-compile(export_all).
-all() -> [{group, spec}].
-suite() -> [{timetrap, {minutes, 1}}].
-groups() -> [
- {spec, [parallel], [
- t_api_spec, t_simple_binary, t_object, t_nest_object, t_error, t_complicated_type,
- t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
- t_local_ref, t_remote_ref, t_bad_ref, t_none_ref, t_sub_fields,
- t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
-].
+all() -> emqx_common_test_helpers:all(?MODULE).
+
+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_simple_binary(_config) ->
Path = "/simple/bin",
diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl
index 9952686c5..e6f85b1d6 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl
@@ -39,15 +39,7 @@ end_suite(Apps) ->
ok.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_App) ->
ok.
diff --git a/apps/emqx_prometheus/rebar.config b/apps/emqx_prometheus/rebar.config
index b05091515..36a53d1bb 100644
--- a/apps/emqx_prometheus/rebar.config
+++ b/apps/emqx_prometheus/rebar.config
@@ -4,7 +4,7 @@
[ {emqx, {path, "../emqx"}},
%% FIXME: tag this as v3.1.3
{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}]}.
diff --git a/build b/build
index 421fdd6bd..ac09f95fe 100755
--- a/build
+++ b/build
@@ -82,7 +82,8 @@ make_doc() {
# shellcheck disable=SC2086
erl -noshell -pa $libs_dir1 $libs_dir2 $libs_dir3 -eval \
"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)."
}
diff --git a/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl b/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl
index a24f3c3f7..10c5998db 100644
--- a/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl
+++ b/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl
@@ -9,5 +9,6 @@
doc_gen_test() ->
Dir = "tmp",
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.
diff --git a/mix.exs b/mix.exs
index e5d149ffa..13a2293e6 100644
--- a/mix.exs
+++ b/mix.exs
@@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do
# in conflict by emqtt and hocon
{:getopt, "1.0.2", 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},
{:esasl, github: "emqx/esasl", tag: "0.2.0"},
{:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},
@@ -342,6 +342,13 @@ defmodule EMQXUmbrella.MixProject do
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
Mix.Generator.copy_file(
Path.join(release.version_path, "start_clean.boot"),
diff --git a/rebar.config b/rebar.config
index ef0af3580..75a5da56a 100644
--- a/rebar.config
+++ b/rebar.config
@@ -66,7 +66,7 @@
, {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.2"}}}
, {getopt, "1.0.2"}
, {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"}}}
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
, {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}
diff --git a/rebar.config.erl b/rebar.config.erl
index 1cc18b434..1e82c06bc 100644
--- a/rebar.config.erl
+++ b/rebar.config.erl
@@ -450,10 +450,12 @@ emqx_etc_overlay_common() ->
emqx_etc_overlay_per_edition(ce) ->
[ {"{{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) ->
[ {"{{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_dashboard/etc/i18n.conf.all", "etc/i18n.conf"}
].
get_vsn(Profile) ->
diff --git a/scripts/merge-i18n.escript b/scripts/merge-i18n.escript
new file mode 100755
index 000000000..13a7df27b
--- /dev/null
+++ b/scripts/merge-i18n.escript
@@ -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.