فهرست منبع

feat(schema): provide type-level documentation snippets

For stuff like `duration()`, `bytesize()` and `secret()` for now.
Andrew Mayorov 2 سال پیش
والد
کامیت
85d7518a7d

+ 29 - 12
apps/emqx_conf/src/emqx_conf.erl

@@ -28,7 +28,7 @@
 -export([remove/2, remove/3]).
 -export([remove/2, remove/3]).
 -export([tombstone/2]).
 -export([tombstone/2]).
 -export([reset/2, reset/3]).
 -export([reset/2, reset/3]).
--export([dump_schema/2, reformat_schema_dump/1]).
+-export([dump_schema/2, reformat_schema_dump/2]).
 -export([schema_module/0]).
 -export([schema_module/0]).
 
 
 %% TODO: move to emqx_dashboard when we stop building api schema at build time
 %% TODO: move to emqx_dashboard when we stop building api schema at build time
@@ -186,7 +186,7 @@ gen_schema_json(Dir, SchemaModule, Lang) ->
     ok = gen_preformat_md_json_files(Dir, StructsJsonArray, Lang).
     ok = gen_preformat_md_json_files(Dir, StructsJsonArray, Lang).
 
 
 gen_preformat_md_json_files(Dir, StructsJsonArray, Lang) ->
 gen_preformat_md_json_files(Dir, StructsJsonArray, Lang) ->
-    NestedStruct = reformat_schema_dump(StructsJsonArray),
+    NestedStruct = reformat_schema_dump(StructsJsonArray, Lang),
     %% write to files
     %% write to files
     NestedJsonFile = filename:join([Dir, "schema-v2-" ++ Lang ++ ".json"]),
     NestedJsonFile = filename:join([Dir, "schema-v2-" ++ Lang ++ ".json"]),
     io:format(user, "===< Generating: ~s~n", [NestedJsonFile]),
     io:format(user, "===< Generating: ~s~n", [NestedJsonFile]),
@@ -196,15 +196,17 @@ gen_preformat_md_json_files(Dir, StructsJsonArray, Lang) ->
     ok.
     ok.
 
 
 %% @doc This function is exported for scripts/schema-dump-reformat.escript
 %% @doc This function is exported for scripts/schema-dump-reformat.escript
-reformat_schema_dump(StructsJsonArray0) ->
+reformat_schema_dump(StructsJsonArray0, Lang) ->
     %% prepare
     %% prepare
