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"}}}