diff --git a/src/hocon_schema.erl b/src/hocon_schema.erl index 70769d8..15d9968 100644 --- a/src/hocon_schema.erl +++ b/src/hocon_schema.erl @@ -28,12 +28,14 @@ -export([ find_structs/1, + find_structs/2, override/2, namespace/1, resolve_struct_name/2, root_names/1, field_schema/2, - assert_fields/2 + assert_fields/2, + path/1 ]). -export([ @@ -100,6 +102,7 @@ #{ type := type(), default => term(), + examples => term(), mapping => undefined | string(), converter => undefined | translationfunc(), validator => undefined | validationfun(), diff --git a/src/hocon_schema_example.erl b/src/hocon_schema_example.erl new file mode 100644 index 0000000..189be67 --- /dev/null +++ b/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/src/hocon_schema_json.erl b/src/hocon_schema_json.erl index 0e6f750..63cad89 100644 --- a/src/hocon_schema_json.erl +++ b/src/hocon_schema_json.erl @@ -59,12 +59,7 @@ gen(Schema, Opts) -> hocon_schema:delete_desc_cache(Cache), Json. -gen_struct(RootNs, Ns0, Name, #{fields := Fields} = Meta, Opts) -> - Ns = - case RootNs =:= Ns0 of - true -> undefined; - false -> Ns0 - end, +gen_struct(_RootNs, Ns, Name, #{fields := Fields} = Meta, Opts) -> Paths = case Meta of #{paths := Ps} -> lists:sort(maps:keys(Ps)); @@ -97,6 +92,7 @@ fmt_field(Ns, Name, FieldSchema, Opts) -> {name, bin(Name)}, {type, fmt_type(Ns, hocon_schema:field_schema(FieldSchema, type))}, {default, fmt_default(hocon_schema:field_schema(FieldSchema, default))}, + {examples, hocon_schema:field_schema(FieldSchema, examples)}, {desc, fmt_desc(hocon_schema:field_schema(FieldSchema, desc), Opts)}, {extra, hocon_schema:field_schema(FieldSchema, extra)}, {mapping, bin(hocon_schema:field_schema(FieldSchema, mapping))} diff --git a/test/hocon_schema_example_tests.erl b/test/hocon_schema_example_tests.erl new file mode 100644 index 0000000..cb62cf1 --- /dev/null +++ b/test/hocon_schema_example_tests.erl @@ -0,0 +1,230 @@ +%%-------------------------------------------------------------------- +%% 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_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-include_lib("hocon/include/hoconsc.hrl"). + +-export([ + roots/0, + fields/1, + namespace/0 +]). + +namespace() -> example. +roots() -> ["example"]. + +fields("example") -> + Element = #{ + <<"test">> => 999, + <<"test1">> => + #{ + <<"http">> => + #{ + <<"backlog">> => 1023, + <<"enable">> => true, + <<"inet6">> => true, + <<"ipv6_v6only">> => true, + <<"send_timeout">> => <<"10s">> + } + } + }, + MapExample = #{ + <<"good">> => Element + }, + Union = hoconsc:union([?R_REF(emqx_schema, "mcast"), ?R_REF(emqx_schema, "etcd")]), + UnionExample = #{test => #{value => #{addr => <<"127.0.0.1">>, ttl => 100}}}, + [ + {key1, + ?HOCON( + ?R_REF("base"), + #{desc => <<"base test">>} + )}, + {map_key1, ?HOCON(hoconsc:map(map_name1, ?REF("map_name")), #{example1s => MapExample})}, + {map_key2, ?HOCON(hoconsc:map(map_name2, ?REF("map_name")), #{})}, + {union_key, ?HOCON(Union, #{examples => UnionExample})}, + {union_key2, Union}, + {array_key, hoconsc:array(?R_REF(emqx_schema, "mcast"))}, + {array_key2, hoconsc:array(?R_REF(emqx_schema, "etcd"))}, + {array_key3, hoconsc:array(integer())}, + {array_key4, hoconsc:array(integer())}, + {array_key5, ?HOCON(hoconsc:array(?R_REF("map_name")), #{examples => [Element]})} + ]; +fields("base") -> + [ + {"http", + ?HOCON( + ?R_REF("http"), + #{ + desc => "TCP listeners", + required => {false, recursively} + } + )}, + {"https", + ?HOCON( + ?R_REF("https"), + #{ + desc => "SSL listeners", + required => {false, recursively} + } + )} + ]; +fields("http") -> + [ + enable(true), + bind(18803) + | common_listener_fields() + ]; +fields("https") -> + [ + enable(false), + bind(18804) + | common_listener_fields() + ]; +fields("map_name") -> + [ + {test, ?HOCON(integer(), #{})}, + {test1, ?HOCON(?R_REF("base"), #{})} + ]. + +common_listener_fields() -> + [ + {"num_acceptors", + ?HOCON( + integer(), + #{ + default => 4, + desc => <<"num_acceptors">> + } + )}, + {"max_connections", + ?HOCON( + integer(), + #{ + default => 512, + desc => <<"max_connections">> + } + )}, + {"backlog", + ?HOCON( + integer(), + #{ + default => 1024, + desc => <<"backlog">> + } + )}, + {"send_timeout", + ?HOCON( + emqx_schema:duration(), + #{ + default => "5s", + desc => <<"send_timeout">> + } + )}, + {"inet6", + ?HOCON( + boolean(), + #{ + default => false, + desc => <<"inet6">> + } + )}, + {"ipv6_v6only", + ?HOCON( + boolean(), + #{ + default => false, + desc => <<"ipv6_v6only">> + } + )} + ]. + +enable(Bool) -> + {"enable", + ?HOCON( + boolean(), + #{ + default => Bool, + required => true, + desc => <<"listener_enable">> + } + )}. + +bind(Port) -> + {"bind", + ?HOCON( + hoconsc:union([non_neg_integer(), emqx_schema:ip_port()]), + #{ + default => Port, + required => true, + extra => #{example => [Port, "0.0.0.0:" ++ integer_to_list(Port)]}, + desc => <<"bind">> + } + )}. + +no_crash_test_() -> + [ + {"example", gen(?MODULE)}, + {"demo_schema", gen(demo_schema, "./test/data/demo_schema_doc.conf")}, + {"demo_schema2", gen(demo_schema2)}, + {"demo_schema3", gen(demo_schema3)}, + {"emqx_schema", gen(emqx_schema)}, + {"arbitrary1", + gen(#{ + namespace => dummy, + roots => [foo], + fields => #{foo => [{"f1", hoconsc:enum([bar])}]} + })}, + {"arbitrary2", + gen(#{ + namespace => dummy, + roots => [foo], + fields => #{foo => [{"f1", hoconsc:mk(hoconsc:ref(emqx_schema, "zone"))}]} + })}, + {"multi-line-default", + gen(#{ + namespace => "rootns", + roots => [foo], + fields => #{ + foo => [ + {"f1", + hoconsc:mk( + hoconsc:ref(emqx_schema, "etcd"), + #{ + default => #{ + <<"server">> => <<"localhost">>, + <<"prefix">> => <<"prefix">>, + <<"node_ttl">> => "100s", + <<"ssl">> => <<>> + } + } + )} + ] + } + })} + ]. + +gen(Schema) -> fun() -> hocon_schema_example:gen(Schema, "test") end. +gen(Schema, DescFile) -> + fun() -> + hocon_schema_example:gen( + Schema, + #{title => "test", body => <<>>, desc_file => DescFile} + ) + end.