emqx_hocon.erl 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. %% @doc HOCON schema help module
  17. -module(emqx_hocon).
  18. -export([
  19. format_path/1,
  20. check/2,
  21. check/3,
  22. compact_errors/2,
  23. format_error/1,
  24. format_error/2,
  25. make_schema/1,
  26. load_and_check/2
  27. ]).
  28. %% @doc Format hocon config field path to dot-separated string in iolist format.
  29. -spec format_path([atom() | string() | binary()]) -> iolist().
  30. format_path([]) -> "";
  31. format_path([Name]) -> iol(Name);
  32. format_path([Name | Rest]) -> [iol(Name), "." | format_path(Rest)].
  33. %% @doc Plain check the input config.
  34. %% The input can either be `richmap' or plain `map'.
  35. %% Always return plain map with atom keys.
  36. -spec check(module(), hocon:config() | iodata()) ->
  37. {ok, hocon:config()} | {error, any()}.
  38. check(SchemaModule, Conf) ->
  39. %% TODO: remove required
  40. %% fields should state required or not in their schema
  41. Opts = #{atom_key => true, required => false},
  42. check(SchemaModule, Conf, Opts).
  43. check(SchemaModule, Conf, Opts) when is_map(Conf) ->
  44. try
  45. {ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)}
  46. catch
  47. throw:Errors:Stacktrace ->
  48. compact_errors(Errors, Stacktrace)
  49. end;
  50. check(SchemaModule, HoconText, Opts) ->
  51. case hocon:binary(HoconText, #{format => map}) of
  52. {ok, MapConfig} ->
  53. check(SchemaModule, MapConfig, Opts);
  54. {error, Reason} ->
  55. {error, Reason}
  56. end.
  57. %% @doc Check if the error error term is a hocon check error.
  58. %% Return {true, FirstError}, otherwise false.
  59. %% NOTE: Hocon tries to be comprehensive, so it returns all found errors
  60. -spec format_error(term()) -> {ok, binary()} | false.
  61. format_error(X) ->
  62. format_error(X, #{}).
  63. format_error({_Schema, [#{kind := K} = First | Rest] = All}, Opts) when
  64. K =:= validation_error orelse K =:= translation_error
  65. ->
  66. Update =
  67. case maps:get(no_stacktrace, Opts, false) of
  68. true ->
  69. fun no_stacktrace/1;
  70. false ->
  71. fun(X) -> X end
  72. end,
  73. case Rest of
  74. [] ->
  75. {ok, emqx_logger_jsonfmt:best_effort_json(Update(First), [])};
  76. _ ->
  77. {ok, emqx_logger_jsonfmt:best_effort_json(lists:map(Update, All), [])}
  78. end;
  79. format_error(_Other, _) ->
  80. false.
  81. make_schema(Fields) ->
  82. #{roots => Fields, fields => #{}}.
  83. %% Ensure iolist()
  84. iol(B) when is_binary(B) -> B;
  85. iol(A) when is_atom(A) -> atom_to_binary(A, utf8);
  86. iol(L) when is_list(L) -> L.
  87. no_stacktrace(Map) ->
  88. maps:without([stacktrace], Map).
  89. %% @doc HOCON tries to be very informative about all the detailed errors
  90. %% it's maybe too much when reporting to the user
  91. -spec compact_errors(any(), Stacktrace :: list()) -> {error, any()}.
  92. compact_errors({SchemaModule, Errors}, Stacktrace) ->
  93. compact_errors(SchemaModule, Errors, Stacktrace);
  94. compact_errors(ErrorContext0, _Stacktrace) when is_map(ErrorContext0) ->
  95. case ErrorContext0 of
  96. #{exception := #{schema_module := _Mod, message := _Msg} = Detail} ->
  97. Error0 = maps:remove(exception, ErrorContext0),
  98. Error = maps:merge(Error0, Detail),
  99. {error, Error};
  100. _ ->
  101. {error, ErrorContext0}
  102. end.
  103. compact_errors(SchemaModule, [Error0 | More], _Stacktrace) when is_map(Error0) ->
  104. Error1 =
  105. case length(More) of
  106. 0 ->
  107. Error0;
  108. N ->
  109. Error0#{unshown_errors_count => N}
  110. end,
  111. Error =
  112. case is_atom(SchemaModule) of
  113. true ->
  114. Error1#{schema_module => SchemaModule};
  115. false ->
  116. Error1
  117. end,
  118. {error, Error};
  119. compact_errors(SchemaModule, Error, Stacktrace) ->
  120. %% unexpected, we need the stacktrace reported
  121. %% if this happens it's a bug in hocon_tconf
  122. {error, #{
  123. schema_module => SchemaModule,
  124. exception => Error,
  125. stacktrace => Stacktrace
  126. }}.
  127. %% @doc This is only used in static check scripts in the CI.
  128. -spec load_and_check(module(), filename:filename_all()) -> {ok, term()} | {error, any()}.
  129. load_and_check(SchemaModule, File) ->
  130. try
  131. do_load_and_check(SchemaModule, File)
  132. catch
  133. throw:Reason:Stacktrace ->
  134. compact_errors(Reason, Stacktrace)
  135. end.
  136. do_load_and_check(SchemaModule, File) ->
  137. case hocon:load(File, #{format => map}) of
  138. {ok, Conf} ->
  139. Opts = #{atom_key => false, required => false},
  140. %% here we check only the provided root keys
  141. %% because the examples are all provided only with one (or maybe two) roots
  142. %% and some roots have required fields.
  143. RootKeys = maps:keys(Conf),
  144. {ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts, RootKeys)};
  145. {error, {parse_error, Reason}} ->
  146. {error, Reason};
  147. {error, Reason} ->
  148. {error, Reason}
  149. end.