+    DescResolver = make_desc_resolver(Lang),
     StructsJsonArray = deduplicate_by_full_name(StructsJsonArray0),
     StructsJsonArray = deduplicate_by_full_name(StructsJsonArray0),
     #{fields := RootFields} = hd(StructsJsonArray),
     #{fields := RootFields} = hd(StructsJsonArray),
     RootNames0 = lists:map(fun(#{name := RootName}) -> RootName end, RootFields),
     RootNames0 = lists:map(fun(#{name := RootName}) -> RootName end, RootFields),
     RootNames = lists:map(fun to_bin/1, RootNames0),
     RootNames = lists:map(fun to_bin/1, RootNames0),
     %% reformat
     %% reformat
     [Root | FlatStructs0] = lists:map(
     [Root | FlatStructs0] = lists:map(
-        fun(Struct) -> gen_flat_doc(RootNames, Struct) end, StructsJsonArray
+        fun(Struct) -> gen_flat_doc(RootNames, Struct, DescResolver) end,
+        StructsJsonArray
     ),
     ),
     FlatStructs = [Root#{text => <<"root">>, hash => <<"root">>} | FlatStructs0],
     FlatStructs = [Root#{text => <<"root">>, hash => <<"root">>} | FlatStructs0],
     gen_nested_doc(FlatStructs).
     gen_nested_doc(FlatStructs).
@@ -302,7 +304,7 @@ expand_ref(#{hash := FullName}, FindFn, Path) ->
 
 
 %% generate flat docs for each struct.
 %% generate flat docs for each struct.
 %% using references to link to other structs.
 %% using references to link to other structs.
-gen_flat_doc(RootNames, #{full_name := FullName, fields := Fields} = S) ->
+gen_flat_doc(RootNames, #{full_name := FullName, fields := Fields} = S, DescResolver) ->
     ShortName = short_name(FullName),
     ShortName = short_name(FullName),
     case is_missing_namespace(ShortName, to_bin(FullName), RootNames) of
     case is_missing_namespace(ShortName, to_bin(FullName), RootNames) of
         true ->
         true ->
@@ -314,18 +316,17 @@ gen_flat_doc(RootNames, #{full_name := FullName, fields := Fields} = S) ->
         text => short_name(FullName),
         text => short_name(FullName),
         hash => format_hash(FullName),
         hash => format_hash(FullName),
         doc => maps:get(desc, S, <<"">>),
         doc => maps:get(desc, S, <<"">>),
-        fields => format_fields(Fields)
+        fields => format_fields(Fields, DescResolver)
     }.
     }.
 
 
-format_fields([]) ->
-    [];
-format_fields([Field | Fields]) ->
-    [format_field(Field) | format_fields(Fields)].
+format_fields(Fields, DescResolver) ->
+    [format_field(F, DescResolver) || F <- Fields].
 
 
-format_field(#{name := Name, aliases := Aliases, type := Type} = F) ->
+format_field(#{name := Name, aliases := Aliases, type := Type} = F, DescResolver) ->
     L = [
     L = [
         {text, Name},
         {text, Name},
         {type, format_type(Type)},
         {type, format_type(Type)},
+        {typedoc, format_type_desc(Type, DescResolver)},
         {refs, format_refs(Type)},
         {refs, format_refs(Type)},
         {aliases,
         {aliases,
             case Aliases of
             case Aliases of
@@ -393,10 +394,26 @@ format_union_members([Member | Members], Acc) ->
     NewAcc = [format_type(Member) | Acc],
     NewAcc = [format_type(Member) | Acc],
     format_union_members(Members, NewAcc).
     format_union_members(Members, NewAcc).
 
 
+format_type_desc(#{kind := primitive, name := Name}, DescResolver) ->
+    format_primitive_type_desc(Name, DescResolver);
+format_type_desc(#{}, _DescResolver) ->
+    undefined.
+
 format_primitive_type(TypeStr) ->
 format_primitive_type(TypeStr) ->
-    Spec = emqx_conf_schema_types:readable_docgen(?MODULE, TypeStr),
+    Spec = get_primitive_typespec(TypeStr),
     to_bin(maps:get(type, Spec)).
     to_bin(maps:get(type, Spec)).
 
 
+format_primitive_type_desc(TypeStr, DescResolver) ->
+    case get_primitive_typespec(TypeStr) of
+        #{desc := Desc} ->
+            DescResolver(Desc);
+        #{} ->
+            undefined
+    end.
+
+get_primitive_typespec(TypeStr) ->
+    emqx_conf_schema_types:readable_docgen(?MODULE, TypeStr).
+
 %% All types should have a namespace to avlid name clashing.
 %% All types should have a namespace to avlid name clashing.
 is_missing_namespace(ShortName, FullName, RootNames) ->
 is_missing_namespace(ShortName, FullName, RootNames) ->
     case lists:member(ShortName, RootNames) of
     case lists:member(ShortName, RootNames) of

+ 15 - 9
apps/emqx_conf/src/emqx_conf_schema_types.erl

@@ -16,6 +16,8 @@
 
 
 -module(emqx_conf_schema_types).
 -module(emqx_conf_schema_types).
 
 
+-include_lib("hocon/include/hocon_types.hrl").
+
 -export([readable/2]).
 -export([readable/2]).
 -export([readable_swagger/2, readable_dashboard/2, readable_docgen/2]).
 -export([readable_swagger/2, readable_dashboard/2, readable_docgen/2]).
 
 
@@ -165,37 +167,37 @@ readable("duration()") ->
     #{
     #{
         swagger => #{type => string, example => <<"12m">>},
         swagger => #{type => string, example => <<"12m">>},
         dashboard => #{type => duration},
         dashboard => #{type => duration},
-        docgen => #{type => "String", example => <<"12m">>}
+        docgen => #{type => "Duration", example => <<"12m">>, desc => ?DESC(duration)}
     };
     };
 readable("duration_s()") ->
 readable("duration_s()") ->
     #{
     #{
         swagger => #{type => string, example => <<"1h">>},
         swagger => #{type => string, example => <<"1h">>},
         dashboard => #{type => duration},
         dashboard => #{type => duration},
-        docgen => #{type => "String", example => <<"1h">>}
+        docgen => #{type => "Duration(s)", example => <<"1h">>, desc => ?DESC(duration)}
     };
     };
 readable("duration_ms()") ->
 readable("duration_ms()") ->
     #{
     #{
         swagger => #{type => string, example => <<"32s">>},
         swagger => #{type => string, example => <<"32s">>},
         dashboard => #{type => duration},
         dashboard => #{type => duration},
-        docgen => #{type => "String", example => <<"32s">>}
+        docgen => #{type => "Duration", example => <<"32s">>, desc => ?DESC(duration)}
     };
     };
 readable("timeout_duration()") ->
 readable("timeout_duration()") ->
     #{
     #{
         swagger => #{type => string, example => <<"12m">>},
         swagger => #{type => string, example => <<"12m">>},
         dashboard => #{type => duration},
         dashboard => #{type => duration},
-        docgen => #{type => "String", example => <<"12m">>}
+        docgen => #{type => "Duration", example => <<"12m">>, desc => ?DESC(duration)}
     };
     };
 readable("timeout_duration_s()") ->
 readable("timeout_duration_s()") ->
     #{
     #{
         swagger => #{type => string, example => <<"1h">>},
         swagger => #{type => string, example => <<"1h">>},
         dashboard => #{type => duration},
         dashboard => #{type => duration},
-        docgen => #{type => "String", example => <<"1h">>}
+        docgen => #{type => "Duration(s)", example => <<"1h">>, desc => ?DESC(duration)}
     };
     };
 readable("timeout_duration_ms()") ->
 readable("timeout_duration_ms()") ->
     #{
     #{
         swagger => #{type => string, example => <<"32s">>},
         swagger => #{type => string, example => <<"32s">>},
         dashboard => #{type => duration},
         dashboard => #{type => duration},
-        docgen => #{type => "String", example => <<"32s">>}
+        docgen => #{type => "Duration", example => <<"32s">>, desc => ?DESC(duration)}
     };
     };
 readable("percent()") ->
 readable("percent()") ->
     #{
     #{
@@ -219,13 +221,13 @@ readable("bytesize()") ->
     #{
     #{
         swagger => #{type => string, example => <<"32MB">>},
         swagger => #{type => string, example => <<"32MB">>},
         dashboard => #{type => 'byteSize'},
         dashboard => #{type => 'byteSize'},
-        docgen => #{type => "String", example => <<"32MB">>}
+        docgen => #{type => "Bytesize", example => <<"32MB">>, desc => ?DESC(bytesize)}
     };
     };
 readable("wordsize()") ->
 readable("wordsize()") ->
     #{
     #{
         swagger => #{type => string, example => <<"1024KB">>},
         swagger => #{type => string, example => <<"1024KB">>},
         dashboard => #{type => 'wordSize'},
         dashboard => #{type => 'wordSize'},
-        docgen => #{type => "String", example => <<"1024KB">>}
+        docgen => #{type => "Bytesize", example => <<"1024KB">>, desc => ?DESC(bytesize)}
     };
     };
 readable("map(" ++ Map) ->
 readable("map(" ++ Map) ->
     [$) | _MapArgs] = lists:reverse(Map),
     [$) | _MapArgs] = lists:reverse(Map),
