diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 47ca7e7d0..cf6cc7a16 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -19,7 +19,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.1"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.24.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.25.0"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}} diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index ebf0baf53..5dedfd285 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -295,7 +295,7 @@ include_dirs() -> [filename:join(emqx:data_dir(), "configs")]. merge_envs(SchemaMod, RawConf) -> - Opts = #{nullable => true, %% TODO: evil, remove, nullable should be declared in schema + Opts = #{required => false, %% TODO: evil, remove, required should be declared in schema format => map, apply_override_envs => true }, @@ -308,7 +308,7 @@ check_config(SchemaMod, RawConf) -> check_config(SchemaMod, RawConf, Opts0) -> Opts1 = #{return_plain => true, - nullable => true, %% TODO: evil, remove, nullable should be declared in schema + required => false, %% TODO: evil, remove, required should be declared in schema format => map }, Opts = maps:merge(Opts0, Opts1), @@ -331,7 +331,7 @@ fill_defaults(RawConf) -> -spec fill_defaults(module(), raw_config()) -> map(). fill_defaults(SchemaMod, RawConf) -> hocon_tconf:check_plain(SchemaMod, RawConf, - #{nullable => true, only_fill_defaults => true}, + #{required => false, only_fill_defaults => true}, root_names_from_conf(RawConf)). diff --git a/apps/emqx/src/emqx_hocon.erl b/apps/emqx/src/emqx_hocon.erl index ebf5e0f44..9d9da03de 100644 --- a/apps/emqx/src/emqx_hocon.erl +++ b/apps/emqx/src/emqx_hocon.erl @@ -34,9 +34,9 @@ format_path([Name | Rest]) -> -spec check(module(), hocon:config() | iodata()) -> {ok, hocon:config()} | {error, any()}. check(SchemaModule, Conf) when is_map(Conf) -> - %% TODO: remove nullable - %% fields should state nullable or not in their schema - Opts = #{atom_key => true, nullable => true}, + %% TODO: remove required + %% fields should state required or not in their schema + Opts = #{atom_key => true, required => false}, try {ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)} catch diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 39e5c96fe..a92c3da67 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -586,31 +586,31 @@ fields("listeners") -> [ {"tcp", sc(map(name, ref("mqtt_tcp_listener")), #{ desc => "TCP listeners" - , nullable => {true, recursively} + , required => {false, recursively} }) } , {"ssl", sc(map(name, ref("mqtt_ssl_listener")), #{ desc => "SSL listeners" - , nullable => {true, recursively} + , required => {false, recursively} }) } , {"ws", sc(map(name, ref("mqtt_ws_listener")), #{ desc => "HTTP websocket listeners" - , nullable => {true, recursively} + , required => {false, recursively} }) } , {"wss", sc(map(name, ref("mqtt_wss_listener")), #{ desc => "HTTPS websocket listeners" - , nullable => {true, recursively} + , required => {false, recursively} }) } , {"quic", sc(map(name, ref("mqtt_quic_listener")), #{ desc => "QUIC listeners" - , nullable => {true, recursively} + , required => {false, recursively} }) } ]; @@ -1129,7 +1129,7 @@ mqtt_listener() -> base_listener() -> [ {"bind", sc(hoconsc:union([ip_port(), integer()]), - #{ nullable => false + #{ required => true })} , {"acceptors", sc(integer(), @@ -1188,7 +1188,7 @@ common_ssl_opts_schema(Defaults) -> , {"cacertfile", sc(string(), #{ default => D("cacertfile") - , nullable => true + , required => false , desc => """Trusted PEM format CA certificates bundle file.
The certificates in this file are used to verify the TLS peer's certificates. @@ -1203,7 +1203,7 @@ already established connections. , {"certfile", sc(string(), #{ default => D("certfile") - , nullable => true + , required => false , desc => """PEM format certificates chain file.
The certificates in this file should be in reversed order of the certificate @@ -1217,7 +1217,7 @@ the file if it is to be added. , {"keyfile", sc(string(), #{ default => D("keyfile") - , nullable => true + , required => false , desc => """PEM format private key file.
""" @@ -1241,7 +1241,7 @@ the file if it is to be added. , {"password", sc(string(), #{ sensitive => true - , nullable => true + , required => false , desc => """String containing the user's password. Only used if the private key file is password-protected.""" @@ -1289,7 +1289,7 @@ server_ssl_opts_schema(Defaults, IsRanchListener) -> [ {"dhfile", sc(string(), #{ default => D("dhfile") - , nullable => true + , required => false , desc => """Path to a file containing PEM-encoded Diffie Hellman parameters to be used by the server if a cipher suite using Diffie Hellman @@ -1345,7 +1345,7 @@ client_ssl_opts_schema(Defaults) -> common_ssl_opts_schema(Defaults) ++ [ { "server_name_indication", sc(hoconsc:union([disable, string()]), - #{ nullable => true + #{ required => false , desc => """Specify the host name to be used in TLS Server Name Indication extension.
For instance, when connecting to \"server.example.net\", the genuine server diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 3219640d4..af4c3da2d 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -116,7 +116,7 @@ fields(request_user_create) -> fields(request_user_update) -> [ {password, binary()}, - {is_superuser, mk(boolean(), #{default => false, nullable => true})} + {is_superuser, mk(boolean(), #{default => false, required => false})} ]; fields(request_move) -> @@ -128,7 +128,7 @@ fields(request_import_users) -> fields(response_user) -> [ {user_id, binary()}, - {is_superuser, mk(boolean(), #{default => false, nullable => true})} + {is_superuser, mk(boolean(), #{default => false, required => false})} ]; fields(response_users) -> @@ -379,8 +379,8 @@ schema("/authentication/:id/users") -> description => <<"List users in authenticator in global authentication chain">>, parameters => [ param_auth_id(), - {page, mk(integer(), #{in => query, desc => <<"Page Index">>, nullable => true})}, - {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, nullable => true})} + {page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})}, + {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})} ], responses => #{ 200 => emqx_dashboard_swagger:schema_with_example( @@ -415,8 +415,8 @@ schema("/listeners/:listener_id/authentication/:id/users") -> description => <<"List users in authenticator in listener authentication chain">>, parameters => [ param_listener_id(), param_auth_id(), - {page, mk(integer(), #{in => query, desc => <<"Page Index">>, nullable => true})}, - {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, nullable => true})} + {page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})}, + {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})} ], responses => #{ 200 => emqx_dashboard_swagger:schema_with_example( diff --git a/apps/emqx_authn/src/emqx_authn_password_hashing.erl b/apps/emqx_authn/src/emqx_authn_password_hashing.erl index 731001498..7c9b0f247 100644 --- a/apps/emqx_authn/src/emqx_authn_password_hashing.erl +++ b/apps/emqx_authn/src/emqx_authn_password_hashing.erl @@ -81,8 +81,7 @@ salt_rounds(default) -> 10; salt_rounds(_) -> undefined. dk_length(type) -> integer(); -dk_length(nullable) -> true; -dk_length(default) -> undefined; +dk_length(required) -> false; dk_length(_) -> undefined. type_rw(type) -> diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 46c410b25..7a30e0b72 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -54,8 +54,8 @@ root_type() -> mechanism(Name) -> hoconsc:mk(hoconsc:enum([Name]), - #{nullable => false}). + #{required => true}). backend(Name) -> hoconsc:mk(hoconsc:enum([Name]), - #{nullable => false}). + #{required => true}). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index f8af51886..002723b75 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -77,7 +77,7 @@ validations() -> url(type) -> binary(); url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")]; -url(nullable) -> false; +url(required) -> true; url(_) -> undefined. headers(type) -> map(); diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index aabb39e82..683429908 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -76,11 +76,11 @@ password_hash_field(type) -> binary(); password_hash_field(_) -> undefined. salt_field(type) -> binary(); -salt_field(nullable) -> true; +salt_field(required) -> false; salt_field(_) -> undefined. is_superuser_field(type) -> binary(); -is_superuser_field(nullable) -> true; +is_superuser_field(required) -> false; is_superuser_field(_) -> undefined. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 9f21437d4..f74792f42 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -85,7 +85,7 @@ fields(position) -> url(type) -> binary(); url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")]; -url(nullable) -> false; +url(required) -> true; url(_) -> undefined. headers(type) -> map(); diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 0d8e80039..a44361fad 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -145,7 +145,7 @@ fields(redis_cluster) -> http_common_fields() -> [ {url, fun url/1} , {request_timeout, mk_duration("Request timeout", #{default => "30s"})} - , {body, #{type => map(), nullable => true}} + , {body, #{type => map(), required => false}} ] ++ maps:to_list(maps:without([ base_url , pool_type], maps:from_list(connector_fields(http)))). @@ -181,7 +181,7 @@ headers_no_content_type(_) -> undefined. url(type) -> binary(); url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")]; -url(nullable) -> false; +url(required) -> true; url(_) -> undefined. %%-------------------------------------------------------------------- diff --git a/apps/emqx_bridge/src/emqx_bridge_http_schema.erl b/apps/emqx_bridge/src/emqx_bridge_http_schema.erl index 5bbd8016f..ccf9c0939 100644 --- a/apps/emqx_bridge/src/emqx_bridge_http_schema.erl +++ b/apps/emqx_bridge/src/emqx_bridge_http_schema.erl @@ -13,7 +13,7 @@ roots() -> []. fields("bridge") -> basic_config() ++ [ {url, mk(binary(), - #{ nullable => false + #{ required => true , desc =>""" The URL of the HTTP Bridge.
Template with variables is allowed in the path, but variables cannot be used in the scheme, host, diff --git a/apps/emqx_bridge/src/emqx_bridge_schema.erl b/apps/emqx_bridge/src/emqx_bridge_schema.erl index 19403e00c..88f3634b7 100644 --- a/apps/emqx_bridge/src/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/emqx_bridge_schema.erl @@ -52,7 +52,7 @@ common_bridge_fields() -> })} , {connector, mk(binary(), - #{ nullable => false + #{ required => true , example => <<"mqtt:my_mqtt_connector">> , desc =>""" The connector ID to be used for this bridge. Connector IDs must be of format: @@ -75,7 +75,7 @@ metrics_status_fields() -> direction_field(Dir, Desc) -> {direction, mk(Dir, - #{ nullable => false + #{ required => true , default => egress , desc => "The direction of the bridge. Can be one of 'ingress' or 'egress'.
" ++ Desc diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index daf9a48a6..5e56bd9d5 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -17,6 +17,7 @@ -compile({no_auto_import, [get/1, get/2]}). -include_lib("emqx/include/logger.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). -export([add_handler/2, remove_handler/1]). -export([get/1, get/2, get_raw/2, get_all/1]). @@ -124,14 +125,22 @@ reset(Node, KeyPath, Opts) -> %% @doc Called from build script. -spec dump_schema(file:name_all()) -> ok. dump_schema(Dir) -> + SchemaMdFile = filename:join([Dir, "config.md"]), + io:format(user, "===< Generating: ~s~n", [SchemaMdFile ]), + ok = gen_doc(SchemaMdFile), + + %% for scripts/spellcheck. SchemaJsonFile = filename:join([Dir, "schema.json"]), + io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]), JsonMap = hocon_schema_json:gen(emqx_conf_schema), IoData = jsx:encode(JsonMap, [space, {indent, 4}]), - io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]), ok = file:write_file(SchemaJsonFile, IoData), - SchemaMarkdownFile = filename:join([Dir, "config.md"]), - io:format(user, "===< Generating: ~s~n", [SchemaMarkdownFile ]), - ok = gen_doc(SchemaMarkdownFile). + + %% hot-update configuration schema + HotConfigSchemaFile = filename:join([Dir, "hot-config-schema.json"]), + io:format(user, "===< Generating: ~s~n", [HotConfigSchemaFile]), + ok = gen_hot_conf_schema(HotConfigSchemaFile), + ok. %%-------------------------------------------------------------------- %% Internal functions @@ -158,3 +167,157 @@ check_cluster_rpc_result(Result) -> {error, Error} -> %% all MFA return not ok or {ok, term()}. Error end. + +%% Only gen hot_conf schema, not all configuration fields. +gen_hot_conf_schema(File) -> + {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(emqx_mgmt_api_configs, + #{schema_converter => fun hocon_schema_to_spec/2}), + ApiSpec = lists:foldl(fun({Path, Spec, _, _}, Acc) -> + NewSpec = maps:fold(fun(Method, #{responses := Responses}, SubAcc) -> + case Responses of + #{<<"200">> := + #{<<"content">> := #{<<"application/json">> := #{<<"schema">> := Schema}}}} -> + SubAcc#{Method => Schema}; + _ -> SubAcc + end + end, #{}, Spec), + Acc#{list_to_atom(Path) => NewSpec} end, #{}, ApiSpec0), + Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0), + IoData = jsx:encode(#{ + info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>}, + paths => ApiSpec, + components => #{schemas => Components} + }, [space, {indent, 4}]), + file:write_file(File, IoData). + +-define(INIT_SCHEMA, #{fields => #{}, translations => #{}, + validations => [], namespace => undefined}). + +-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])). +-define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>, + ?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)])). + +hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) -> + {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, + [{Module, StructName}]}; +hocon_schema_to_spec(?REF(StructName), LocalModule) -> + {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, + [{LocalModule, StructName}]}; +hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) -> + {typename_to_spec(typerefl:name(Type), LocalModule), []}; +hocon_schema_to_spec(?ARRAY(Item), LocalModule) -> + {Schema, Refs} = hocon_schema_to_spec(Item, LocalModule), + {#{type => array, items => Schema}, Refs}; +hocon_schema_to_spec(?LAZY(Item), LocalModule) -> + hocon_schema_to_spec(Item, LocalModule); +hocon_schema_to_spec(?ENUM(Items), _LocalModule) -> + {#{type => enum, symbols => Items}, []}; +hocon_schema_to_spec(?MAP(Name, Type), LocalModule) -> + {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule), + {#{<<"type">> => object, + <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}}, + SubRefs}; +hocon_schema_to_spec(?UNION(Types), LocalModule) -> + {OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) -> + {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule), + {[Schema | Acc], SubRefs ++ RefsAcc} + end, {[], []}, Types), + {#{<<"oneOf">> => OneOf}, Refs}; +hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) -> + {#{type => enum, symbols => [Atom]}, []}. + +typename_to_spec("user_id_type()", _Mod) -> #{type => enum, symbols => [clientid, username]}; +typename_to_spec("term()", _Mod) -> #{type => string}; +typename_to_spec("boolean()", _Mod) -> #{type => boolean}; +typename_to_spec("binary()", _Mod) -> #{type => string}; +typename_to_spec("float()", _Mod) -> #{type => number}; +typename_to_spec("integer()", _Mod) -> #{type => number}; +typename_to_spec("non_neg_integer()", _Mod) -> #{type => number, minimum => 1}; +typename_to_spec("number()", _Mod) -> #{type => number}; +typename_to_spec("string()", _Mod) -> #{type => string}; +typename_to_spec("atom()", _Mod) -> #{type => string}; + +typename_to_spec("duration()", _Mod) -> #{type => duration}; +typename_to_spec("duration_s()", _Mod) -> #{type => duration}; +typename_to_spec("duration_ms()", _Mod) -> #{type => duration}; +typename_to_spec("percent()", _Mod) -> #{type => percent}; +typename_to_spec("file()", _Mod) -> #{type => string}; +typename_to_spec("ip_port()", _Mod) -> #{type => ip_port}; +typename_to_spec("url()", _Mod) -> #{type => url}; +typename_to_spec("bytesize()", _Mod) -> #{type => byteSize}; +typename_to_spec("wordsize()", _Mod) -> #{type => byteSize}; +typename_to_spec("qos()", _Mod) -> #{type => enum, symbols => [0, 1, 2]}; +typename_to_spec("comma_separated_list()", _Mod) -> #{type => comma_separated_string}; +typename_to_spec("comma_separated_atoms()", _Mod) -> #{type => comma_separated_string}; +typename_to_spec("pool_type()", _Mod) -> #{type => enum, symbols => [random, hash]}; +typename_to_spec("log_level()", _Mod) -> + #{type => enum, symbols => [debug, info, notice, warning, error, critical, alert, emergency, all]}; +typename_to_spec("rate()", _Mod) -> #{type => string}; +typename_to_spec("capacity()", _Mod) -> #{type => string}; +typename_to_spec("burst_rate()", _Mod) -> #{type => string}; +typename_to_spec("failure_strategy()", _Mod) -> #{type => enum, symbols => [force, drop, throw]}; +typename_to_spec("initial()", _Mod) -> #{type => string}; +typename_to_spec(Name, Mod) -> + Spec = range(Name), + Spec1 = remote_module_type(Spec, Name, Mod), + Spec2 = typerefl_array(Spec1, Name, Mod), + Spec3 = integer(Spec2, Name), + default_type(Spec3). + +default_type(nomatch) -> #{type => string}; +default_type(Type) -> Type. + +range(Name) -> + case string:split(Name, "..") of + [MinStr, MaxStr] -> %% 1..10 1..inf -inf..10 + Schema = #{type => number}, + Schema1 = add_integer_prop(Schema, minimum, MinStr), + add_integer_prop(Schema1, maximum, MaxStr); + _ -> nomatch + end. + +%% Module:Type +remote_module_type(nomatch, Name, Mod) -> + case string:split(Name, ":") of + [_Module, Type] -> typename_to_spec(Type, Mod); + _ -> nomatch + end; +remote_module_type(Spec, _Name, _Mod) -> Spec. + +%% [string()] or [integer()] or [xxx]. +typerefl_array(nomatch, Name, Mod) -> + case string:trim(Name, leading, "[") of + Name -> nomatch; + Name1 -> + case string:trim(Name1, trailing, "]") of + Name1 -> notmatch; + Name2 -> + Schema = typename_to_spec(Name2, Mod), + #{type => array, items => Schema} + end + end; +typerefl_array(Spec, _Name, _Mod) -> Spec. + +%% integer(1) +integer(nomatch, Name) -> + case string:to_integer(Name) of + {Int, []} -> #{type => enum, symbols => [Int], default => Int}; + _ -> nomatch + end; +integer(Spec, _Name) -> Spec. + +add_integer_prop(Schema, Key, Value) -> + case string:to_integer(Value) of + {error, no_integer} -> Schema; + {Int, []}when Key =:= minimum -> Schema#{Key => Int}; + {Int, []} -> Schema#{Key => Int} + end. + +to_bin(List) when is_list(List) -> + case io_lib:printable_list(List) of + true -> unicode:characters_to_binary(List); + false -> List + end; +to_bin(Boolean) when is_boolean(Boolean) -> Boolean; +to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); +to_bin(X) -> X. diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index ee18200cf..32aa7667a 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -262,7 +262,7 @@ fields("node") -> })} , {"data_dir", sc(string(), - #{ nullable => false, + #{ required => true, mapping => "emqx.data_dir", desc => "Path to the persistent data directory. It must be unique per broker instance." })} diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index ee59f75eb..a63ed78bb 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -61,7 +61,7 @@ roots() -> fields(config) -> [ {base_url, sc(url(), - #{ nullable => false + #{ required => true , validator => fun(#{query := _Query}) -> {error, "There must be no query in the base_url"}; (_) -> ok @@ -106,7 +106,7 @@ For example: http://localhost:9901/ , {request, hoconsc:mk( ref("request"), #{ default => undefined - , nullable => true + , required => false , desc => """ If the request is provided, the caller can send HTTP requests via emqx_resource:query(ResourceId, {send_message, BridgeId, Message}) @@ -115,13 +115,13 @@ If the request is provided, the caller can send HTTP requests via ] ++ emqx_connector_schema_lib:ssl_fields(); fields("request") -> - [ {method, hoconsc:mk(hoconsc:enum([post, put, get, delete]), #{nullable => true})} - , {path, hoconsc:mk(binary(), #{nullable => true})} - , {body, hoconsc:mk(binary(), #{nullable => true})} - , {headers, hoconsc:mk(map(), #{nullable => true})} + [ {method, hoconsc:mk(hoconsc:enum([post, put, get, delete]), #{required => false})} + , {path, hoconsc:mk(binary(), #{required => false})} + , {body, hoconsc:mk(binary(), #{required => false})} + , {headers, hoconsc:mk(map(), #{required => false})} , {request_timeout, sc(emqx_schema:duration_ms(), - #{ nullable => true + #{ required => false , desc => "The timeout when sending request to the HTTP server" })} ]. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 05f30c2ee..135ba3382 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -91,11 +91,9 @@ mongo_fields() -> , {pool_size, fun emqx_connector_schema_lib:pool_size/1} , {username, fun emqx_connector_schema_lib:username/1} , {password, fun emqx_connector_schema_lib:password/1} - , {auth_source, #{type => binary(), - nullable => true}} + , {auth_source, #{type => binary(), required => false}} , {database, fun emqx_connector_schema_lib:database/1} - , {topology, #{type => hoconsc:ref(?MODULE, topology), - nullable => true}} + , {topology, #{type => hoconsc:ref(?MODULE, topology), required => false}} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -289,14 +287,14 @@ init_worker_options([], Acc) -> Acc. %% Schema funcs server(type) -> emqx_schema:ip_port(); -server(nullable) -> false; +server(required) -> true; server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; server(converter) -> fun to_server_raw/1; server(desc) -> ?SERVER_DESC("MongoDB", integer_to_list(?MONGO_DEFAULT_PORT)); server(_) -> undefined. servers(type) -> binary(); -servers(nullable) -> false; +servers(required) -> true; servers(validator) -> [?NOT_EMPTY("the value of the field 'servers' cannot be empty")]; servers(converter) -> fun to_servers_raw/1; servers(desc) -> ?SERVERS_DESC ++ server(desc); @@ -311,11 +309,11 @@ r_mode(default) -> master; r_mode(_) -> undefined. duration(type) -> emqx_schema:duration_ms(); -duration(nullable) -> true; +duration(required) -> false; duration(_) -> undefined. replica_set_name(type) -> binary(); -replica_set_name(nullable) -> true; +replica_set_name(required) -> false; replica_set_name(_) -> undefined. srv_record(type) -> boolean(); diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index 699027b4b..8fbed74fb 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -50,7 +50,7 @@ fields(config) -> emqx_connector_schema_lib:ssl_fields(). server(type) -> emqx_schema:ip_port(); -server(nullable) -> false; +server(required) -> true; server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; server(converter) -> fun to_server/1; server(desc) -> ?SERVER_DESC("MySQL", integer_to_list(?MYSQL_DEFAULT_PORT)); diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 9e2c72e0b..77509b276 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -56,11 +56,11 @@ fields(config) -> emqx_connector_schema_lib:ssl_fields(). named_queries(type) -> map(); -named_queries(nullable) -> true; +named_queries(required) -> false; named_queries(_) -> undefined. server(type) -> emqx_schema:ip_port(); -server(nullable) -> false; +server(required) -> true; server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; server(converter) -> fun to_server/1; server(desc) -> ?SERVER_DESC("PostgreSQL", integer_to_list(?PGSQL_DEFAULT_PORT)); diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 91928acc6..e8bcc86ac 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -76,14 +76,14 @@ fields(sentinel) -> emqx_connector_schema_lib:ssl_fields(). server(type) -> emqx_schema:ip_port(); -server(nullable) -> false; +server(required) -> true; server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; server(converter) -> fun to_server_raw/1; server(desc) -> ?SERVER_DESC("Redis", integer_to_list(?REDIS_DEFAULT_PORT)); server(_) -> undefined. servers(type) -> list(); -servers(nullable) -> false; +servers(required) -> true; servers(validator) -> [?NOT_EMPTY("the value of the field 'servers' cannot be empty")]; servers(converter) -> fun to_servers_raw/1; servers(desc) -> ?SERVERS_DESC ++ server(desc); diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index a43cab0d4..32ddcd518 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -66,7 +66,7 @@ relational_db_fields() -> ]. database(type) -> binary(); -database(nullable) -> false; +database(required) -> true; database(validator) -> [?NOT_EMPTY("the value of the field 'database' cannot be empty")]; database(_) -> undefined. @@ -76,11 +76,11 @@ pool_size(validator) -> [?MIN(1)]; pool_size(_) -> undefined. username(type) -> binary(); -username(nullable) -> true; +username(required) -> false; username(_) -> undefined. password(type) -> binary(); -password(nullable) -> true; +password(required) -> false; password(_) -> undefined. auto_reconnect(type) -> boolean(); diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl index d01f0110a..ddf3b8bc2 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -57,7 +57,7 @@ topic filters for 'remote_topic' of ingress connections. })} , {name, sc(binary(), - #{ nullable => true + #{ required => false , desc => "Connector name, used as a human-readable description of the connector." })} , {server, @@ -105,7 +105,7 @@ fields("ingress") -> %% the message maybe subscribed by rules, in this case 'local_topic' is not necessary [ {remote_topic, sc(binary(), - #{ nullable => false + #{ required => true , desc => "Receive messages from which topic of the remote broker" })} , {remote_qos, diff --git a/apps/emqx_connector/test/emqx_connector_test_helpers.erl b/apps/emqx_connector/test/emqx_connector_test_helpers.erl index 7bb0086fe..9e3fc5257 100644 --- a/apps/emqx_connector/test/emqx_connector_test_helpers.erl +++ b/apps/emqx_connector/test/emqx_connector_test_helpers.erl @@ -28,9 +28,12 @@ check_fields({FieldName, FieldValue}) -> ?assert(is_atom(FieldName)), if is_map(FieldValue) -> + ct:pal("~p~n", [{FieldName, FieldValue}]), ?assert( - maps:is_key(type, FieldValue) and maps:is_key(default, FieldValue) or - (maps:is_key(nullable, FieldValue) and maps:get(nullable, FieldValue, false) =:= true) + (maps:is_key(type, FieldValue) + andalso maps:is_key(default, FieldValue)) + orelse ((maps:is_key(required, FieldValue) + andalso maps:get(required, FieldValue) =:= false)) ); true -> ?assert(is_function(FieldValue)) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index eb1d79f45..f112fc852 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -48,7 +48,7 @@ fields("http") -> [ {"protocol", sc( hoconsc:enum([http, https]), #{ desc => "HTTP/HTTPS protocol." - , nullable => false + , required => true , default => http})} , {"bind", fun bind/1} , {"num_acceptors", sc( @@ -74,18 +74,18 @@ fields("https") -> bind(type) -> hoconsc:union([non_neg_integer(), emqx_schema:ip_port()]); bind(default) -> 18083; -bind(nullable) -> false; +bind(required) -> true; bind(desc) -> "Port without IP(18083) or port with specified IP(127.0.0.1:18083)."; bind(_) -> undefined. default_username(type) -> string(); default_username(default) -> "admin"; -default_username(nullable) -> false; +default_username(required) -> true; default_username(_) -> undefined. default_password(type) -> string(); default_password(default) -> "public"; -default_password(nullable) -> false; +default_password(required) -> true; default_password(sensitive) -> true; default_password(desc) -> """ The initial default password for dashboard 'admin' user. @@ -94,7 +94,7 @@ default_password(_) -> undefined. cors(type) -> boolean(); cors(default) -> false; -cors(nullable) -> true; +cors(required) -> false; cors(desc) -> """Support Cross-Origin Resource Sharing (CORS). Allows a server to indicate any origins (domain, scheme, or port) other than diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 5744e7325..89b6795a9 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -21,21 +21,21 @@ %% API -export([spec/1, spec/2]). --export([namespace/0, fields/1]). +-export([namespace/0, namespace/1, fields/1]). -export([schema_with_example/2, schema_with_examples/2]). -export([error_codes/1, error_codes/2]). -export([filter_check_request/2, filter_check_request_and_translate_body/2]). -ifdef(TEST). --export([ parse_spec_ref/2 - , components/1 +-export([ parse_spec_ref/3 + , components/2 ]). -endif. -define(METHODS, [get, post, put, head, delete, patch, options, trace]). --define(DEFAULT_FIELDS, [example, allowReserved, style, format, +-define(DEFAULT_FIELDS, [example, allowReserved, style, format, readOnly, explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]). -define(INIT_SCHEMA, #{fields => #{}, translations => #{}, @@ -56,7 +56,10 @@ -type(filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}). -type(filter() :: fun((request(), request_meta()) -> filter_result())). --type(spec_opts() :: #{check_schema => boolean() | filter(), translate_body => boolean()}). +-type(spec_opts() :: #{check_schema => boolean() | filter(), + translate_body => boolean(), + schema_converter => fun((hocon_schema:schema(), Module::atom()) -> map()) + }). -type(route_path() :: string() | binary()). -type(route_methods() :: map()). @@ -79,12 +82,12 @@ spec(Module, Options) -> Paths = apply(Module, paths, []), {ApiSpec, AllRefs} = lists:foldl(fun(Path, {AllAcc, AllRefsAcc}) -> - {OperationId, Specs, Refs} = parse_spec_ref(Module, Path), + {OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options), CheckSchema = support_check_schema(Options), {[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc], Refs ++ AllRefsAcc} end, {[], []}, Paths), - {ApiSpec, components(lists:usort(AllRefs))}. + {ApiSpec, components(lists:usort(AllRefs), Options)}. -spec(namespace() -> hocon_schema:name()). namespace() -> "public". @@ -162,7 +165,7 @@ support_check_schema(#{check_schema := Filter}) when is_function(Filter, 2) -> support_check_schema(_) -> #{filter => undefined}. -parse_spec_ref(Module, Path) -> +parse_spec_ref(Module, Path, Options) -> Schema = try erlang:apply(Module, schema, [Path]) @@ -172,7 +175,7 @@ parse_spec_ref(Module, Path) -> {Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) -> (not lists:member(Method, ?METHODS)) andalso throw({error, #{module => Module, path => Path, method => Method}}), - {Spec, SubRefs} = meta_to_spec(Meta, Module), + {Spec, SubRefs} = meta_to_spec(Meta, Module, Options), {Acc#{Method => Spec}, SubRefs ++ RefsAcc} end, {#{}, []}, maps:without(['operationId'], Schema)), @@ -217,7 +220,7 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) -> _ -> Type0 end, NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]}, - Option = #{nullable => true}, + Option = #{required => false}, #{<<"root">> := NewBody} = CheckFun(NewSchema, #{<<"root">> => Body}, Option), NewBody; %% TODO not support nest object check yet, please use ref! @@ -239,10 +242,10 @@ check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false)when is_map( Body. %% tags, description, summary, security, deprecated -meta_to_spec(Meta, Module) -> +meta_to_spec(Meta, Module, Options) -> {Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module), {RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module), - {Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module), + {Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options), { to_spec(Meta, Params, RequestBody, Responses), lists:usort(Refs1 ++ Refs2 ++ Refs3) @@ -265,30 +268,22 @@ parameters(Params, Module) -> In = hocon_schema:field_schema(Type, in), In =:= undefined andalso throw({error, <<"missing in:path/query field in parameters">>}), - Nullable = hocon_schema:field_schema(Type, nullable), + Required = hocon_schema:field_schema(Type, required), Default = hocon_schema:field_schema(Type, default), HoconType = hocon_schema:field_schema(Type, type), - Meta = init_meta(Nullable, Default), + Meta = init_meta(Default), {ParamType, Refs} = hocon_schema_to_spec(HoconType, Module), Spec0 = init_prop([required | ?DEFAULT_FIELDS], #{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type), - Spec1 = trans_required(Spec0, Nullable, In), + Spec1 = trans_required(Spec0, Required, In), Spec2 = trans_desc(Spec1, Type), {[Spec2 | Acc], Refs ++ RefsAcc} end end, {[], []}, Params), {lists:reverse(SpecList), AllRefs}. -init_meta(Nullable, Default) -> - Init = - case Nullable of - true -> #{nullable => true}; - _ -> #{} - end, - case Default =:= undefined of - true -> Init; - false -> Init#{default => Default} - end. +init_meta(undefined) -> #{}; +init_meta(Default) -> #{default => Default}. init_prop(Keys, Init, Type) -> lists:foldl(fun(Key, Acc) -> @@ -298,7 +293,7 @@ init_prop(Keys, Init, Type) -> end end, Init, Keys). -trans_required(Spec, false, _) -> Spec#{required => true}; +trans_required(Spec, true, _) -> Spec#{required => true}; trans_required(Spec, _, path) -> Spec#{required => true}; trans_required(Spec, _, _) -> Spec. @@ -317,28 +312,29 @@ request_body(Schema, Module) -> HoconSchema = hocon_schema:field_schema(Schema, type), SchemaExamples = hocon_schema:field_schema(Schema, examples), {hocon_schema_to_spec(HoconSchema, Module), SchemaExamples}; - false -> {parse_object(Schema, Module), undefined} + false -> {parse_object(Schema, Module, #{}), undefined} end, {#{<<"content">> => content(Props, Examples)}, Refs}. -responses(Responses, Module) -> - {Spec, Refs, _} = maps:fold(fun response/3, {#{}, [], Module}, Responses), +responses(Responses, Module, Options) -> + {Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses), {Spec, Refs}. -response(Status, Bin, {Acc, RefsAcc, Module}) when is_binary(Bin) -> - {Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module}; +response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) -> + {Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options}; %% Support swagger raw object(file download). %% TODO: multi type response(i.e. Support both 'application/json' and 'plain/text') -response(Status, #{content := _} = Content, {Acc, RefsAcc, Module}) -> - {Acc#{integer_to_binary(Status) => Content}, RefsAcc, Module}; -response(Status, ?REF(StructName), {Acc, RefsAcc, Module}) -> - response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module}); -response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module}) -> - {Spec, Refs} = hocon_schema_to_spec(RRef, Module), +response(Status, #{content := _} = Content, {Acc, RefsAcc, Module, Options}) -> + {Acc#{integer_to_binary(Status) => Content}, RefsAcc, Module, Options}; +response(Status, ?REF(StructName), {Acc, RefsAcc, Module, Options}) -> + response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module, Options}); +response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module, Options}) -> + SchemaToSpec = schema_converter(Options), + {Spec, Refs} = SchemaToSpec(RRef, Module), Content = content(Spec), - {Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module}; -response(Status, Schema, {Acc, RefsAcc, Module}) -> + {Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module, Options}; +response(Status, Schema, {Acc, RefsAcc, Module, Options}) -> case hoconsc:is_schema(Schema) of true -> Hocon = hocon_schema:field_schema(Schema, type), @@ -348,33 +344,34 @@ response(Status, Schema, {Acc, RefsAcc, Module}) -> Content = content(Spec, Examples), { Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}}, - Refs ++ RefsAcc, Module + Refs ++ RefsAcc, Module, Options }; false -> - {Props, Refs} = parse_object(Schema, Module), - Content = #{<<"content">> => content(Props)}, - {Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module} + {Props, Refs} = parse_object(Schema, Module, Options), + Init = trans_desc(#{}, Schema), + Content = Init#{<<"content">> => content(Props)}, + {Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options} end. -components(Refs) -> +components(Refs, Options) -> lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [], - components(Refs, #{}, []))). + components(Options, Refs, #{}, []))). -components([], SpecAcc, []) -> SpecAcc; -components([], SpecAcc, SubRefAcc) -> components(SubRefAcc, SpecAcc, []); -components([{Module, Field} | Refs], SpecAcc, SubRefsAcc) -> +components(_Options, [], SpecAcc, []) -> SpecAcc; +components(Options, [], SpecAcc, SubRefAcc) -> components(Options, SubRefAcc, SpecAcc, []); +components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) -> Props = hocon_schema_fields(Module, Field), Namespace = namespace(Module), - {Object, SubRefs} = parse_object(Props, Module), + {Object, SubRefs} = parse_object(Props, Module, Options), NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Object}, - components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc); + components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc); %% parameters in ref only have one value, not array -components([{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) -> +components(Options, [{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) -> Props = hocon_schema_fields(Module, Field), {[Param], SubRefs} = parameters(Props, Module), Namespace = namespace(Module), NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param}, - components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc). + components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc). hocon_schema_fields(Module, StructName) -> case apply(Module, fields, [StructName]) of @@ -425,7 +422,7 @@ hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) -> %% todo: Find a way to fetch enum value from user_id_type(). typename_to_spec("user_id_type()", _Mod) -> #{type => string, enum => [clientid, username]}; -typename_to_spec("term()", _Mod) -> #{type => string, example => "term"}; +typename_to_spec("term()", _Mod) -> #{type => string, example => "any"}; typename_to_spec("boolean()", _Mod) -> #{type => boolean, example => true}; typename_to_spec("binary()", _Mod) -> #{type => string, example => <<"binary-example">>}; typename_to_spec("float()", _Mod) -> #{type => number, example => 3.14159}; @@ -444,7 +441,6 @@ typename_to_spec("epoch_millisecond()", _Mod) -> #{type => integer, example => 1640995200000, desc => <<"epoch-millisecond">>}, #{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}] }; -typename_to_spec("unicode_binary()", _Mod) -> #{type => string, example => <<"unicode-binary">>}; typename_to_spec("duration()", _Mod) -> #{type => string, example => <<"12m">>}; typename_to_spec("duration_s()", _Mod) -> #{type => string, example => <<"1h">>}; typename_to_spec("duration_ms()", _Mod) -> #{type => string, example => <<"32s">>}; @@ -547,7 +543,7 @@ to_bin(Boolean) when is_boolean(Boolean) -> Boolean; to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); to_bin(X) -> X. -parse_object(PropList = [_ | _], Module) when is_list(PropList) -> +parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) -> {Props, Required, Refs} = lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) -> NameBin = to_bin(Name), @@ -556,7 +552,8 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) -> HoconType = hocon_schema:field_schema(Hocon, type), Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon), Init = trans_desc(Init0, Hocon), - {Prop, Refs1} = hocon_schema_to_spec(HoconType, Module), + SchemaToSpec = schema_converter(Options), + {Prop, Refs1} = SchemaToSpec(HoconType, Module), NewRequiredAcc = case is_required(Hocon) of true -> [NameBin | RequiredAcc]; @@ -564,7 +561,7 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) -> end, {[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc}; false -> - {SubObject, SubRefs} = parse_object(Hocon, Module), + {SubObject, SubRefs} = parse_object(Hocon, Module, Options), {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc} end end, {[], [], []}, PropList), @@ -573,14 +570,13 @@ parse_object(PropList = [_ | _], Module) when is_list(PropList) -> [] -> {Object, Refs}; _ -> {maps:put(required, Required, Object), Refs} end; -parse_object(Other, Module) -> +parse_object(Other, Module, Options) -> erlang:throw({error, #{msg => <<"Object only supports not empty proplists">>, - args => Other, module => Module}}). + args => Other, module => Module, options => Options}}). is_required(Hocon) -> - hocon_schema:field_schema(Hocon, required) =:= true orelse - hocon_schema:field_schema(Hocon, nullable) =:= false. + hocon_schema:field_schema(Hocon, required) =:= true. content(ApiSpec) -> content(ApiSpec, undefined). @@ -593,3 +589,6 @@ content(ApiSpec, Examples) when is_map(Examples) -> to_ref(Mod, StructName, Acc, RefsAcc) -> Ref = #{<<"$ref">> => ?TO_COMPONENTS_PARAM(Mod, StructName)}, {[Ref | Acc], [{Mod, StructName, parameter} | RefsAcc]}. + +schema_converter(Options) -> + maps:get(schema_converter, Options, fun hocon_schema_to_spec/2). diff --git a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl index c47c91d9c..4cf53ef38 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl @@ -50,8 +50,8 @@ t_ref(_Config) -> LocalPath = "/test/in/ref/local", Path = "/test/in/ref", Expect = [#{<<"$ref">> => <<"#/components/parameters/emqx_swagger_parameter_SUITE.page">>}], - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, LocalPath), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, LocalPath, #{}), ?assertEqual(test, OperationId), Params = maps:get(parameters, maps:get(post, Spec)), ?assertEqual(Expect, Params), @@ -64,7 +64,7 @@ t_public_ref(_Config) -> #{<<"$ref">> => <<"#/components/parameters/public.page">>}, #{<<"$ref">> => <<"#/components/parameters/public.limit">>} ], - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), ?assertEqual(test, OperationId), Params = maps:get(parameters, maps:get(post, Spec)), ?assertEqual(Expect, Params), @@ -80,7 +80,7 @@ t_public_ref(_Config) -> #{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>, example => 1,in => query,name => page, schema => #{default => 1,example => 100,type => integer}}}], - ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs)), + ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs,#{})), ok. t_in_mix(_Config) -> @@ -110,7 +110,7 @@ t_in_mix(_Config) -> t_without_in(_Config) -> ?assertThrow({error, <<"missing in:path/query field in parameters">>}, - emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in")), + emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in", #{})), ok. t_require(_Config) -> @@ -124,8 +124,7 @@ t_nullable(_Config) -> NullableFalse = [#{in => query,name => userid, required => true, schema => #{example => <<"binary-example">>, type => string}}], NullableTrue = [#{in => query,name => userid, - schema => #{example => <<"binary-example">>, type => string, - nullable => true}}], + schema => #{example => <<"binary-example">>, type => string}, required => false}], validate("/nullable/false", NullableFalse), validate("/nullable/true", NullableTrue), ok. @@ -133,10 +132,10 @@ t_nullable(_Config) -> t_method(_Config) -> PathOk = "/method/ok", PathError = "/method/error", - {test, Spec, []} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk), + {test, Spec, []} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk, #{}), ?assertEqual(lists:sort(?METHODS), lists:sort(maps:keys(Spec))), ?assertThrow({error, #{module := ?MODULE, path := PathError, method := bar}}, - emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError)), + emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError, #{})), ok. t_in_path_trans(_Config) -> @@ -242,7 +241,7 @@ assert_all_filters_equal(Spec, Filter) -> Spec). validate(Path, ExpectParams) -> - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), ?assertEqual(test, OperationId), Params = maps:get(parameters, maps:get(post, Spec)), ?assertEqual(ExpectParams, Params), @@ -358,9 +357,9 @@ schema("/test/without/in") -> schema("/required/false") -> to_schema([{'userid', mk(binary(), #{in => query, required => false})}]); schema("/nullable/false") -> - to_schema([{'userid', mk(binary(), #{in => query, nullable => false})}]); + to_schema([{'userid', mk(binary(), #{in => query, required => true})}]); schema("/nullable/true") -> - to_schema([{'userid', mk(binary(), #{in => query, nullable => true})}]); + to_schema([{'userid', mk(binary(), #{in => query, required => false})}]); schema("/method/ok") -> Response = #{responses => #{200 => <<"ok">>}}, lists:foldl(fun(Method, Acc) -> Acc#{Method => Response} end, diff --git a/apps/emqx_dashboard/test/emqx_swagger_remote_schema.erl b/apps/emqx_dashboard/test/emqx_swagger_remote_schema.erl index 110f08ebe..d2a47e59e 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_remote_schema.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_remote_schema.erl @@ -50,10 +50,10 @@ fields("ref3") -> default_username(type) -> string(); default_username(default) -> "admin"; -default_username(nullable) -> false; +default_username(required) -> true; default_username(_) -> undefined. default_password(type) -> string(); default_password(default) -> "public"; -default_password(nullable) -> false; +default_password(required) -> true; default_password(_) -> undefined. diff --git a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl index 84cac6229..bf48046c2 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl @@ -151,7 +151,7 @@ t_nest_ref(_Config) -> t_none_ref(_Config) -> Path = "/ref/none", ?assertThrow({error, #{mfa := {?MODULE, schema, [Path]}}}, - emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path)), + emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{})), ok. t_sub_fields(_Config) -> @@ -472,11 +472,11 @@ t_object_trans_error(_Config) -> ok. validate(Path, ExpectSpec, ExpectRefs) -> - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), ?assertEqual(test, OperationId), ?assertEqual(ExpectSpec, Spec), ?assertEqual(ExpectRefs, Refs), - {Spec, emqx_dashboard_swagger:components(Refs)}. + {Spec, emqx_dashboard_swagger:components(Refs, #{})}. filter(ApiSpec, Path) -> @@ -499,16 +499,16 @@ paths() -> schema("/object") -> to_schema([ - {per_page, mk(range(1, 100), #{nullable => false, desc => <<"good per page desc">>})}, + {per_page, mk(range(1, 100), #{required => true, desc => <<"good per page desc">>})}, {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]), - #{default => 5, nullable => false})}, + #{default => 5, required => true})}, {inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})} ]); schema("/nest/object") -> to_schema([ {per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})}, {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]), - #{default => 5, nullable => false})}, + #{default => 5, required => true})}, {nest_object, [ {good_nest_1, mk(integer(), #{})}, {good_nest_2, mk(hoconsc:ref(?MODULE, good_ref), #{})} @@ -572,5 +572,5 @@ enable(_) -> undefined. init_file(type) -> binary(); init_file(desc) -> <<"test test desc">>; -init_file(nullable) -> true; +init_file(required) -> false; init_file(_) -> undefined. diff --git a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl index be27b954e..3b68c8eff 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl @@ -69,7 +69,7 @@ t_error(_Config) -> {<<"message">>, #{description => <<"Details description of the error.">>, example => <<"Error code to troubleshoot problems.">>, type => string}}] }}}}, - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), ?assertEqual(test, OperationId), Response = maps:get(responses, maps:get(get, Spec)), ?assertEqual(Error400, maps:get(<<"400">>, Response)), @@ -197,7 +197,7 @@ t_complicated_type(_Config) -> {<<"fix_integer">>, #{default => 100, enum => [100], example => 100,type => integer}} ], <<"type">> => object}}}}, - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), ?assertEqual(test, OperationId), Response = maps:get(responses, maps:get(post, Spec)), ?assertEqual(Object, maps:get(<<"200">>, Response)), @@ -299,9 +299,9 @@ schema("/simple/bin") -> to_schema(<<"binary ok">>); schema("/object") -> Object = [ - {per_page, mk(range(1, 100), #{nullable => false, desc => <<"good per page desc">>})}, + {per_page, mk(range(1, 100), #{required => true, desc => <<"good per page desc">>})}, {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]), - #{default => 5, nullable => false})}, + #{default => 5, required => true})}, {inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})} ], to_schema(Object); @@ -309,7 +309,7 @@ schema("/nest/object") -> Response = [ {per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})}, {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]), - #{default => 5, nullable => false})}, + #{default => 5, required => true})}, {nest_object, [ {good_nest_1, mk(integer(), #{})}, {good_nest_2, mk(hoconsc:ref(?MODULE, good_ref), #{})} @@ -379,14 +379,14 @@ schema("/fields/sub") -> to_schema(hoconsc:ref(sub_fields)). validate(Path, ExpectObject, ExpectRefs) -> - {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path), + {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}), ?assertEqual(test, OperationId), Response = maps:get(responses, maps:get(post, Spec)), ?assertEqual(ExpectObject, maps:get(<<"200">>, Response)), ?assertEqual(ExpectObject, maps:get(<<"201">>, Response)), ?assertEqual(#{}, maps:without([<<"201">>, <<"200">>], Response)), ?assertEqual(ExpectRefs, Refs), - {Spec, emqx_dashboard_swagger:components(Refs)}. + {Spec, emqx_dashboard_swagger:components(Refs, #{})}. to_schema(Object) -> #{ @@ -425,5 +425,5 @@ enable(_) -> undefined. init_file(type) -> binary(); init_file(desc) -> <<"test test desc">>; -init_file(nullable) -> true; +init_file(required) -> false; init_file(_) -> undefined. diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 610971a8c..b3cbcb5b4 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -219,7 +219,7 @@ params_gateway_status_in_qs() -> [{status, mk(binary(), #{ in => query - , nullable => true + , required => false , desc => <<"Gateway Status">> , example => <<"">> })} @@ -245,11 +245,11 @@ fields(gateway_overview) -> #{desc => <<"The Gateway created datetime">>})} , {started_at, mk(binary(), - #{ nullable => true + #{ required => false , desc => <<"The Gateway started datetime">>})} , {stopped_at, mk(binary(), - #{ nullable => true + #{ required => false , desc => <<"The Gateway stopped datetime">>})} , {max_connections, mk(integer(), @@ -260,7 +260,7 @@ fields(gateway_overview) -> })} , {listeners, mk(hoconsc:array(ref(gateway_listener_overview)), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => <<"The Gateway listeners overview">>})} ]; fields(gateway_listener_overview) -> @@ -295,7 +295,7 @@ fields(Listener) when Listener == tcp_listener; Listener == dtls_listener -> [ {id, mk(binary(), - #{ nullable => true + #{ required => false , desc => <<"Listener ID">>})} , {type, mk(hoconsc:union([tcp, ssl, udp, dtls]), @@ -305,7 +305,7 @@ fields(Listener) when Listener == tcp_listener; #{ desc => <<"Listener Name">>})} , {running, mk(boolean(), - #{ nullable => true + #{ required => false , desc => <<"Listener running status">>})} ] ++ emqx_gateway_schema:fields(Listener); @@ -334,7 +334,7 @@ convert_listener_struct(Schema) -> {value, {listeners, #{type := Type}}, Schema1} = lists:keytake(listeners, 1, Schema), ListenerSchema = hoconsc:mk(listeners_schema(Type), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => <<"The gateway listeners">> }), lists:keystore(listeners, 1, Schema1, {listeners, ListenerSchema}). diff --git a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl index 5d48c9ac3..30ea7b050 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl @@ -287,13 +287,13 @@ params_userid_in_path() -> params_paging_in_qs() -> [{page, mk(integer(), #{ in => query - , nullable => true + , required => false , desc => <<"Page Index">> , example => 1 })}, {limit, mk(integer(), #{ in => query - , nullable => true + , required => false , desc => <<"Page Limit">> , example => 100 })} diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index 22c3dee37..e9c0fa879 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -468,7 +468,7 @@ params_client_insta() -> ++ params_gateway_name_in_path(). params_client_searching_in_qs() -> - M = #{in => query, nullable => true, example => <<"">>}, + M = #{in => query, required => false, example => <<"">>}, [ {node, mk(binary(), M#{desc => <<"Match the client's node name">>})} @@ -532,7 +532,7 @@ params_paging() -> [ {page, mk(integer(), #{ in => query - , nullable => true + , required => false , desc => <<"Page Index">> , example => 1 })} @@ -540,7 +540,7 @@ params_paging() -> mk(integer(), #{ in => query , desc => <<"Page Limit">> - , nullable => true + , required => false , example => 100 })} ]. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl index 95910cddd..8b8ab6fd2 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl @@ -434,13 +434,13 @@ params_userid_in_path() -> params_paging_in_qs() -> [{page, mk(integer(), #{ in => query - , nullable => true + , required => false , desc => <<"Page Index">> , example => 1 })}, {limit, mk(integer(), #{ in => query - , nullable => true + , required => false , desc => <<"Page Limit">> , example => 100 })} @@ -457,22 +457,22 @@ fields(listener) -> common_listener_opts() ++ [ {tcp, mk(ref(tcp_listener_opts), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => <<"The tcp socket options for tcp or ssl listener">> })} , {ssl, mk(ref(ssl_listener_opts), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => <<"The ssl socket options for ssl listener">> })} , {udp, mk(ref(udp_listener_opts), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => <<"The udp socket options for udp or dtls listener">> })} , {dtls, mk(ref(dtls_listener_opts), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => <<"The dtls socket options for dtls listener">> })} ]; @@ -529,47 +529,47 @@ lists_key_without([K | Ks], N, L) -> common_listener_opts() -> [ {enable, mk(boolean(), - #{ nullable => true + #{ required => false , desc => <<"Whether to enable this listener">>})} , {id, mk(binary(), - #{ nullable => true + #{ required => false , desc => <<"Listener Id">>})} , {name, mk(binary(), - #{ nullable => true + #{ required => false , desc => <<"Listener name">>})} , {type, mk(hoconsc:enum([tcp, ssl, udp, dtls]), - #{ nullable => true + #{ required => false , desc => <<"Listener type. Enum: tcp, udp, ssl, dtls">>})} , {running, mk(boolean(), - #{ nullable => true + #{ required => false , desc => <<"Listener running status">>})} , {bind, mk(binary(), - #{ nullable => true + #{ required => false , desc => <<"Listener bind address or port">>})} , {acceptors, mk(integer(), - #{ nullable => true + #{ required => false , desc => <<"Listener acceptors number">>})} , {access_rules, mk(hoconsc:array(binary()), - #{ nullable => true + #{ required => false , desc => <<"Listener Access rules for client">>})} , {max_conn_rate, mk(integer(), - #{ nullable => true + #{ required => false , desc => <<"Max connection rate for the listener">>})} , {max_connections, mk(integer(), - #{ nullable => true + #{ required => false , desc => <<"Max connections for the listener">>})} , {mountpoint, mk(binary(), - #{ nullable => true + #{ required => false , desc => <<"The Mounpoint for clients of the listener. " "The gateway-level mountpoint configuration can be overloaded " @@ -577,7 +577,7 @@ common_listener_opts() -> %% FIXME: , {authentication, mk(emqx_authn_schema:authenticator_type(), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => <<"The authenticatior for this listener">> })} ] ++ emqx_gateway_schema:proxy_protocol_opts(). diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index df46ace25..822118e2a 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -59,21 +59,21 @@ roots() -> [gateway]. fields(gateway) -> [{stomp, sc(ref(stomp), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => "The Stomp Gateway configuration.
This gateway supports v1.2/1.1/1.0" })}, {mqttsn, sc(ref(mqttsn), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => "The MQTT-SN Gateway configuration.
This gateway only supports the v1.2 protocol" })}, {coap, sc(ref(coap), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => "The CoAP Gateway configuration.
This gateway is implemented based on RFC-7252 and @@ -81,14 +81,14 @@ https://core-wg.github.io/coap-pubsub/draft-ietf-core-pubsub.html" })}, {lwm2m, sc(ref(lwm2m), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => "The LwM2M Gateway configuration.
This gateway only supports the v1.0.1 protocol" })}, {exproto, sc(ref(exproto), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => "The Extension Protocol configuration" })} ]; @@ -120,7 +120,7 @@ fields(mqttsn) -> [ {gateway_id, sc(integer(), #{ default => 1 - , nullable => false + , required => true , desc => "MQTT-SN Gateway ID.
When the broadcast option is enabled, @@ -145,7 +145,7 @@ The client just sends its 'PUBLISH' messages to a GW" , {predefined, sc(hoconsc:array(ref(mqttsn_predefined)), #{ default => [] - , nullable => {true, recursively} + , required => {false, recursively} , desc => <<"The pre-defined topic IDs and topic names.
A 'pre-defined' topic ID is a topic ID whose mapping to a topic name @@ -221,7 +221,7 @@ fields(lwm2m) -> [ {xml_dir, sc(binary(), #{ default =>"etc/lwm2m_xml" - , nullable => false + , required => true , desc => "The Directory for LwM2M Resource definition" })} , {lifetime_min, @@ -261,7 +261,7 @@ beyond this time window are temporarily stored in memory." })} , {translators, sc(ref(lwm2m_translators), - #{ nullable => false + #{ required => true , desc => "Topic configuration for LwM2M's gateway publishing and subscription" })} , {listeners, sc(ref(udp_listeners))} @@ -270,12 +270,12 @@ beyond this time window are temporarily stored in memory." fields(exproto) -> [ {server, sc(ref(exproto_grpc_server), - #{ nullable => false + #{ required => true , desc => "Configurations for starting the ConnectionAdapter service" })} , {handler, sc(ref(exproto_grpc_handler), - #{ nullable => false + #{ required => true , desc => "Configurations for request to ConnectionHandler service" })} , {listeners, sc(ref(udp_tcp_listeners))} @@ -284,18 +284,18 @@ fields(exproto) -> fields(exproto_grpc_server) -> [ {bind, sc(hoconsc:union([ip_port(), integer()]), - #{nullable => false})} + #{required => true})} , {ssl, sc(ref(ssl_server_opts), - #{ nullable => {true, recursively} + #{ required => {false, recursively} })} ]; fields(exproto_grpc_handler) -> - [ {address, sc(binary(), #{nullable => false})} + [ {address, sc(binary(), #{required => true})} , {ssl, sc(ref(ssl_client_opts), - #{ nullable => {true, recursively} + #{ required => {false, recursively} })} ]; @@ -324,13 +324,13 @@ fields(lwm2m_translators) -> For each new LwM2M client that succeeds in going online, the gateway creates a subscription relationship to receive downstream commands and send it to the LwM2M client" - , nullable => false + , required => true })} , {response, sc(ref(translator), #{ desc => "The topic for gateway to publish the acknowledge events from LwM2M client" - , nullable => false + , required => true })} , {notify, sc(ref(translator), @@ -338,24 +338,24 @@ the LwM2M client" "The topic for gateway to publish the notify events from LwM2M client.
After succeed observe a resource of LwM2M client, Gateway will send the notify events via this topic, if the client reports any resource changes" - , nullable => false + , required => true })} , {register, sc(ref(translator), #{ desc => "The topic for gateway to publish the register events from LwM2M client.
" - , nullable => false + , required => true })} , {update, sc(ref(translator), #{ desc => "The topic for gateway to publish the update events from LwM2M client.
" - , nullable => false + , required => true })} ]; fields(translator) -> - [ {topic, sc(binary(), #{nullable => false})} + [ {topic, sc(binary(), #{required => true})} , {qos, sc(emqx_schema:qos(), #{default => 0})} ]; @@ -423,7 +423,7 @@ fields(dtls_opts) -> authentication_schema() -> sc(emqx_authn_schema:authenticator_type(), - #{ nullable => {true, recursively} + #{ required => {false, recursively} , desc => """Default authentication configs for all the gateway listeners.
For per-listener overrides see authentication diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index e5cfca44d..86e9e6905 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -44,7 +44,7 @@ schema("/alarms") -> hoconsc:ref(emqx_dashboard_swagger, limit), {activated, hoconsc:mk(boolean(), #{in => query, desc => <<"All alarms, if not specified">>, - nullable => true})} + required => false})} ], responses => #{ 200 => [ diff --git a/apps/emqx_management/src/emqx_mgmt_api_app.erl b/apps/emqx_management/src/emqx_mgmt_api_app.erl index 51bd92bc0..fb43629ff 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_app.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_app.erl @@ -94,7 +94,7 @@ fields(app) -> {expired_at, hoconsc:mk(hoconsc:union([undefined, emqx_datetime:epoch_second()]), #{desc => "No longer valid datetime", example => <<"2021-12-05T02:01:34.186Z">>, - nullable => true, + required => false, default => undefined })}, {created_at, hoconsc:mk(emqx_datetime:epoch_second(), @@ -102,8 +102,8 @@ fields(app) -> example => <<"2021-12-01T00:00:00.000Z">> })}, {desc, hoconsc:mk(binary(), - #{example => <<"Note">>, nullable => true})}, - {enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", nullable => true})} + #{example => <<"Note">>, required => false})}, + {enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})} ]; fields(name) -> [{name, hoconsc:mk(binary(), diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl index 8633ccfc2..265fb2a9b 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_banned.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_banned.erl @@ -101,27 +101,27 @@ fields(ban) -> [ {as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{ desc => <<"Banned type clientid, username, peerhost">>, - nullable => false, + required => true, example => username})}, {who, hoconsc:mk(binary(), #{ desc => <<"Client info as banned type">>, - nullable => false, + required => true, example => <<"Banned name"/utf8>>})}, {by, hoconsc:mk(binary(), #{ desc => <<"Commander">>, - nullable => true, + required => false, example => <<"mgmt_api">>})}, {reason, hoconsc:mk(binary(), #{ desc => <<"Banned reason">>, - nullable => true, + required => false, example => <<"Too many requests">>})}, {at, hoconsc:mk(emqx_datetime:epoch_second(), #{ desc => <<"Create banned time, rfc3339, now if not specified">>, - nullable => true, + required => false, example => <<"2021-10-25T21:48:47+08:00">>})}, {until, hoconsc:mk(emqx_datetime:epoch_second(), #{ desc => <<"Cancel banned time, rfc3339, now + 5 minute if not specified">>, - nullable => true, + required => false, example => <<"2021-10-25T21:53:47+08:00">>}) } ]; diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 3218d707e..86d24434c 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -93,65 +93,65 @@ schema("/clients") -> hoconsc:ref(emqx_dashboard_swagger, limit), {node, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Node name">>, example => atom_to_list(node())})}, {username, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"User name">>})}, {zone, hoconsc:mk(binary(), #{ in => query, - nullable => true})}, + required => false})}, {ip_address, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Client's IP address">>, example => <<"127.0.0.1">>})}, {conn_state, hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{ in => query, - nullable => true, + required => false, desc => <<"The current connection status of the client, ", "the possible values are connected,idle,disconnected">>})}, {clean_start, hoconsc:mk(boolean(), #{ in => query, - nullable => true, + required => false, description => <<"Whether the client uses a new session">>})}, {proto_name, hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{ in => query, - nullable => true, + required => false, description => <<"Client protocol name, ", "the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>})}, {proto_ver, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Client protocol version">>})}, {like_clientid, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Fuzzy search of client identifier by substring method">>})}, {like_username, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Client user name, fuzzy search by substring">>})}, {gte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{ in => query, - nullable => true, + required => false, desc => <<"Search client session creation time by greater", " than or equal method, rfc3339 or timestamp(millisecond)">>})}, {lte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{ in => query, - nullable => true, + required => false, desc => <<"Search client session creation time by less", " than or equal method, rfc3339 or timestamp(millisecond)">>})}, {gte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{ in => query, - nullable => true, + required => false, desc => <<"Search client connection creation time by greater" " than or equal method, rfc3339 or timestamp(epoch millisecond)">>})}, {lte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{ in => query, - nullable => true, + required => false, desc => <<"Search client connection creation time by less" " than or equal method, rfc3339 or timestamp(millisecond)">>})} ], diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 599073704..8f9797843 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -19,7 +19,7 @@ -include_lib("hocon/include/hoconsc.hrl"). -behaviour(minirest_api). --export([api_spec/0]). +-export([api_spec/0, namespace/0]). -export([paths/0, schema/1, fields/1]). -export([config/3, config_reset/3, configs/3, get_full_config/0]). @@ -29,15 +29,31 @@ -define(PREFIX, "/configs/"). -define(PREFIX_RESET, "/configs_reset/"). -define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))). --define(EXCLUDES, [listeners, node, cluster, gateway, rule_engine]). +-define(EXCLUDES, [ + exhook, + gateway, + plugins, + bridges, + "rule_engine", + "authorization", + "authentication", + "rpc", + "db", + "connectors", + "slow_subs", + "psk_authentication" +]). api_spec() -> emqx_dashboard_swagger:spec(?MODULE). +namespace() -> "configuration". + paths() -> ["/configs", "/configs_reset/:rootname"] ++ lists:map(fun({Name, _Type}) -> ?PREFIX ++ to_list(Name) end, config_list(?EXCLUDES)). + schema("/configs") -> #{ 'operationId' => configs, @@ -51,12 +67,15 @@ schema("/configs") -> desc => <<"Node's name: If you do not fill in the fields, this node will be used by default.">>})}], responses => #{ - 200 => config_list([]) + 200 => config_list(?EXCLUDES) } } }; schema("/configs_reset/:rootname") -> - Paths = lists:map(fun({Path, _}) -> Path end, config_list(?EXCLUDES)), + Paths = lists:map( + fun({Path, _})when is_atom(Path) -> Path; + ({Path, _}) when is_list(Path) -> list_to_atom(Path) + end, config_list(?EXCLUDES)), #{ 'operationId' => config_reset, post => #{ @@ -186,7 +205,7 @@ conf_path_from_querystr(Req) -> config_list(Exclude) -> Roots = emqx_conf_schema:roots(), - lists:foldl(fun(Key, Acc) -> lists:delete(Key, Acc) end, Roots, Exclude). + lists:foldl(fun(Key, Acc) -> lists:keydelete(Key, 1, Acc) end, Roots, Exclude). to_list(L) when is_list(L) -> L; to_list(Atom) when is_atom(Atom) -> atom_to_list(Atom). diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl index 5ebc01c21..ee087efa9 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl @@ -71,7 +71,7 @@ schema("/metrics") -> [{ aggregate , mk( boolean() , #{ in => query - , nullable => true + , required => false , desc => <<"Whether to aggregate all nodes Metrics">>}) }] , responses => diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 79c40a2e6..5eb16ad08 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -140,7 +140,7 @@ fields(node_info) -> , mk( emqx_schema:bytesize() , #{desc => <<"Used memory">>, example => "256.00M"})} , { node_status - , mk( enum(["Running", "Stopped"]) + , mk( enum(['Running', 'Stopped']) , #{desc => <<"Node status">>, example => "Running"})} , { otp_release , mk( string() @@ -164,7 +164,7 @@ fields(node_info) -> , mk( string() , #{desc => <<"Path to log files">>, example => "path/to/log | not found"})} , { role - , mk( enum(["core", "replicant"]) + , mk( enum([core, replicant]) , #{desc => <<"Node role">>, example => "core"})} ]. diff --git a/apps/emqx_management/src/emqx_mgmt_api_stats.erl b/apps/emqx_management/src/emqx_mgmt_api_stats.erl index d97a46623..ee28e8f73 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_stats.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_stats.erl @@ -59,7 +59,7 @@ fields(aggregate) -> , mk( boolean() , #{ desc => <<"Calculation aggregate for all nodes">> , in => query - , nullable => true + , required => false , example => false})} ]; fields(node_stats_data) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 40e3948a7..b33db0cd1 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -75,38 +75,38 @@ parameters() -> { node, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Node name">>, example => atom_to_list(node())}) }, { clientid, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Client ID">>}) }, { qos, hoconsc:mk(emqx_schema:qos(), #{ in => query, - nullable => true, + required => false, desc => <<"QoS">>}) }, { topic, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Topic, url encoding">>}) }, { match_topic, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Match topic string, url encoding">>}) }, { share_group, hoconsc:mk(binary(), #{ in => query, - nullable => true, + required => false, desc => <<"Shared subscription group name">>}) } ]. diff --git a/apps/emqx_management/src/emqx_mgmt_api_trace.erl b/apps/emqx_management/src/emqx_mgmt_api_trace.erl index 232d523c2..b9aaec95e 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_trace.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_trace.erl @@ -144,45 +144,45 @@ fields(trace) -> {name, hoconsc:mk(binary(), #{desc => "Unique and format by [a-zA-Z0-9-_]", validator => fun ?MODULE:validate_name/1, - nullable => false, + required => true, example => <<"EMQX-TRACE-1">>})}, {type, hoconsc:mk(hoconsc:enum([clientid, topic, ip_address]), #{desc => """Filter type""", - nullable => false, + required => true, example => <<"clientid">>})}, {topic, hoconsc:mk(binary(), #{desc => """support mqtt wildcard topic.""", - nullable => true, + required => false, example => <<"/dev/#">>})}, {clientid, hoconsc:mk(binary(), #{desc => """mqtt clientid.""", - nullable => true, + required => false, example => <<"dev-001">>})}, %% TODO add ip_address type in emqx_schema.erl {ip_address, hoconsc:mk(binary(), #{desc => "client ip address", - nullable => true, + required => false, example => <<"127.0.0.1">> })}, {status, hoconsc:mk(hoconsc:enum([running, stopped, waiting]), #{desc => "trace status", - nullable => true, + required => false, example => running })}, {start_at, hoconsc:mk(emqx_datetime:epoch_second(), #{desc => "rfc3339 timestamp or epoch second", - nullable => true, + required => false, example => <<"2021-11-04T18:17:38+08:00">> })}, {end_at, hoconsc:mk(emqx_datetime:epoch_second(), #{desc => "rfc3339 timestamp or epoch second", - nullable => true, + required => false, example => <<"2021-11-05T18:17:38+08:00">> })}, {log_size, hoconsc:mk(hoconsc:array(map()), #{desc => "trace log size", example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}], - nullable => true})} + required => false})} ]; fields(name) -> [{name, hoconsc:mk(binary(), @@ -198,14 +198,14 @@ fields(node) -> #{ desc => "Node name", in => query, - nullable => true + required => false })}]; fields(bytes) -> [{bytes, hoconsc:mk(integer(), #{ desc => "Maximum number of bytes to store in request", in => query, - nullable => true, + required => false, default => 1000 })}]; fields(position) -> @@ -213,7 +213,7 @@ fields(position) -> #{ desc => "Offset from the current trace position.", in => query, - nullable => true, + required => false, default => 0 })}]. diff --git a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl index bbd68948a..409fe9104 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl @@ -58,7 +58,7 @@ t_http_test(_Config) -> ?assertEqual( #{ <<"code">> => <<"BAD_REQUEST">>, - <<"message">> => <<"name : not_nullable">> + <<"message">> => <<"name : mandatory_required_field">> }, json(Body)), Name = <<"test-name">>, diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index 889268a4a..ddd7ffe88 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -121,12 +121,12 @@ fields(reset) -> <<"Topic Name. If this parameter is not present," " all created topic metrics will be reset">> , example => <<"testtopic/1">> - , nullable => true})} + , required => false})} , {action , mk( string() , #{ desc => <<"Action Name. Only as a \"reset\"">> , enum => [reset] - , nullable => false + , required => true , example => <<"reset">>})} ]; @@ -135,21 +135,21 @@ fields(topic_metrics) -> , mk( binary() , #{ desc => <<"Topic Name">> , example => <<"testtopic/1">> - , nullable => false})}, + , required => true})}, { create_time , mk( emqx_datetime:epoch_second() , #{ desc => <<"Topic Metrics created date time, in rfc3339">> - , nullable => false + , required => true , example => <<"2022-01-14T21:48:47+08:00">>})}, { reset_time , mk( emqx_datetime:epoch_second() , #{ desc => <<"Topic Metrics reset date time, in rfc3339. Nullable if never reset">> - , nullable => true + , required => false , example => <<"2022-01-14T21:48:47+08:00">>})}, { metrics , mk( ref(metrics) , #{ desc => <<"Topic Metrics fields">> - , nullable => false}) + , required => true}) } ]; diff --git a/apps/emqx_plugins/src/emqx_plugins_schema.erl b/apps/emqx_plugins/src/emqx_plugins_schema.erl index b47a0b3c3..9dfb77589 100644 --- a/apps/emqx_plugins/src/emqx_plugins_schema.erl +++ b/apps/emqx_plugins/src/emqx_plugins_schema.erl @@ -54,12 +54,12 @@ state_fields() -> "It should match the plugin application name-version as the " "for the plugin release package name
" "For example: my_plugin-0.1.0." - , nullable => false + , required => true })} , {enable, hoconsc:mk(boolean(), #{ desc => "Set to 'true' to enable this plugin" - , nullable => false + , required => true })} ]. @@ -69,14 +69,14 @@ root_fields() -> ]. states(type) -> hoconsc:array(hoconsc:ref(state)); -states(nullable) -> true; +states(required) -> false; states(default) -> []; states(desc) -> "An array of plugins in the desired states.
" "The plugins are started in the defined order"; states(_) -> undefined. install_dir(type) -> string(); -install_dir(nullable) -> true; +install_dir(required) -> false; install_dir(default) -> "plugins"; %% runner's root dir install_dir(T) when T =/= desc -> undefined; install_dir(desc) -> """ diff --git a/apps/emqx_prometheus/rebar.config b/apps/emqx_prometheus/rebar.config index c8b94178a..96afa679a 100644 --- a/apps/emqx_prometheus/rebar.config +++ b/apps/emqx_prometheus/rebar.config @@ -4,7 +4,7 @@ [ {emqx, {path, "../emqx"}}, %% FIXME: tag this as v3.1.3 {prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}}, - {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.24.0"}}} + {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.25.0"}}} ]}. {edoc_opts, [{preprocess, true}]}. diff --git a/apps/emqx_psk/src/emqx_psk_schema.erl b/apps/emqx_psk/src/emqx_psk_schema.erl index e9a71e35b..a8a870ea1 100644 --- a/apps/emqx_psk/src/emqx_psk_schema.erl +++ b/apps/emqx_psk/src/emqx_psk_schema.erl @@ -60,7 +60,7 @@ init_file(desc) -> "The file has to be structured line-by-line, each line must be in ", "the format of PSKIdentity:SharedSecret. For example: ", "mydevice1:c2VjcmV0">>; -init_file(nullable) -> true; +init_file(required) -> false; init_file(_) -> undefined. separator(type) -> binary(); diff --git a/apps/emqx_resource/test/emqx_test_resource.erl b/apps/emqx_resource/test/emqx_test_resource.erl index 1849e329b..b68fba1c1 100644 --- a/apps/emqx_resource/test/emqx_test_resource.erl +++ b/apps/emqx_resource/test/emqx_test_resource.erl @@ -35,11 +35,11 @@ roots() -> [{name, fun name/1}, {register, fun register/1}]. name(type) -> atom(); -name(nullable) -> false; +name(required) -> true; name(_) -> undefined. register(type) -> boolean(); -register(nullable) -> false; +register(required) -> true; register(default) -> false; register(_) -> undefined. diff --git a/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl index 0b9063fd1..66064ac24 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl @@ -54,8 +54,8 @@ fields("rule_info") -> %% TODO: we can delete this API if the Dashboard not denpends on it fields("rule_events") -> - ETopics = [emqx_rule_events:event_topic(E) || E <- emqx_rule_events:event_names()], - [ {"event", sc(hoconsc:enum(ETopics), #{desc => "The event topics", nullable => false})} + ETopics = [binary_to_atom(emqx_rule_events:event_topic(E)) || E <- emqx_rule_events:event_names()], + [ {"event", sc(hoconsc:enum(ETopics), #{desc => "The event topics", required => true})} , {"title", sc(binary(), #{desc => "The title", example => "some title"})} , {"description", sc(binary(), #{desc => "The description", example => "some desc"})} , {"columns", sc(map(), #{desc => "The columns"})} @@ -75,7 +75,7 @@ fields("rule_test") -> ]), #{desc => "The context of the event for testing", default => #{}})} - , {"sql", sc(binary(), #{desc => "The SQL of the rule for testing", nullable => false})} + , {"sql", sc(binary(), #{desc => "The SQL of the rule for testing", required => true})} ]; fields("metrics") -> @@ -121,7 +121,7 @@ fields("node_metrics") -> ] ++ fields("metrics"); fields("ctx_pub") -> - [ {"event_type", sc(message_publish, #{desc => "Event Type", nullable => false})} + [ {"event_type", sc(message_publish, #{desc => "Event Type", required => true})} , {"id", sc(binary(), #{desc => "Message ID"})} , {"clientid", sc(binary(), #{desc => "The Client ID"})} , {"username", sc(binary(), #{desc => "The User Name"})} @@ -133,7 +133,7 @@ fields("ctx_pub") -> ] ++ [qos()]; fields("ctx_sub") -> - [ {"event_type", sc(session_subscribed, #{desc => "Event Type", nullable => false})} + [ {"event_type", sc(session_subscribed, #{desc => "Event Type", required => true})} , {"clientid", sc(binary(), #{desc => "The Client ID"})} , {"username", sc(binary(), #{desc => "The User Name"})} , {"payload", sc(binary(), #{desc => "The Message Payload"})} @@ -144,11 +144,11 @@ fields("ctx_sub") -> ] ++ [qos()]; fields("ctx_unsub") -> - [{"event_type", sc(session_unsubscribed, #{desc => "Event Type", nullable => false})}] ++ + [{"event_type", sc(session_unsubscribed, #{desc => "Event Type", required => true})}] ++ proplists:delete("event_type", fields("ctx_sub")); fields("ctx_delivered") -> - [ {"event_type", sc(message_delivered, #{desc => "Event Type", nullable => false})} + [ {"event_type", sc(message_delivered, #{desc => "Event Type", required => true})} , {"id", sc(binary(), #{desc => "Message ID"})} , {"from_clientid", sc(binary(), #{desc => "The Client ID"})} , {"from_username", sc(binary(), #{desc => "The User Name"})} @@ -162,11 +162,11 @@ fields("ctx_delivered") -> ] ++ [qos()]; fields("ctx_acked") -> - [{"event_type", sc(message_acked, #{desc => "Event Type", nullable => false})}] ++ + [{"event_type", sc(message_acked, #{desc => "Event Type", required => true})}] ++ proplists:delete("event_type", fields("ctx_delivered")); fields("ctx_dropped") -> - [ {"event_type", sc(message_dropped, #{desc => "Event Type", nullable => false})} + [ {"event_type", sc(message_dropped, #{desc => "Event Type", required => true})} , {"id", sc(binary(), #{desc => "Message ID"})} , {"reason", sc(binary(), #{desc => "The Reason for Dropping"})} , {"clientid", sc(binary(), #{desc => "The Client ID"})} @@ -179,7 +179,7 @@ fields("ctx_dropped") -> ] ++ [qos()]; fields("ctx_connected") -> - [ {"event_type", sc(client_connected, #{desc => "Event Type", nullable => false})} + [ {"event_type", sc(client_connected, #{desc => "Event Type", required => true})} , {"clientid", sc(binary(), #{desc => "The Client ID"})} , {"username", sc(binary(), #{desc => "The User Name"})} , {"mountpoint", sc(binary(), #{desc => "The Mountpoint"})} @@ -196,7 +196,7 @@ fields("ctx_connected") -> ]; fields("ctx_disconnected") -> - [ {"event_type", sc(client_disconnected, #{desc => "Event Type", nullable => false})} + [ {"event_type", sc(client_disconnected, #{desc => "Event Type", required => true})} , {"clientid", sc(binary(), #{desc => "The Client ID"})} , {"username", sc(binary(), #{desc => "The User Name"})} , {"reason", sc(binary(), #{desc => "The Reason for Disconnect"})} @@ -211,7 +211,7 @@ qos() -> rule_id() -> {"id", sc(binary(), - #{ desc => "The ID of the rule", nullable => false + #{ desc => "The ID of the rule", required => true , example => "293fb66f" })}. 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 0946ed755..de04ee736 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -46,7 +46,7 @@ SQL query to transform the messages.
Example: SELECT * FROM \"test/topic\" WHERE payload.x = 1
""" , example => "SELECT * FROM \"test/topic\" WHERE payload.x = 1" - , nullable => false + , required => true , validator => fun ?MODULE:validate_sql/1 })} , {"outputs", sc(hoconsc:array(hoconsc:union(outputs())), @@ -143,7 +143,7 @@ fields("republish_args") -> The target topic of message to be re-published.
Template with variables is allowed, see description of the 'republish_args'. """ - , nullable => false + , required => true , example => <<"a/1">> })} , {qos, sc(qos(), @@ -182,7 +182,7 @@ rule_name() -> {"name", sc(binary(), #{ desc => "The name of the rule" , default => "" - , nullable => false + , required => true , example => "foo" })}. diff --git a/apps/emqx_statsd/src/emqx_statsd_schema.erl b/apps/emqx_statsd/src/emqx_statsd_schema.erl index 76725226a..09522870d 100644 --- a/apps/emqx_statsd/src/emqx_statsd_schema.erl +++ b/apps/emqx_statsd/src/emqx_statsd_schema.erl @@ -33,19 +33,19 @@ namespace() -> "statsd". roots() -> ["statsd"]. fields("statsd") -> - [ {enable, hoconsc:mk(boolean(), #{default => false, nullable => false})} + [ {enable, hoconsc:mk(boolean(), #{default => false, required => true})} , {server, fun server/1} , {sample_time_interval, fun duration_ms/1} , {flush_time_interval, fun duration_ms/1} ]. server(type) -> emqx_schema:ip_port(); -server(nullable) -> false; +server(required) -> true; server(default) -> "127.0.0.1:8125"; server(_) -> undefined. duration_ms(type) -> emqx_schema:duration_ms(); -duration_ms(nullable) -> false; +duration_ms(required) -> true; duration_ms(default) -> "10s"; duration_ms(_) -> undefined. diff --git a/mix.exs b/mix.exs index df0d0f3a3..aae205eec 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do # in conflict by emqtt and hocon {:getopt, "1.0.2", override: true}, {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "0.18.0", override: true}, - {:hocon, github: "emqx/hocon", tag: "0.24.0", override: true}, + {:hocon, github: "emqx/hocon", tag: "0.25.0", override: true}, {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.4.1", override: true}, {:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, diff --git a/rebar.config b/rebar.config index 374732362..109a4b0df 100644 --- a/rebar.config +++ b/rebar.config @@ -66,7 +66,7 @@ , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.2"}}} , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.24.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.25.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.1"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}