From 630cc8ee346a357660cda22f3fc3ef686366e08c Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 15 Apr 2022 08:42:13 +0800 Subject: [PATCH] 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.