@@ -287,7 +289,11 @@ readable("secret()") ->
     #{
     #{
         swagger => #{type => string, example => <<"R4ND0M/S∃CЯ∃T"/utf8>>},
         swagger => #{type => string, example => <<"R4ND0M/S∃CЯ∃T"/utf8>>},
         dashboard => #{type => string},
         dashboard => #{type => string},
-        docgen => #{type => "String", example => <<"R4ND0M/S∃CЯ∃T"/utf8>>}
+        docgen => #{
+            type => "Secret",
+            example => <<"R4ND0M/S∃CЯ∃T"/utf8>>,
+            desc => ?DESC(secret)
+        }
     };
     };
 readable(TypeStr0) ->
 readable(TypeStr0) ->
     case string:split(TypeStr0, ":") of
     case string:split(TypeStr0, ":") of

+ 12 - 0
rel/i18n/emqx_conf_schema_types.hocon

@@ -0,0 +1,12 @@
+emqx_conf_schema_types {
+
+    duration.desc:
+    """A string that represents a time duration, for example: <code>10s</code>, <code>2.5m</code>, <code>1h30m</code>, <code>1W2D</code>, or <code>2345ms</code>, which is the smallest unit. When precision is specified, finer portions of the duration may be ignored: writing <code>1200ms</code> for <code>Duration(s)</code> is equivalent to writing <code>1s</code>. It doesn't matter if units are in upper or lower case."""
+
+    bytesize.desc:
+    """A string that represents a number of bytes, for example: <code>10B</code>, <code>640kb</code>, <code>4MB</code>, <code>1GB</code>. Units are interpreted as powers of 1024, and the unit part is case-insensitive."""
+
+    secret.desc:
+    """A string holding some sensitive information, such as a password. When secret starts with <code>file://</code>, the rest of the string is interpreted as a path to a file containing the secret itself: whole content of the file except any trailing whitespace characters is considered a secret value."""
+
+}

+ 2 - 1
scripts/schema-dump-reformat.escript

@@ -18,7 +18,8 @@ main(_) ->
     halt(1).
     halt(1).
 
 
 reformat(Json) ->
 reformat(Json) ->
-    emqx_conf:reformat_schema_dump(fix(Json)).
+    %% NOTE: Assuming schema would contain no types needing localized typedocs.
+    emqx_conf:reformat_schema_dump(fix(Json), _Lang = "en").
 
 
 %% fix old type specs to make them compatible with new type specs
 %% fix old type specs to make them compatible with new type specs
 fix(#{
 fix(#{