Ver código fonte

feat: generate example.conf from schemas

Zhongwen Deng 3 anos atrás
pai
commit
9ec804ae03

+ 12 - 1
apps/emqx/src/emqx_limiter/src/emqx_limiter_schema.erl

@@ -105,7 +105,18 @@ fields(limiter_opts) ->
         {bucket,
         {bucket,
             sc(
             sc(
                 map("bucket_name", ref(bucket_opts)),
                 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) ->
 fields(bucket_opts) ->

+ 8 - 8
apps/emqx/src/emqx_schema.erl

@@ -118,7 +118,7 @@ roots(high) ->
             )},
             )},
         {"zones",
         {"zones",
             sc(
             sc(
-                map("name", ref("zone")),
+                map("my_zone_name", ref("zone")),
                 #{desc => ?DESC(zones)}
                 #{desc => ?DESC(zones)}
             )},
             )},
         {"mqtt",
         {"mqtt",
@@ -744,7 +744,7 @@ fields("listeners") ->
     [
     [
         {"tcp",
         {"tcp",
             sc(
             sc(
-                map(name, ref("mqtt_tcp_listener")),
+                map(default, ref("mqtt_tcp_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_tcp),
                     desc => ?DESC(fields_listeners_tcp),
                     required => {false, recursively}
                     required => {false, recursively}
@@ -752,7 +752,7 @@ fields("listeners") ->
             )},
             )},
         {"ssl",
         {"ssl",
             sc(
             sc(
-                map(name, ref("mqtt_ssl_listener")),
+                map(default, ref("mqtt_ssl_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_ssl),
                     desc => ?DESC(fields_listeners_ssl),
                     required => {false, recursively}
                     required => {false, recursively}
@@ -760,7 +760,7 @@ fields("listeners") ->
             )},
             )},
         {"ws",
         {"ws",
             sc(
             sc(
-                map(name, ref("mqtt_ws_listener")),
+                map(default, ref("mqtt_ws_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_ws),
                     desc => ?DESC(fields_listeners_ws),
                     required => {false, recursively}
                     required => {false, recursively}
@@ -768,7 +768,7 @@ fields("listeners") ->
             )},
             )},
         {"wss",
         {"wss",
             sc(
             sc(
-                map(name, ref("mqtt_wss_listener")),
+                map(default, ref("mqtt_wss_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_wss),
                     desc => ?DESC(fields_listeners_wss),
                     required => {false, recursively}
                     required => {false, recursively}
@@ -776,7 +776,7 @@ fields("listeners") ->
             )},
             )},
         {"quic",
         {"quic",
             sc(
             sc(
-                map(name, ref("mqtt_quic_listener")),
+                map(default, ref("mqtt_quic_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_quic),
                     desc => ?DESC(fields_listeners_quic),
                     required => {false, recursively}
                     required => {false, recursively}
@@ -1582,7 +1582,7 @@ base_listener() ->
             )},
             )},
         {"limiter",
         {"limiter",
             sc(
             sc(
-                map("ratelimit's type", emqx_limiter_schema:bucket_name()),
+                map("ratelimit_name", emqx_limiter_schema:bucket_name()),
                 #{
                 #{
                     desc => ?DESC(base_listener_limiter),
                     desc => ?DESC(base_listener_limiter),
                     default => #{}
                     default => #{}
@@ -2183,7 +2183,7 @@ authentication(Type) ->
     %% authentication schema is lazy to make it more 'plugable'
     %% authentication schema is lazy to make it more 'plugable'
     %% the type checks are done in emqx_auth application when it boots.
     %% the type checks are done in emqx_auth application when it boots.
     %% and in emqx_authentication_config module for runtime changes.
     %% 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
     %% as the type is lazy, the runtime module injection
     %% from EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY
     %% from EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY
     %% is for now only affecting document generation.
     %% is for now only affecting document generation.

+ 0 - 5
apps/emqx_authz/etc/emqx_authz.conf

@@ -1,9 +1,4 @@
 authorization {
 authorization {
-  cache: {
-    enable: true
-    max_size: 32
-    ttl: "60s"
-  }
   deny_action: ignore
   deny_action: ignore
   no_match: allow
   no_match: allow
   sources: [
   sources: [

+ 13 - 1
apps/emqx_conf/src/emqx_conf.erl

@@ -144,7 +144,8 @@ dump_schema(Dir, SchemaModule, I18nFile) ->
     lists:foreach(
     lists:foreach(
         fun(Lang) ->
         fun(Lang) ->
             gen_config_md(Dir, I18nFile, SchemaModule, 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,
         end,
         [en, zh]
         [en, zh]
     ),
     ),
@@ -173,6 +174,12 @@ gen_config_md(Dir, I18nFile, SchemaModule, Lang0) ->
     io:format(user, "===< Generating: ~s~n", [SchemaMdFile]),
     io:format(user, "===< Generating: ~s~n", [SchemaMdFile]),
     ok = gen_doc(SchemaMdFile, SchemaModule, I18nFile, Lang).
     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.
 %% @doc return the root schema module.
 -spec schema_module() -> module().
 -spec schema_module() -> module().
 schema_module() ->
 schema_module() ->
@@ -195,6 +202,11 @@ gen_doc(File, SchemaModule, I18nFile, Lang) ->
     Doc = hocon_schema_md:gen(SchemaModule, Opts),
     Doc = hocon_schema_md:gen(SchemaModule, Opts),
     file:write_file(File, Doc).
     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) ->
 check_cluster_rpc_result(Result) ->
     case Result of
     case Result of
         {ok, _TnxId, Res} ->
         {ok, _TnxId, Res} ->

+ 17 - 19
apps/emqx_conf/src/emqx_conf_schema.erl

@@ -76,22 +76,22 @@ roots() ->
         [
         [
             {"node",
             {"node",
                 sc(
                 sc(
-                    ref("node"),
+                    ?R_REF("node"),
                     #{translate_to => ["emqx"]}
                     #{translate_to => ["emqx"]}
                 )},
                 )},
             {"cluster",
             {"cluster",
                 sc(
                 sc(
-                    ref("cluster"),
+                    ?R_REF("cluster"),
                     #{translate_to => ["ekka"]}
                     #{translate_to => ["ekka"]}
                 )},
                 )},
             {"log",
             {"log",
                 sc(
                 sc(
-                    ref("log"),
+                    ?R_REF("log"),
                     #{translate_to => ["kernel"]}
                     #{translate_to => ["kernel"]}
                 )},
                 )},
             {"rpc",
             {"rpc",
                 sc(
                 sc(
-                    ref("rpc"),
+                    ?R_REF("rpc"),
                     #{translate_to => ["gen_rpc"]}
                     #{translate_to => ["gen_rpc"]}
                 )}
                 )}
         ] ++
         ] ++
@@ -166,27 +166,27 @@ fields("cluster") ->
             )},
             )},
         {"static",
         {"static",
             sc(
             sc(
-                ref(cluster_static),
+                ?R_REF(cluster_static),
                 #{}
                 #{}
             )},
             )},
         {"mcast",
         {"mcast",
             sc(
             sc(
-                ref(cluster_mcast),
+                ?R_REF(cluster_mcast),
                 #{}
                 #{}
             )},
             )},
         {"dns",
         {"dns",
             sc(
             sc(
-                ref(cluster_dns),
+                ?R_REF(cluster_dns),
                 #{}
                 #{}
             )},
             )},
         {"etcd",
         {"etcd",
             sc(
             sc(
-                ref(cluster_etcd),
+                ?R_REF(cluster_etcd),
                 #{}
                 #{}
             )},
             )},
         {"k8s",
         {"k8s",
             sc(
             sc(
-                ref(cluster_k8s),
+                ?R_REF(cluster_k8s),
                 #{}
                 #{}
             )}
             )}
     ];
     ];
@@ -328,7 +328,7 @@ fields(cluster_etcd) ->
             )},
             )},
         {"ssl",
         {"ssl",
             sc(
             sc(
-                hoconsc:ref(emqx_schema, "ssl_client_opts"),
+                ?R_REF(emqx_schema, "ssl_client_opts"),
                 #{
                 #{
                     desc => ?DESC(cluster_etcd_ssl),
                     desc => ?DESC(cluster_etcd_ssl),
                     'readOnly' => true
                     'readOnly' => true
@@ -512,7 +512,7 @@ fields("node") ->
             )},
             )},
         {"cluster_call",
         {"cluster_call",
             sc(
             sc(
-                ref("cluster_call"),
+                ?R_REF("cluster_call"),
                 #{'readOnly' => true}
                 #{'readOnly' => true}
             )},
             )},
         {"db_backend",
         {"db_backend",
@@ -783,10 +783,10 @@ fields("rpc") ->
     ];
     ];
 fields("log") ->
 fields("log") ->
     [
     [
-        {"console_handler", ref("console_handler")},
+        {"console_handler", ?R_REF("console_handler")},
         {"file_handlers",
         {"file_handlers",
             sc(
             sc(
-                map(name, ref("log_file_handler")),
+                map(name, ?R_REF("log_file_handler")),
                 #{desc => ?DESC("log_file_handlers")}
                 #{desc => ?DESC("log_file_handlers")}
             )},
             )},
         {"error_logger",
         {"error_logger",
@@ -814,7 +814,7 @@ fields("log_file_handler") ->
             )},
             )},
         {"rotation",
         {"rotation",
             sc(
             sc(
-                ref("log_rotation"),
+                ?R_REF("log_rotation"),
                 #{}
                 #{}
             )},
             )},
         {"max_size",
         {"max_size",
@@ -1137,8 +1137,8 @@ log_handler_common_confs(Enable) ->
                     desc => ?DESC("common_handler_flush_qlen")
                     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",
         {"supervisor_reports",
             sc(
             sc(
                 hoconsc:enum([error, progress]),
                 hoconsc:enum([error, progress]),
@@ -1251,8 +1251,6 @@ sc(Type, Meta) -> hoconsc:mk(Type, Meta).
 
 
 map(Name, Type) -> hoconsc:map(Name, Type).
 map(Name, Type) -> hoconsc:map(Name, Type).
 
 
-ref(Field) -> hoconsc:ref(?MODULE, Field).
-
 options(static, Conf) ->
 options(static, Conf) ->
     [{seeds, conf_get("cluster.static.seeds", Conf, [])}];
     [{seeds, conf_get("cluster.static.seeds", Conf, [])}];
 options(mcast, Conf) ->
 options(mcast, Conf) ->
@@ -1321,7 +1319,7 @@ emqx_schema_high_prio_roots() ->
     Authz =
     Authz =
         {"authorization",
         {"authorization",
             sc(
             sc(
-                hoconsc:ref(?MODULE, "authorization"),
+                ?R_REF("authorization"),
                 #{desc => ?DESC(authorization)}
                 #{desc => ?DESC(authorization)}
             )},
             )},
     lists:keyreplace("authorization", 1, Roots, Authz).
     lists:keyreplace("authorization", 1, Roots, Authz).

+ 539 - 0
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}.

+ 1 - 1
apps/emqx_connector/src/emqx_connector_schema.erl

@@ -64,7 +64,7 @@ fields("connectors") ->
             mk(
             mk(
                 hoconsc:map(
                 hoconsc:map(
                     name,
                     name,
-                    hoconsc:union([ref(emqx_connector_mqtt_schema, "connector")])
+                    ref(emqx_connector_mqtt_schema, "connector")
                 ),
                 ),
                 #{desc => ?DESC("mqtt")}
                 #{desc => ?DESC("mqtt")}
             )}
             )}

+ 1 - 0
apps/emqx_dashboard/src/emqx_dashboard_schema.erl

@@ -171,6 +171,7 @@ bind(Port) ->
             #{
             #{
                 default => Port,
                 default => Port,
                 required => true,
                 required => true,
+                extra => #{example => [Port, "0.0.0.0:" ++ integer_to_list(Port)]},
                 desc => ?DESC(bind)
                 desc => ?DESC(bind)
             }
             }
         )}.
         )}.

+ 3 - 2
apps/emqx_gateway/src/emqx_gateway_schema.erl

@@ -567,7 +567,8 @@ authentication_schema() ->
         emqx_authn_schema:authenticator_type(),
         emqx_authn_schema:authenticator_type(),
         #{
         #{
             required => {false, recursively},
             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(<<"">>).
 mountpoint(Default) ->
 mountpoint(Default) ->
     sc(
     sc(
         binary(),
         binary(),

+ 1 - 4
apps/emqx_retainer/src/emqx_retainer_schema.erl

@@ -109,10 +109,7 @@ sc(Type, DescId, Default) ->
     hoconsc:mk(Type, #{default => Default, desc => ?DESC(DescId)}).
     hoconsc:mk(Type, #{default => Default, desc => ?DESC(DescId)}).
 
 
 backend_config() ->
 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) ->
 retainer_indices(type) ->
     list(list(integer()));
     list(list(integer()));

+ 1 - 1
apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl

@@ -186,7 +186,7 @@ rule_name() ->
             binary(),
             binary(),
             #{
             #{
                 desc => ?DESC("rules_name"),
                 desc => ?DESC("rules_name"),
-                default => "",
+                default => <<"">>,
                 required => false,
                 required => false,
                 example => "foo"
                 example => "foo"
             }
             }

+ 1 - 1
apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl

@@ -32,7 +32,7 @@ fields("slow_subs") ->
             )},
             )},
         {stats_type,
         {stats_type,
             sc(
             sc(
-                hoconsc:union([whole, internal, response]),
+                hoconsc:enum([whole, internal, response]),
                 whole,
                 whole,
                 stats_type
                 stats_type
             )}
             )}