From 9ec804ae03bb20db0fab4c057861173148eb0d3e Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 May 2022 14:43:35 +0800 Subject: [PATCH] feat: generate example.conf from schemas --- .../emqx_limiter/src/emqx_limiter_schema.erl | 13 +- apps/emqx/src/emqx_schema.erl | 16 +- apps/emqx_authz/etc/emqx_authz.conf | 5 - apps/emqx_conf/src/emqx_conf.erl | 14 +- apps/emqx_conf/src/emqx_conf_schema.erl | 36 +- apps/emqx_conf/src/hocon_schema_example.erl | 539 ++++++++++++++++++ .../src/emqx_connector_schema.erl | 2 +- .../src/emqx_dashboard_schema.erl | 1 + apps/emqx_gateway/src/emqx_gateway_schema.erl | 5 +- .../src/emqx_retainer_schema.erl | 5 +- .../src/emqx_rule_engine_schema.erl | 2 +- .../src/emqx_slow_subs_schema.erl | 2 +- 12 files changed, 597 insertions(+), 43 deletions(-) create mode 100644 apps/emqx_conf/src/hocon_schema_example.erl diff --git a/apps/emqx/src/emqx_limiter/src/emqx_limiter_schema.erl b/apps/emqx/src/emqx_limiter/src/emqx_limiter_schema.erl index dd3b81f6e..5d1d720e6 100644 --- a/apps/emqx/src/emqx_limiter/src/emqx_limiter_schema.erl +++ b/apps/emqx/src/emqx_limiter/src/emqx_limiter_schema.erl @@ -105,7 +105,18 @@ fields(limiter_opts) -> {bucket, sc( map("bucket_name", ref(bucket_opts)), - #{desc => ?DESC(bucket_cfg), default => #{<<"default">> => #{}}} + #{ + desc => ?DESC(bucket_cfg), + default => #{<<"default">> => #{}}, + examples => #{ + <<"mybucket-name">> => #{ + <<"rate">> => <<"infinity">>, + <<"capcity">> => <<"infinity">>, + <<"initial">> => <<"100">>, + <<"per_client">> => #{<<"rate">> => <<"infinity">>} + } + } + } )} ]; fields(bucket_opts) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index ee59b039d..423ac96f6 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -118,7 +118,7 @@ roots(high) -> )}, {"zones", sc( - map("name", ref("zone")), + map("my_zone_name", ref("zone")), #{desc => ?DESC(zones)} )}, {"mqtt", @@ -744,7 +744,7 @@ fields("listeners") -> [ {"tcp", sc( - map(name, ref("mqtt_tcp_listener")), + map(default, ref("mqtt_tcp_listener")), #{ desc => ?DESC(fields_listeners_tcp), required => {false, recursively} @@ -752,7 +752,7 @@ fields("listeners") -> )}, {"ssl", sc( - map(name, ref("mqtt_ssl_listener")), + map(default, ref("mqtt_ssl_listener")), #{ desc => ?DESC(fields_listeners_ssl), required => {false, recursively} @@ -760,7 +760,7 @@ fields("listeners") -> )}, {"ws", sc( - map(name, ref("mqtt_ws_listener")), + map(default, ref("mqtt_ws_listener")), #{ desc => ?DESC(fields_listeners_ws), required => {false, recursively} @@ -768,7 +768,7 @@ fields("listeners") -> )}, {"wss", sc( - map(name, ref("mqtt_wss_listener")), + map(default, ref("mqtt_wss_listener")), #{ desc => ?DESC(fields_listeners_wss), required => {false, recursively} @@ -776,7 +776,7 @@ fields("listeners") -> )}, {"quic", sc( - map(name, ref("mqtt_quic_listener")), + map(default, ref("mqtt_quic_listener")), #{ desc => ?DESC(fields_listeners_quic), required => {false, recursively} @@ -1582,7 +1582,7 @@ base_listener() -> )}, {"limiter", sc( - map("ratelimit's type", emqx_limiter_schema:bucket_name()), + map("ratelimit_name", emqx_limiter_schema:bucket_name()), #{ desc => ?DESC(base_listener_limiter), default => #{} @@ -2183,7 +2183,7 @@ authentication(Type) -> %% authentication schema is lazy to make it more 'plugable' %% the type checks are done in emqx_auth application when it boots. %% and in emqx_authentication_config module for runtime changes. - Default = hoconsc:lazy(hoconsc:union([typerefl:map(), hoconsc:array(typerefl:map())])), + Default = hoconsc:lazy(hoconsc:union([hoconsc:array(typerefl:map())])), %% as the type is lazy, the runtime module injection %% from EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY %% is for now only affecting document generation. diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 844d8a2e1..09dc0febc 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,9 +1,4 @@ authorization { - cache: { - enable: true - max_size: 32 - ttl: "60s" - } deny_action: ignore no_match: allow sources: [ diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index 3f843063f..e9a6abadb 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -144,7 +144,8 @@ dump_schema(Dir, SchemaModule, I18nFile) -> lists:foreach( fun(Lang) -> gen_config_md(Dir, I18nFile, SchemaModule, Lang), - gen_hot_conf_schema_json(Dir, I18nFile, Lang) + gen_hot_conf_schema_json(Dir, I18nFile, Lang), + gen_example_conf(Dir, I18nFile, SchemaModule, Lang) end, [en, zh] ), @@ -173,6 +174,12 @@ gen_config_md(Dir, I18nFile, SchemaModule, Lang0) -> io:format(user, "===< Generating: ~s~n", [SchemaMdFile]), ok = gen_doc(SchemaMdFile, SchemaModule, I18nFile, Lang). +gen_example_conf(Dir, I18nFile, SchemaModule, Lang0) -> + Lang = atom_to_list(Lang0), + SchemaMdFile = filename:join([Dir, "emqx-" ++ Lang ++ ".conf.example"]), + io:format(user, "===< Generating: ~s~n", [SchemaMdFile]), + ok = gen_example(SchemaMdFile, SchemaModule, I18nFile, Lang). + %% @doc return the root schema module. -spec schema_module() -> module(). schema_module() -> @@ -195,6 +202,11 @@ gen_doc(File, SchemaModule, I18nFile, Lang) -> Doc = hocon_schema_md:gen(SchemaModule, Opts), file:write_file(File, Doc). +gen_example(File, SchemaModule, I18nFile, Lang) -> + Opts = #{title => <<"Title">>, body => <<"Body">>, desc_file => I18nFile, lang => Lang}, + Example = hocon_schema_example:gen(SchemaModule, Opts), + file:write_file(File, Example). + check_cluster_rpc_result(Result) -> case Result of {ok, _TnxId, Res} -> diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 8e5fc99f2..b68a7fc2b 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -76,22 +76,22 @@ roots() -> [ {"node", sc( - ref("node"), + ?R_REF("node"), #{translate_to => ["emqx"]} )}, {"cluster", sc( - ref("cluster"), + ?R_REF("cluster"), #{translate_to => ["ekka"]} )}, {"log", sc( - ref("log"), + ?R_REF("log"), #{translate_to => ["kernel"]} )}, {"rpc", sc( - ref("rpc"), + ?R_REF("rpc"), #{translate_to => ["gen_rpc"]} )} ] ++ @@ -166,27 +166,27 @@ fields("cluster") -> )}, {"static", sc( - ref(cluster_static), + ?R_REF(cluster_static), #{} )}, {"mcast", sc( - ref(cluster_mcast), + ?R_REF(cluster_mcast), #{} )}, {"dns", sc( - ref(cluster_dns), + ?R_REF(cluster_dns), #{} )}, {"etcd", sc( - ref(cluster_etcd), + ?R_REF(cluster_etcd), #{} )}, {"k8s", sc( - ref(cluster_k8s), + ?R_REF(cluster_k8s), #{} )} ]; @@ -328,7 +328,7 @@ fields(cluster_etcd) -> )}, {"ssl", sc( - hoconsc:ref(emqx_schema, "ssl_client_opts"), + ?R_REF(emqx_schema, "ssl_client_opts"), #{ desc => ?DESC(cluster_etcd_ssl), 'readOnly' => true @@ -512,7 +512,7 @@ fields("node") -> )}, {"cluster_call", sc( - ref("cluster_call"), + ?R_REF("cluster_call"), #{'readOnly' => true} )}, {"db_backend", @@ -783,10 +783,10 @@ fields("rpc") -> ]; fields("log") -> [ - {"console_handler", ref("console_handler")}, + {"console_handler", ?R_REF("console_handler")}, {"file_handlers", sc( - map(name, ref("log_file_handler")), + map(name, ?R_REF("log_file_handler")), #{desc => ?DESC("log_file_handlers")} )}, {"error_logger", @@ -814,7 +814,7 @@ fields("log_file_handler") -> )}, {"rotation", sc( - ref("log_rotation"), + ?R_REF("log_rotation"), #{} )}, {"max_size", @@ -1137,8 +1137,8 @@ log_handler_common_confs(Enable) -> desc => ?DESC("common_handler_flush_qlen") } )}, - {"overload_kill", sc(ref("log_overload_kill"), #{})}, - {"burst_limit", sc(ref("log_burst_limit"), #{})}, + {"overload_kill", sc(?R_REF("log_overload_kill"), #{})}, + {"burst_limit", sc(?R_REF("log_burst_limit"), #{})}, {"supervisor_reports", sc( hoconsc:enum([error, progress]), @@ -1251,8 +1251,6 @@ sc(Type, Meta) -> hoconsc:mk(Type, Meta). map(Name, Type) -> hoconsc:map(Name, Type). -ref(Field) -> hoconsc:ref(?MODULE, Field). - options(static, Conf) -> [{seeds, conf_get("cluster.static.seeds", Conf, [])}]; options(mcast, Conf) -> @@ -1321,7 +1319,7 @@ emqx_schema_high_prio_roots() -> Authz = {"authorization", sc( - hoconsc:ref(?MODULE, "authorization"), + ?R_REF("authorization"), #{desc => ?DESC(authorization)} )}, lists:keyreplace("authorization", 1, Roots, Authz). diff --git a/apps/emqx_conf/src/hocon_schema_example.erl b/apps/emqx_conf/src/hocon_schema_example.erl new file mode 100644 index 000000000..189be6787 --- /dev/null +++ b/apps/emqx_conf/src/hocon_schema_example.erl @@ -0,0 +1,539 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021-2022 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(hocon_schema_example). +-include_lib("hocon/include/hoconsc.hrl"). + +-export([gen/2]). + +-define(COMMENT, "# "). +-define(COMMENT2, "## "). +-define(INDENT, " "). +-define(NL, io_lib:nl()). +-define(DOC, "@doc "). +-define(TYPE, "@type "). +-define(PATH, "@path "). +-define(LINK, "@link "). +-define(DEFAULT, "@default "). +-define(BIND, ": "). + +gen(Schema, undefined) -> + gen(Schema, "# HOCON Example"); +gen(Schema, Title) when is_list(Title) orelse is_binary(Title) -> + gen(Schema, #{title => Title, body => <<>>}); +gen(Schema, #{title := Title, body := Body} = Opts) -> + File = maps:get(desc_file, Opts, undefined), + Lang = maps:get(lang, Opts, "en"), + [Roots | Fields0] = hocon_schema_json:gen(Schema, #{desc_file => File, lang => Lang}), + Fields = lists:foldl(fun(F = #{full_name := Name}, Acc) -> Acc#{Name => F} end, #{}, Fields0), + #{fields := RootKeys} = Roots, + FmtOpts = #{tid => new_link_cache(), indent => "", comment => false}, + try + Structs = lists:map( + fun(Root) -> + [ + fmt_desc(Root, ""), + fmt_field(Root, Fields, "", FmtOpts) + ] + end, + RootKeys + ), + [ + ?COMMENT2, + Title, + ?NL, + ?NL, + ?COMMENT2, + Body, + ?NL, + ?NL, + Structs + ] + after + delete_link_cache(FmtOpts) + end. + +fmt_field(#{type := #{kind := struct, name := SubName}, name := Name} = Field, All, Path0, Opts) -> + case maps:find(SubName, All) of + {ok, #{fields := SubFields}} -> + #{indent := Indent, comment := Comment} = Opts, + Opts1 = Opts#{indent => Indent ++ ?INDENT}, + {PathName, ValName} = resolve_name(Name), + Path = [str(PathName) | Path0], + SubStructs = + case maps:get(examples, Field, #{}) of + #{} = Example -> + fmt_field_with_example(Path, SubFields, Example, All, Opts1); + {union, UnionExamples} -> + Examples1 = filter_union_example(UnionExamples, SubFields), + fmt_field_with_example(Path, SubFields, Examples1, All, Opts1); + {array, ArrayExamples} -> + lists:flatmap( + fun(SubExample) -> + fmt_field_with_example(Path, SubFields, SubExample, All, Opts1) + end, + ArrayExamples + ) + end, + [ + Indent, + comment(Comment), + ValName, + " {", + ?NL, + lists:join(?NL, SubStructs), + Indent, + comment(Comment), + " }", + ?NL + ]; + Unknown -> + throw({error, {Path0, SubName, Unknown}}) + end; +fmt_field(#{type := #{kind := primitive, name := TypeName}} = Field, _All, Path, Opts) -> + Name = str(maps:get(name, Field)), + Fix = fmt_fix_header(Field, TypeName, [Name | Path], Opts), + [Fix, fmt_examples(Name, Field, Opts)]; +fmt_field(#{type := #{kind := singleton, name := SingleTon}} = Field, _All, Path, Opts) -> + Name = str(maps:get(name, Field)), + #{indent := Indent, comment := Comment} = Opts, + Fix = fmt_fix_header(Field, "singleton", [Name | Path], Opts), + [Fix, fmt(Indent, Comment, Name, SingleTon)]; +fmt_field(#{type := #{kind := enum, symbols := Symbols}} = Field, _All, Path, Opts) -> + TypeName = ["enum: ", lists:join(" | ", Symbols)], + Name = str(maps:get(name, Field)), + Fix = fmt_fix_header(Field, TypeName, [str(Name) | Path], Opts), + [Fix, fmt_examples(Name, Field, Opts)]; +fmt_field(#{type := #{kind := union, members := Members0} = Type} = Field, All, Path0, Opts) -> + Name = str(maps:get(name, Field)), + Names = lists:map(fun(#{name := N}) -> N end, Members0), + Path = [Name | Path0], + TypeStr = ["union() ", lists:join(" | ", Names)], + Fix = fmt_fix_header(Field, TypeStr, Path, Opts), + Link = fmt_union_link(Type, Path, Opts), + Fix1 = [Fix, Link], + case Link =:= "" andalso need_comment_example(union, Opts, Type, Path) of + true -> + #{indent := Indent} = Opts, + Indent1 = Indent ++ ?INDENT, + Opts1 = Opts#{indent => Indent1}, + case fmt_sub_fields(Opts1, Field, All, Path0) of + [] -> fallback_to_example(Field, Fix1, Indent1, Name, Opts1, Indent, ""); + ValFields -> [Fix1, ValFields, ?NL] + end; + false -> + [Fix1, ?NL] + end; +fmt_field(#{type := #{kind := map, name := MapName} = Type} = Field, All, Path0, Opts) -> + Name = str(maps:get(name, Field)), + #{indent := Indent} = Opts, + Path = [Name | Path0], + Path1 = ["$" ++ str(MapName) | Path], + Fix = fmt_fix_header(Field, "map_struct()", Path, Opts), + Link = fmt_map_link(Path1, Type, All, Opts), + Fix1 = [Fix, Link], + case Link =:= "" andalso need_comment_example(map, Opts, Path1) of + true -> + Indent1 = Indent ++ ?INDENT, + Opts1 = Opts#{indent => Indent1}, + ValFields = fmt_sub_fields(Opts1, Field, All, Path), + [Fix1, Indent1, ?COMMENT, Name, ?BIND, ?NL, ValFields, ?NL]; + false -> + [Fix1, ?NL] + end; +fmt_field(#{type := #{kind := array} = Type} = Field, All, Path0, Opts) -> + #{indent := Indent, comment := Comment} = Opts, + Name = str(maps:get(name, Field)), + Path = [Name | Path0], + Fix = fmt_fix_header(Field, "array()", Path, Opts), + Link = fmt_array_link(Type, Path, Opts), + Fix1 = [Fix, Link], + case Link =:= "" andalso need_comment_example(array, Opts, Type, Path) of + true -> + Indent1 = Indent ++ ?INDENT, + Opts1 = Opts#{indent => Indent1}, + case fmt_sub_fields(Opts1, Field, All, Path) of + [] -> + fallback_to_example(Field, Fix1, Indent1, Name, Opts1, Indent, "[]"); + ValFields -> + [ + Fix1, + Indent1, + comment(Comment), + Name, + ?BIND, + "[", + ?NL, + ValFields, + ?NL, + Indent1, + comment(Comment), + "]", + ?NL + ] + end; + false -> + [Fix1, ?NL] + end. + +fmt(Indent, Comment, Name, Value) -> + [Indent, comment(Comment), Name, ?BIND, Value, ?NL]. + +fallback_to_example(Field, Fix1, Indent1, Name, Opts, Indent, Default) -> + case Field of + #{examples := Examples} -> + [ + Fix1, + Indent1, + ?COMMENT, + Name, + ?BIND, + fmt_example(Examples, Opts#{comment => true}), + ?NL + ]; + _ -> + Default2 = + case get_default(Field, Opts) of + undefined -> Default; + Default1 -> Default1 + end, + [Fix1, Indent, ?COMMENT, Name, ?BIND, Default2, ?NL] + end. + +fmt_field_with_example(Path, SubFields, Examples, All, Opts1) -> + lists:map( + fun(F) -> + #{name := N} = F, + case maps:find(N, Examples) of + {ok, SubExample} -> + fmt_field(F#{examples => SubExample}, All, Path, Opts1); + error -> + fmt_field(F, All, Path, Opts1) + end + end, + SubFields + ). + +fmt_sub_fields(Opts, Field, All, Path) -> + Opts1 = Opts#{comment => true}, + SubFields = get_sub_fields(Field), + [fmt_field(F, All, Path, Opts1) || F <- SubFields]. + +get_sub_fields(#{type := #{kind := array, elements := ElemT}, name := Name} = Field) -> + case is_simple_type(ElemT) of + true -> + []; + false -> + Examples = + case get_examples(Name, Field) of + undefined -> []; + Example0 -> Example0 + end, + [ + #{ + name => {"$INDEX", str(Name) ++ ".$INDEX"}, + type => ElemT, + examples => {array, Examples} + } + ] + end; +get_sub_fields(#{type := #{kind := union, members := Members}, name := Name} = Field) -> + case is_simple_type(Members) of + true -> + []; + false -> + Example = + case get_examples(Name, Field) of + undefined -> + []; + [Example0] when is_map(Example0) -> + [Value || #{value := Value} <- maps:values(Example0)]; + %% TODO array + _ -> + [] + end, + lists:map( + fun(M) -> #{name => Name, type => M, examples => {union, Example}} end, Members + ) + end; +get_sub_fields(#{type := #{kind := map, values := ValT, name := MapName0}} = Field) -> + MapName = "$" ++ str(MapName0), + case get_examples(MapName0, Field) of + undefined -> + [#{name => MapName, type => ValT}]; + [] -> + [#{name => MapName, type => ValT}]; + Examples -> + lists:map( + fun(Example) -> + [{SubName, SubValue}] = maps:to_list(Example), + #{name => {MapName, SubName}, type => ValT, examples => SubValue} + end, + Examples + ) + end. + +filter_union_example(Examples0, SubFields) -> + TargetKeys = lists:sort([binary_to_atom(Name) || #{name := Name} <- SubFields]), + Examples = + lists:filtermap( + fun(Example) -> + case lists:all(fun(K) -> lists:member(K, TargetKeys) end, maps:keys(Example)) of + true -> {true, ensure_bin_key(Example)}; + false -> false + end + end, + Examples0 + ), + case Examples of + [Example] -> Example; + [] -> #{}; + Other -> throw({error, {find_union_example_failed, Examples, SubFields, Other}}) + end. + +ensure_bin_key(Map) -> + maps:fold( + fun + (K0, V0 = #{}, Acc) -> Acc#{bin(K0) => ensure_bin_key(V0)}; + (K0, V, Acc) -> Acc#{bin(K0) => V} + end, + #{}, + Map + ). + +fmt_desc(#{desc := Desc0}, Indent) -> + Target = iolist_to_binary([?NL, Indent, ?COMMENT2]), + Desc = string:trim(Desc0, both), + replace_nl(Indent, true, Desc, Target); +fmt_desc(_, _) -> + <<"">>. + +fmt_type(Type, Indent) -> + [Indent, ?COMMENT2, ?TYPE, Type, ?NL]. + +fmt_path(Path, Indent) -> [Indent, ?COMMENT2, ?PATH, hocon_schema:path(Path), ?NL]. + +fmt_fix_header(Field, Type, Path, #{indent := Indent}) -> + [ + fmt_desc(Field, Indent), + fmt_path(Path, Indent), + fmt_type(Type, Indent), + fmt_default(Field, Indent) + ]. + +fmt_map_link(Path0, Type, All, Opts) -> + case Type of + #{values := #{name := ValueName}} -> + fmt_map_link2(Path0, ValueName, All, Opts); + #{values := #{members := Members}} -> + lists:map(fun(M) -> fmt_map_link(Path0, M, All, Opts) end, Members); + _ -> + [] + end. + +fmt_map_link2(Path0, ValueName, All, Opts) -> + Paths = + case maps:find(ValueName, All) of + {ok, #{paths := SubPaths}} -> SubPaths; + _ -> [] + end, + PathStr = hocon_schema:path(Path0), + Path = bin(PathStr), + #{indent := Indent} = Opts, + case find_link(Opts, {map, PathStr}) of + {ok, Link} -> + [Indent, ?COMMENT2, ?LINK, Link, ?NL]; + {error, not_found} -> + case lists:member(Path, Paths) of + true -> + insert_link(Opts, [{{map, binary_to_list(P)}, Path} || P <- Paths, P =/= Path]); + false -> + ok + end, + "" + end. + +fmt_union_link(Type = #{members := Members}, Path, Opts = #{indent := Indent}) -> + case find_link(Opts, {union, Type}) of + {ok, Link} -> + link(Link, Indent); + {error, not_found} -> + case is_simple_type(Members) of + true -> ok; + false -> insert_link(Opts, {{union, Type}, Path}) + end, + "" + end. + +fmt_array_link(Type = #{elements := ElemT}, Path, Opts = #{indent := Indent}) -> + case find_link(Opts, {array, Type}) of + {ok, Link} -> + link(Link, Indent); + {error, not_found} -> + case is_simple_type(ElemT) of + true -> ok; + false -> insert_link(Opts, {{array, Type}, Path}) + end, + "" + end. + +link(Link, Indent) -> + [Indent, ?COMMENT2, ?LINK, hocon_schema:path(Link), ?NL]. + +is_simple_type(Types) when is_list(Types) -> + lists:all( + fun(#{kind := Kind}) -> + Kind =:= primitive orelse Kind =:= singleton + end, + Types + ); +is_simple_type(Type) -> + is_simple_type([Type]). + +need_comment_example(map, Opts, Path) -> + case find_link(Opts, {map, hocon_schema:path(Path)}) of + {ok, _} -> false; + {error, not_found} -> true + end. + +need_comment_example(Type, Opts, Key, Link) when Type =:= union; Type =:= array -> + case find_link(Opts, {union, Key}) of + {ok, Link} -> true; + {error, not_found} -> true; + {ok, _} -> false + end. + +get_examples(_MapName, #{examples := Examples}) -> + ensure_list(Examples); +get_examples(MapName, #{default := #{hocon := Hocon}}) -> + case hocon:binary(Hocon) of + {ok, Default} -> [#{MapName => Default}]; + {error, _} -> [#{MapName => Hocon}] + end; +get_examples(_, _) -> + undefined. + +fmt_examples(Name, #{examples := {union, Examples}}, Opts) -> + fmt_examples(Name, #{examples => Examples}, Opts); +fmt_examples(Name, #{examples := Examples}, Opts) -> + #{indent := Indent, comment := Comment} = Opts, + lists:map( + fun(E) -> + [Indent, comment(Comment), Name, ?BIND, fmt_example(E, Opts), ?NL] + end, + ensure_list(Examples) + ); +fmt_examples(Name, Field, Opts = #{indent := Indent, comment := Comment}) -> + case get_default(Field, Opts) of + undefined -> [Indent, ?COMMENT, Name, ?BIND, ?NL]; + Default -> fmt(Indent, Comment, Name, Default) + end. + +ensure_list(L) when is_list(L) -> L; +ensure_list(T) -> [T]. + +fmt_example(Value, #{indent := Indent0, comment := Comment}) -> + case hocon_pp:do(Value, #{newline => "", embedded => true}) of + [OneLine] -> + [try_to_remove_quote(OneLine)]; + Lines -> + Indent = Indent0 ++ ?INDENT, + Target = iolist_to_binary([?NL, Indent, comment(Comment)]), + [ + ?NL, + Indent, + comment(Comment), + binary:replace(bin(Lines), [<<"\n">>], Target, [global]), + ?NL + ] + end. + +fmt_default(Field, Indent) -> + case get_default(Field, #{indent => Indent, comment => true}) of + undefined -> ""; + Default -> [Indent, ?COMMENT2, ?DEFAULT, Default, ?NL] + end. + +get_default(#{default := Default}, Opts) when is_map(Opts) -> + #{indent := Indent, comment := Comment} = Opts, + get_default(Default, Indent, Comment); +get_default(_, _Opts) -> + undefined. + +-define(RE, <<"^[A-Za-z0-9\"]+$">>). + +get_default(#{oneliner := true, hocon := Content}, _Indent, _Comment) -> + try_to_remove_quote(Content); +get_default(#{oneliner := false, hocon := Content}, Indent0, Comment) -> + Target = iolist_to_binary([?NL, Indent0, comment2(Comment), ?INDENT]), + replace_nl(Indent0, Comment, Content, Target); +get_default(Bin, _Indent, _Comment) -> + Bin. + +replace_nl(Indent0, Comment, Content, Target) -> + [ + ?NL, + Indent0, + comment2(Comment), + ?INDENT, + binary:replace(Content, [<<"\n">>], Target, [global]), + ?NL + ]. + +try_to_remove_quote(Content) -> + case re:run(Content, ?RE) of + nomatch -> + Content; + _ -> + case string:trim(Content, both, [$"]) of + <<"">> -> Content; + Other -> Other + end + end. + +bin(S) when is_list(S) -> unicode:characters_to_binary(S, utf8); +bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom); +bin(Int) when is_integer(Int) -> integer_to_binary(Int); +bin(Bin) -> Bin. + +str(A) when is_atom(A) -> atom_to_list(A); +str(S) when is_list(S) -> S; +str(B) when is_binary(B) -> binary_to_list(B); +str({KeyName, _ValName}) -> str(KeyName). + +comment(true) -> ?COMMENT; +comment(false) -> "". + +comment2(true) -> ?COMMENT2; +comment2(false) -> "". + +new_link_cache() -> + ets:new(?MODULE, [private, set, {keypos, 1}]). + +delete_link_cache(#{tid := Tid}) -> + ets:delete(Tid). + +find_link(#{tid := Tid}, Key) -> + case ets:lookup(Tid, Key) of + [{_, Value}] -> {ok, Value}; + [] -> {error, not_found} + end. + +insert_link(#{tid := Tid}, Item) -> + ets:insert(Tid, Item). + +resolve_name({N1, N2}) -> {N1, N2}; +resolve_name(N) -> {N, N}. diff --git a/apps/emqx_connector/src/emqx_connector_schema.erl b/apps/emqx_connector/src/emqx_connector_schema.erl index 3f1d4f4aa..f92c12287 100644 --- a/apps/emqx_connector/src/emqx_connector_schema.erl +++ b/apps/emqx_connector/src/emqx_connector_schema.erl @@ -64,7 +64,7 @@ fields("connectors") -> mk( hoconsc:map( name, - hoconsc:union([ref(emqx_connector_mqtt_schema, "connector")]) + ref(emqx_connector_mqtt_schema, "connector") ), #{desc => ?DESC("mqtt")} )} diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index c358aef17..12a799418 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -171,6 +171,7 @@ bind(Port) -> #{ default => Port, required => true, + extra => #{example => [Port, "0.0.0.0:" ++ integer_to_list(Port)]}, desc => ?DESC(bind) } )}. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 83f51d110..f3b3a0fa6 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -567,7 +567,8 @@ authentication_schema() -> emqx_authn_schema:authenticator_type(), #{ required => {false, recursively}, - desc => ?DESC(gateway_common_authentication) + desc => ?DESC(gateway_common_authentication), + examples => emqx_authn_api:authenticator_examples() } ). @@ -606,7 +607,7 @@ gateway_common_options() -> ]. mountpoint() -> - mountpoint(<<>>). + mountpoint(<<"">>). mountpoint(Default) -> sc( binary(), diff --git a/apps/emqx_retainer/src/emqx_retainer_schema.erl b/apps/emqx_retainer/src/emqx_retainer_schema.erl index be2a64868..db419bf1d 100644 --- a/apps/emqx_retainer/src/emqx_retainer_schema.erl +++ b/apps/emqx_retainer/src/emqx_retainer_schema.erl @@ -109,10 +109,7 @@ sc(Type, DescId, Default) -> hoconsc:mk(Type, #{default => Default, desc => ?DESC(DescId)}). backend_config() -> - hoconsc:mk( - hoconsc:union([hoconsc:ref(?MODULE, mnesia_config)]), - #{desc => ?DESC(backend)} - ). + hoconsc:mk(hoconsc:ref(?MODULE, mnesia_config), #{desc => ?DESC(backend)}). retainer_indices(type) -> list(list(integer())); diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl index 61b8dd4b9..55d8586e4 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -186,7 +186,7 @@ rule_name() -> binary(), #{ desc => ?DESC("rules_name"), - default => "", + default => <<"">>, required => false, example => "foo" } diff --git a/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl b/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl index d570e2004..b7f7337d2 100644 --- a/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl +++ b/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl @@ -32,7 +32,7 @@ fields("slow_subs") -> )}, {stats_type, sc( - hoconsc:union([whole, internal, response]), + hoconsc:enum([whole, internal, response]), whole, stats_type )}