From 630cc8ee346a357660cda22f3fc3ef686366e08c Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 15 Apr 2022 08:42:13 +0800 Subject: [PATCH 1/4] feat: support i18n with desc struct. --- Makefile | 1 + apps/emqx/rebar.config | 2 +- apps/emqx_conf/src/emqx_conf.erl | 299 ++++++--- .../etc/emqx_dashboard_i18n.conf | 12 + apps/emqx_dashboard/src/emqx_dashboard.erl | 197 ++++-- .../src/emqx_dashboard_schema.erl | 234 ++++--- .../src/emqx_dashboard_swagger.erl | 595 ++++++++++++------ apps/emqx_prometheus/rebar.config | 2 +- mix.exs | 8 +- rebar.config | 2 +- rebar.config.erl | 2 + scripts/merge-config.escript | 3 +- scripts/merge-i18n.escript | 62 ++ 13 files changed, 958 insertions(+), 461 deletions(-) create mode 100644 apps/emqx_dashboard/etc/emqx_dashboard_i18n.conf create mode 100755 scripts/merge-i18n.escript diff --git a/Makefile b/Makefile index 96eb3ad6e..f87ce0bcb 100644 --- a/Makefile +++ b/Makefile @@ -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_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index 372875d25..e11acd8af 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -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}); @@ -129,22 +136,38 @@ dump_schema(Dir) -> dump_schema(Dir, emqx_conf_schema). dump_schema(Dir, SchemaModule) -> - SchemaMdFile = filename:join([Dir, "config.md"]), - io:format(user, "===< Generating: ~s~n", [SchemaMdFile ]), - ok = gen_doc(SchemaMdFile, SchemaModule), + PrivDir = filename:dirname(filename:dirname(Dir)), + lists:foreach( + fun(Lang) -> + gen_config_md(Dir, PrivDir, SchemaModule, Lang), + gen_hot_conf_schema_json(Dir, PrivDir, Lang) + end, + [en, zh] + ), + gen_schema_json(Dir, PrivDir, SchemaModule). - %% for scripts/spellcheck. +%% for scripts/spellcheck. +gen_schema_json(Dir, PrivDir, SchemaModule) -> SchemaJsonFile = filename:join([Dir, "schema.json"]), io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]), - JsonMap = hocon_schema_json:gen(SchemaModule), + Opts = #{desc_file => i18n_file(PrivDir), 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, PrivDir, Lang) -> + emqx_dashboard:init_i18n(i18n_file(PrivDir), 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, PrivDir, 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, PrivDir, Lang). %% @doc return the root schema module. -spec schema_module() -> module(). @@ -158,63 +181,97 @@ 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, EtcDir, 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}), + DescFile = i18n_file(EtcDir), + Opts = #{title => Title, body => Body, desc_file => DescFile, 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 +283,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 +386,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 +401,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 +427,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 +444,8 @@ 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. + +i18n_file(EtcDir) -> + filename:join([EtcDir, "i18n.conf"]). diff --git a/apps/emqx_dashboard/etc/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/etc/emqx_dashboard_i18n.conf new file mode 100644 index 000000000..2a588b752 --- /dev/null +++ b/apps/emqx_dashboard/etc/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..6f907d735 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -18,11 +18,18 @@ -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, + get_i18n/0, + clear_i18n/0 +]). %% Authorization -export([authorize/1]). @@ -48,6 +55,7 @@ stop_listeners() -> start_listeners(Listeners) -> {ok, _} = application:ensure_all_started(minirest), + init_i18n(), Authorization = {?MODULE, authorize}, GlobalSpec = #{ openapi => "3.0.0", @@ -58,12 +66,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 +85,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 = emqx:etc_file("i18n.conf"), + Lang = emqx_conf:get([dashboard, i18n_lang]), + 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 +201,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 +233,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 +254,16 @@ 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}}. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index f518b8091..5531b16d5 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) -> "i18n language"; +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..644654306 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,15 +289,18 @@ 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 @@ -268,69 +330,104 @@ 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) -> {[], []}; +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) -> + {Content, []}; +request_body([], _Module) -> + {[], []}; request_body(Schema, Module) -> {{Props, Refs}, Examples} = case hoconsc:is_schema(Schema) of @@ -338,10 +435,10 @@ request_body(Schema, Module) -> HoconSchema = hocon_schema:field_schema(Schema, type), SchemaExamples = hocon_schema:field_schema(Schema, examples), {hocon_schema_to_spec(HoconSchema, Module), SchemaExamples}; - false -> {parse_object(Schema, Module, #{}), undefined} + false -> + {parse_object(Schema, Module, #{}), undefined} end, - {#{<<"content">> => content(Props, Examples)}, - Refs}. + {#{<<"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_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/mix.exs b/mix.exs index 5ee200120..a5ee0e1f4 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"}, @@ -341,6 +341,12 @@ defmodule EMQXUmbrella.MixProject do "apps/emqx/etc/certs", Path.join(etc, "certs") ) + # required by emqx_dashboard + Mix.Generator.copy_file( + "apps/emqx_dashboard/etc/i18n.conf", + Path.join(etc, "i18n.conf"), + force: overwrite? + ) # this is required by the produced escript / nodetool Mix.Generator.copy_file( diff --git a/rebar.config b/rebar.config index 199b7462a..6cb1b18cc 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 fc8d6d33b..c93aecfd0 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -456,10 +456,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-config.escript b/scripts/merge-config.escript index 49eba8169..67ac70923 100755 --- a/scripts/merge-config.escript +++ b/scripts/merge-config.escript @@ -62,7 +62,8 @@ get_cfgs(Dir, Cfgs) -> %% the conf name must start with emqx %% because there are some other conf, and these conf don't start with emqx Confs = filelib:wildcard("emqx*.conf", EtcDir), - NewCfgs = [filename:join([EtcDir, Name]) || Name <- Confs], + Confs1 = lists:filter(fun(N) -> string:find(N, "i18n") =:= nomatch end, Confs), + NewCfgs = [filename:join([EtcDir, Name]) || Name <- Confs1], try_enter_child(Dir, Files, NewCfgs ++ Cfgs) end end. diff --git a/scripts/merge-i18n.escript b/scripts/merge-i18n.escript new file mode 100755 index 000000000..d1bc0405c --- /dev/null +++ b/scripts/merge-i18n.escript @@ -0,0 +1,62 @@ +#!/usr/bin/env escript + +-mode(compile). + +main(_) -> + {ok, BaseConf} = file:read_file("apps/emqx_dashboard/etc/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("etc", Files) of + false -> + try_enter_child(Dir, Files, Cfgs); + true -> + EtcDir = filename:join([Dir, "etc"]), + %% the conf name must start with emqx + %% because there are some other conf, and these conf don't start with emqx + Confs = filelib:wildcard("emqx*_i18n.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. From 700c2cfb398f7ed813db8db2b249a2ac0b76250c Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 15 Apr 2022 09:54:22 +0800 Subject: [PATCH 2/4] feat: move *i18n.conf from etc to i18n dir --- .gitignore | 1 + apps/emqx_conf/src/emqx_conf.erl | 34 ++++++++----------- .../{etc => i18n}/emqx_dashboard_i18n.conf | 0 build | 3 +- mix.exs | 7 ++-- scripts/merge-config.escript | 3 +- scripts/merge-i18n.escript | 10 +++--- 7 files changed, 27 insertions(+), 31 deletions(-) rename apps/emqx_dashboard/{etc => i18n}/emqx_dashboard_i18n.conf (100%) 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/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index e11acd8af..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 @@ -133,41 +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) -> - PrivDir = filename:dirname(filename:dirname(Dir)), +dump_schema(Dir, SchemaModule, I18nFile) -> lists:foreach( fun(Lang) -> - gen_config_md(Dir, PrivDir, SchemaModule, Lang), - gen_hot_conf_schema_json(Dir, PrivDir, Lang) + gen_config_md(Dir, I18nFile, SchemaModule, Lang), + gen_hot_conf_schema_json(Dir, I18nFile, Lang) end, [en, zh] ), - gen_schema_json(Dir, PrivDir, SchemaModule). + gen_schema_json(Dir, I18nFile, SchemaModule). %% for scripts/spellcheck. -gen_schema_json(Dir, PrivDir, SchemaModule) -> +gen_schema_json(Dir, I18nFile, SchemaModule) -> SchemaJsonFile = filename:join([Dir, "schema.json"]), io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]), - Opts = #{desc_file => i18n_file(PrivDir), lang => "en"}, + 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). -gen_hot_conf_schema_json(Dir, PrivDir, Lang) -> - emqx_dashboard:init_i18n(i18n_file(PrivDir), Lang), +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), emqx_dashboard:clear_i18n(). -gen_config_md(Dir, PrivDir, SchemaModule, Lang0) -> +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, PrivDir, Lang). + ok = gen_doc(SchemaMdFile, SchemaModule, I18nFile, Lang). %% @doc return the root schema module. -spec schema_module() -> module(). @@ -182,13 +182,12 @@ schema_module() -> %%-------------------------------------------------------------------- -spec gen_doc(file:name_all(), module(), file:name_all(), string()) -> ok. -gen_doc(File, SchemaModule, EtcDir, Lang) -> +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), - DescFile = i18n_file(EtcDir), - Opts = #{title => Title, body => Body, desc_file => DescFile, lang => Lang}, + Opts = #{title => Title, body => Body, desc_file => I18nFile, lang => Lang}, Doc = hocon_schema_md:gen(SchemaModule, Opts), file:write_file(File, Doc). @@ -446,6 +445,3 @@ to_bin(Boolean) when is_boolean(Boolean) -> Boolean; to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); to_bin(X) -> X. - -i18n_file(EtcDir) -> - filename:join([EtcDir, "i18n.conf"]). diff --git a/apps/emqx_dashboard/etc/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf similarity index 100% rename from apps/emqx_dashboard/etc/emqx_dashboard_i18n.conf rename to apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf 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/mix.exs b/mix.exs index a5ee0e1f4..bbe790321 100644 --- a/mix.exs +++ b/mix.exs @@ -341,12 +341,13 @@ defmodule EMQXUmbrella.MixProject do "apps/emqx/etc/certs", Path.join(etc, "certs") ) + # required by emqx_dashboard - Mix.Generator.copy_file( - "apps/emqx_dashboard/etc/i18n.conf", + 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( diff --git a/scripts/merge-config.escript b/scripts/merge-config.escript index 67ac70923..49eba8169 100755 --- a/scripts/merge-config.escript +++ b/scripts/merge-config.escript @@ -62,8 +62,7 @@ get_cfgs(Dir, Cfgs) -> %% the conf name must start with emqx %% because there are some other conf, and these conf don't start with emqx Confs = filelib:wildcard("emqx*.conf", EtcDir), - Confs1 = lists:filter(fun(N) -> string:find(N, "i18n") =:= nomatch end, Confs), - NewCfgs = [filename:join([EtcDir, Name]) || Name <- Confs1], + NewCfgs = [filename:join([EtcDir, Name]) || Name <- Confs], try_enter_child(Dir, Files, NewCfgs ++ Cfgs) end end. diff --git a/scripts/merge-i18n.escript b/scripts/merge-i18n.escript index d1bc0405c..13a7df27b 100755 --- a/scripts/merge-i18n.escript +++ b/scripts/merge-i18n.escript @@ -3,7 +3,7 @@ -mode(compile). main(_) -> - {ok, BaseConf} = file:read_file("apps/emqx_dashboard/etc/emqx_dashboard_i18n.conf"), + {ok, BaseConf} = file:read_file("apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf"), Cfgs = get_all_cfgs("apps/"), Conf = [merge(BaseConf, Cfgs), @@ -40,14 +40,12 @@ get_cfgs(Dir, Cfgs) -> Cfgs; _ -> Files = filelib:wildcard("*", Dir), - case lists:member("etc", Files) of + case lists:member("i18n", Files) of false -> try_enter_child(Dir, Files, Cfgs); true -> - EtcDir = filename:join([Dir, "etc"]), - %% the conf name must start with emqx - %% because there are some other conf, and these conf don't start with emqx - Confs = filelib:wildcard("emqx*_i18n.conf", EtcDir), + 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 From 164754b1826e942b7c9e557a4ca0f7f49355c700 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 15 Apr 2022 11:30:18 +0800 Subject: [PATCH 3/4] test: dashboard ct failed with lack i18n_lang --- Makefile | 2 +- .../test/emqx_auto_subscribe_SUITE.erl | 103 +++++++++--------- .../emqx_conf/test/emqx_conf_schema_tests.erl | 3 +- apps/emqx_dashboard/src/emqx_dashboard.erl | 10 +- .../test/emqx_dashboard_SUITE.erl | 7 +- .../test/emqx_dashboard_api_test_helpers.erl | 9 +- .../test/emqx_dashboard_bad_api_SUITE.erl | 10 +- .../test/emqx_dashboard_error_code_SUITE.erl | 10 +- .../test/emqx_dashboard_monitor_SUITE.erl | 10 +- .../test/emqx_mgmt_api_test_util.erl | 10 +- .../emqx_enterprise_conf_schema_tests.erl | 3 +- 11 files changed, 77 insertions(+), 100 deletions(-) diff --git a/Makefile b/Makefile index f87ce0bcb..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 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/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/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 6f907d735..870003c1c 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -172,8 +172,8 @@ 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 = emqx:etc_file("i18n.conf"), - Lang = emqx_conf:get([dashboard, i18n_lang]), + File = i18n_file(), + Lang = emqx_conf:get([dashboard, i18n_lang], en), init_i18n(File, Lang). ranch_opts(RanchOptions) -> @@ -267,3 +267,9 @@ return_unauthorized(Code, Message) -> <<"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/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_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/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. From f98cb972e2bd305ffebd97664730f7779f45146e Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 15 Apr 2022 13:43:26 +0800 Subject: [PATCH 4/4] fix: dashboard ct failed --- apps/emqx_dashboard/src/emqx_dashboard.erl | 1 + .../src/emqx_dashboard_schema.erl | 2 +- .../src/emqx_dashboard_swagger.erl | 10 ++-- .../test/emqx_swagger_parameter_SUITE.erl | 22 +++++++ .../test/emqx_swagger_requestBody_SUITE.erl | 59 +++++++++---------- .../test/emqx_swagger_response_SUITE.erl | 39 +++++++----- 6 files changed, 82 insertions(+), 51 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 870003c1c..c0f9cddf8 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -27,6 +27,7 @@ -export([ init_i18n/2, + init_i18n/0, get_i18n/0, clear_i18n/0 ]). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 5531b16d5..bbe198f46 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -197,7 +197,7 @@ cors(_) -> i18n_lang(type) -> ?ENUM([en, zh]); i18n_lang(default) -> zh; i18n_lang('readOnly') -> true; -i18n_lang(desc) -> "i18n language"; +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 644654306..7ef374ec3 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -306,7 +306,7 @@ check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map %% 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)), @@ -424,11 +424,11 @@ resolve_desc(Key, Struct) -> false -> Desc end. -request_body(#{content := _} = Content, _Module) -> +request_body(#{content := _} = Content, _Module, _Options) -> {Content, []}; -request_body([], _Module) -> +request_body([], _Module, _Options) -> {[], []}; -request_body(Schema, Module) -> +request_body(Schema, Module, Options) -> {{Props, Refs}, Examples} = case hoconsc:is_schema(Schema) of true -> @@ -436,7 +436,7 @@ request_body(Schema, Module) -> SchemaExamples = hocon_schema:field_schema(Schema, examples), {hocon_schema_to_spec(HoconSchema, Module), SchemaExamples}; false -> - {parse_object(Schema, Module, #{}), undefined} + {parse_object(Schema, Module, Options), undefined} end, {#{<<"content">> => content(Props, Examples)}, Refs}. 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",