• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emqx / emqx / 8684992748

15 Apr 2024 07:16AM UTC coverage: 67.831% (+5.4%) from 62.388%
8684992748

push

github

web-flow
Merge pull request #12877 from id/0415-sync-release-56

sync release 56

29 of 40 new or added lines in 7 files covered. (72.5%)

129 existing lines in 17 files now uncovered.

37939 of 55932 relevant lines covered (67.83%)

7734.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.49
/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
1
%%--------------------------------------------------------------------
2
%% Copyright (c) 2021-2024 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

17
-module(emqx_dashboard_swagger).
18

19
-include_lib("typerefl/include/types.hrl").
20
-include_lib("hocon/include/hoconsc.hrl").
21

22
-define(BASE_PATH, "/api/v5").
23

24
%% API
25
-export([spec/1, spec/2]).
26
-export([namespace/0, namespace/1, fields/1]).
27
-export([schema_with_example/2, schema_with_examples/2]).
28
-export([error_codes/1, error_codes/2]).
29
-export([file_schema/1]).
30
-export([base_path/0]).
31
-export([relative_uri/1, get_relative_uri/1]).
32
-export([compose_filters/2]).
33

34
-export([
35
    filter_check_request/2,
36
    filter_check_request_and_translate_body/2,
37
    gen_api_schema_json_iodata/3
38
]).
39

40
-ifdef(TEST).
41
-export([
42
    parse_spec_ref/3,
43
    components/2
44
]).
45
-endif.
46

47
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
48

49
-define(DEFAULT_FIELDS, [
50
    example,
51
    allowReserved,
52
    style,
53
    format,
54
    readOnly,
55
    explode,
56
    maxLength,
57
    allowEmptyValue,
58
    deprecated,
59
    minimum,
60
    maximum
61
]).
62

63
-define(INIT_SCHEMA, #{
64
    fields => #{},
65
    translations => #{},
66
    validations => [],
67
    namespace => undefined
68
}).
69

70
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
71
-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
72
    iolist_to_binary([
73
        <<"#/components/schemas/">>,
74
        ?TO_REF(namespace(_M_), _F_)
75
    ])
76
).
77
-define(TO_COMPONENTS_PARAM(_M_, _F_),
78
    iolist_to_binary([
79
        <<"#/components/parameters/">>,
80
        ?TO_REF(namespace(_M_), _F_)
81
    ])
82
).
83

84
-define(NO_I18N, undefined).
85

86
-define(MAX_ROW_LIMIT, 10000).
87
-define(DEFAULT_ROW, 100).
88

89
-type request() :: #{bindings => map(), query_string => map(), body => map()}.
90
-type request_meta() :: #{module => module(), path => string(), method => atom()}.
91

92
%% More exact types are defined in minirest.hrl, but we don't want to include it
93
%% because it defines a lot of types and they may clash with the types declared locally.
94
-type status_code() :: pos_integer().
95
-type error_code() :: atom() | binary().
96
-type error_message() :: binary().
97
-type response_body() :: term().
98
-type headers() :: map().
99

100
-type response() ::
101
    status_code()
102
    | {status_code()}
103
    | {status_code(), response_body()}
104
    | {status_code(), headers(), response_body()}
105
    | {status_code(), error_code(), error_message()}.
106

107
-type filter_result() :: {ok, request()} | response().
108
-type filter() :: emqx_maybe:t(fun((request(), request_meta()) -> filter_result())).
109

110
-type spec_opts() :: #{
111
    check_schema => boolean() | filter(),
112
    translate_body => boolean(),
113
    schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map()),
114
    i18n_lang => atom() | string() | binary(),
115
    filter => filter()
116
}.
117

118
-type route_path() :: string() | binary().
119
-type route_methods() :: map().
120
-type route_handler() :: atom().
121
-type route_options() :: #{filter => filter()}.
122

123
-type api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}.
124
-type api_spec_component() :: map().
125

126
%%------------------------------------------------------------------------------
127
%% API
128
%%------------------------------------------------------------------------------
129

130
%% @equiv spec(Module, #{check_schema => false})
131
-spec spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}.
132
spec(Module) -> spec(Module, #{check_schema => false}).
106✔
133

134
-spec spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}.
135
spec(Module, Options) ->
136
    Paths = apply(Module, paths, []),
5,363✔
137
    {ApiSpec, AllRefs} =
5,363✔
138
        lists:foldl(
139
            fun(Path, {AllAcc, AllRefsAcc}) ->
140
                {OperationId, Specs, Refs, RouteOpts} = parse_spec_ref(Module, Path, Options),
23,399✔
141
                {
23,399✔
142
                    [{filename:join("/", Path), Specs, OperationId, RouteOpts} | AllAcc],
143
                    Refs ++ AllRefsAcc
144
                }
145
            end,
146
            {[], []},
147
            Paths
148
        ),
149
    {ApiSpec, components(lists:usort(AllRefs), Options)}.
5,363✔
150

151
-spec namespace() -> hocon_schema:name().
152
namespace() -> "public".
10,442✔
153

154
-spec fields(hocon_schema:name()) -> hocon_schema:fields().
155
fields(page) ->
156
    Desc = <<"Page number of the results to fetch.">>,
2,864✔
157
    Meta = #{in => query, desc => Desc, default => 1, example => 1},
2,864✔
158
    [{page, hoconsc:mk(pos_integer(), Meta)}];
2,864✔
159
fields(limit) ->
160
    Desc = iolist_to_binary([
3,003✔
161
        <<"Results per page(max ">>,
162
        integer_to_binary(?MAX_ROW_LIMIT),
163
        <<")">>
164
    ]),
165
    Meta = #{in => query, desc => Desc, default => ?DEFAULT_ROW, example => 50},
3,003✔
166
    [{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}];
3,003✔
167
fields(cursor) ->
168
    Desc = <<"Opaque value representing the current iteration state.">>,
205✔
169
    Meta = #{default => none, in => query, desc => Desc},
205✔
170
    [{cursor, hoconsc:mk(hoconsc:union([none, binary()]), Meta)}];
205✔
171
fields(cursor_response) ->
172
    Desc = <<"Opaque value representing the current iteration state.">>,
174✔
173
    Meta = #{desc => Desc, required => false},
174✔
174
    [{cursor, hoconsc:mk(binary(), Meta)}];
174✔
175
fields(count) ->
176
    Desc = <<
1,407✔
177
        "Total number of records matching the query.<br/>"
178
        "Note: this field is present only if the query can be optimized and does "
179
        "not require a full table scan."
180
    >>,
181
    Meta = #{desc => Desc, required => false},
1,407✔
182
    [{count, hoconsc:mk(non_neg_integer(), Meta)}];
1,407✔
183
fields(hasnext) ->
184
    Desc = <<
1,407✔
185
        "Flag indicating whether there are more results available on next pages."
186
    >>,
187
    Meta = #{desc => Desc, required => true},
1,407✔
188
    [{hasnext, hoconsc:mk(boolean(), Meta)}];
1,407✔
189
fields(position) ->
190
    Desc = <<
568✔
191
        "An opaque token that can then be in subsequent requests to get "
192
        " the next chunk of results: \"?position={prev_response.meta.position}\"<br/>"
193
        "It is used instead of \"page\" parameter to traverse highly volatile data.<br/>"
194
        "Can be omitted or set to \"none\" to get the first chunk of data."
195
    >>,
196
    Meta = #{
568✔
197
        in => query, desc => Desc, required => false, example => <<"none">>
198
    },
199
    [{position, hoconsc:mk(hoconsc:union([none, end_of_data, binary()]), Meta)}];
568✔
200
fields(start) ->
201
    Desc = <<"The position of the current first element of the data collection.">>,
348✔
202
    Meta = #{
348✔
203
        desc => Desc, required => true, example => <<"none">>
204
    },
205
    [{start, hoconsc:mk(hoconsc:union([none, binary()]), Meta)}];
348✔
206
fields(meta) ->
207
    fields(page) ++ fields(limit) ++ fields(count) ++ fields(hasnext);
1,233✔
208
fields(meta_with_cursor) ->
209
    fields(count) ++ fields(hasnext) ++ fields(cursor_response);
174✔
210
fields(continuation_meta) ->
211
    fields(start) ++ fields(position).
348✔
212

213
-spec schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema().
214
schema_with_example(Type, Example) ->
215
    hoconsc:mk(Type, #{examples => #{<<"example">> => Example}}).
6,218✔
216

217
-spec schema_with_examples(hocon_schema:type(), map() | list(tuple())) ->
218
    hocon_schema:field_schema().
219
schema_with_examples(Type, Examples) ->
220
    hoconsc:mk(Type, #{examples => #{<<"examples">> => Examples}}).
8,191✔
221

222
-spec error_codes(list(atom())) -> hocon_schema:fields().
223
error_codes(Codes) ->
224
    error_codes(Codes, <<"Error code to troubleshoot problems.">>).
8,006✔
225

226
-spec error_codes(nonempty_list(atom()), binary() | {desc, module(), term()}) ->
227
    hocon_schema:fields().
228
error_codes(Codes = [_ | _], MsgDesc) ->
229
    [
43,366✔
230
        {code, hoconsc:mk(hoconsc:enum(Codes))},
231
        {message,
232
            hoconsc:mk(string(), #{
233
                desc => MsgDesc
234
            })}
235
    ].
236

237
-spec base_path() -> uri_string:uri_string().
238
base_path() ->
239
    ?BASE_PATH.
668✔
240

241
-spec relative_uri(uri_string:uri_string()) -> uri_string:uri_string().
242
relative_uri(Uri) ->
243
    base_path() ++ Uri.
278✔
244

245
-spec get_relative_uri(uri_string:uri_string()) -> {ok, uri_string:uri_string()} | error.
246
get_relative_uri(<<?BASE_PATH, Path/binary>>) ->
247
    {ok, Path};
3,315✔
248
get_relative_uri(_Path) ->
249
    error.
×
250

251
file_schema(FileName) ->
252
    #{
290✔
253
        content => #{
254
            'multipart/form-data' => #{
255
                schema => #{
256
                    type => object,
257
                    properties => #{
258
                        FileName => #{type => string, format => binary}
259
                    }
260
                }
261
            }
262
        }
263
    }.
264

265
gen_api_schema_json_iodata(SchemaMod, SchemaInfo, Converter) ->
266
    {ApiSpec0, Components0} = spec(
8✔
267
        SchemaMod,
268
        #{
269
            schema_converter => Converter,
270
            i18n_lang => ?NO_I18N
271
        }
272
    ),
273
    ApiSpec = lists:foldl(
8✔
274
        fun({Path, Spec, _, _}, Acc) ->
275
            NewSpec = maps:fold(
97✔
276
                fun(Method, #{responses := Responses}, SubAcc) ->
277
                    case Responses of
137✔
278
                        #{
279
                            <<"200">> :=
280
                                #{
281
                                    <<"content">> := #{
282
                                        <<"application/json">> := #{<<"schema">> := Schema}
283
                                    }
284
                                }
285
                        } ->
286
                            SubAcc#{Method => Schema};
64✔
287
                        _ ->
288
                            SubAcc
73✔
289
                    end
290
                end,
291
                #{},
292
                Spec
293
            ),
294
            Acc#{list_to_atom(Path) => NewSpec}
97✔
295
        end,
296
        #{},
297
        ApiSpec0
298
    ),
299
    Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
8✔
300
    emqx_utils_json:encode(
8✔
301
        #{
302
            info => SchemaInfo,
303
            paths => ApiSpec,
304
            components => #{schemas => Components}
305
        },
306
        [pretty, force_utf8]
307
    ).
308

309
-spec compose_filters(filter(), filter()) -> filter().
310
compose_filters(undefined, Filter2) ->
311
    Filter2;
4,912✔
312
compose_filters(Filter1, undefined) ->
313
    Filter1;
41,656✔
314
compose_filters(Filter1, Filter2) ->
315
    fun(Request, RequestMeta) ->
300✔
316
        case Filter1(Request, RequestMeta) of
49✔
317
            {ok, Request1} ->
318
                Filter2(Request1, RequestMeta);
46✔
319
            Response ->
320
                Response
3✔
321
        end
322
    end.
323

324
%%------------------------------------------------------------------------------
325
%% Private functions
326
%%------------------------------------------------------------------------------
327

328
filter_check_request_and_translate_body(Request, RequestMeta) ->
329
    translate_req(Request, RequestMeta, fun check_and_translate/3).
860✔
330

331
filter_check_request(Request, RequestMeta) ->
332
    translate_req(Request, RequestMeta, fun check_only/3).
1,652✔
333

334
translate_req(Request, #{module := Module, path := Path, method := Method}, CheckFun) ->
335
    #{Method := Spec} = apply(Module, schema, [Path]),
2,512✔
336
    try
2,512✔
337
        Params = maps:get(parameters, Spec, []),
2,512✔
338
        Body = maps:get('requestBody', Spec, []),
2,512✔
339
        {Bindings, QueryStr} = check_parameters(Request, Params, Module),
2,512✔
340
        NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
2,447✔
341
        {ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
2,380✔
342
    catch
343
        throw:HoconError ->
344
            Msg = hocon_error_msg(HoconError),
132✔
345
            {400, 'BAD_REQUEST', Msg}
132✔
346
    end.
347

348
check_and_translate(Schema, Map, Opts) ->
349
    hocon_tconf:check_plain(Schema, Map, Opts).
1,019✔
350

351
check_only(Schema, Map, Opts) ->
352
    _ = hocon_tconf:check_plain(Schema, Map, Opts),
738✔
353
    Map.
684✔
354

355
filter(Options) ->
356
    CheckSchemaFilter = check_schema_filter(Options),
23,434✔
357
    CustomFilter = custom_filter(Options),
23,434✔
358
    compose_filters(CheckSchemaFilter, CustomFilter).
23,434✔
359

360
custom_filter(Options) ->
361
    maps:get(filter, Options, undefined).
46,868✔
362

363
check_schema_filter(#{check_schema := true, translate_body := true}) ->
364
    fun ?MODULE:filter_check_request_and_translate_body/2;
7,084✔
365
check_schema_filter(#{check_schema := true}) ->
366
    fun ?MODULE:filter_check_request/2;
13,886✔
367
check_schema_filter(#{check_schema := Filter}) when is_function(Filter, 2) ->
368
    Filter;
8✔
369
check_schema_filter(_) ->
370
    undefined.
2,456✔
371

372
parse_spec_ref(Module, Path, Options) ->
373
    Schema =
23,439✔
374
        try
375
            erlang:apply(Module, schema, [Path])
23,439✔
376
        catch
377
            Error:Reason:Stacktrace ->
378
                failed_to_generate_swagger_spec(Module, Path, Error, Reason, Stacktrace)
2✔
379
        end,
380
    OperationId = maps:get('operationId', Schema),
23,437✔
381
    {Specs, Refs} = maps:fold(
23,437✔
382
        fun(Method, Meta, {Acc, RefsAcc}) ->
383
            (not lists:member(Method, ?METHODS)) andalso
33,076✔
384
                throw({error, #{module => Module, path => Path, method => Method}}),
1✔
385
            {Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
33,075✔
386
            {Acc#{Method => Spec}, SubRefs ++ RefsAcc}
33,073✔
387
        end,
388
        {#{}, []},
389
        maps:without(['operationId', 'filter'], Schema)
390
    ),
391
    RouteOpts = generate_route_opts(Schema, Options),
23,434✔
392
    {OperationId, Specs, Refs, RouteOpts}.
23,434✔
393

394
-ifdef(TEST).
395
-spec failed_to_generate_swagger_spec(_, _, _, _, _) -> no_return().
396
failed_to_generate_swagger_spec(Module, Path, Error, Reason, Stacktrace) ->
397
    error({failed_to_generate_swagger_spec, Module, Path, Error, Reason, Stacktrace}).
2✔
398
-else.
399
-spec failed_to_generate_swagger_spec(_, _, _, _, _) -> no_return().
400
failed_to_generate_swagger_spec(Module, Path, Error, Reason, Stacktrace) ->
401
    %% This error is intended to fail the build
402
    %% hence print to standard_error
403
    io:format(
404
        standard_error,
405
        "Failed to generate swagger for path ~p in module ~p~n"
406
        "error:~p~nreason:~p~n~p~n",
407
        [Module, Path, Error, Reason, Stacktrace]
408
    ),
409
    error({failed_to_generate_swagger_spec, Module, Path}).
410

411
-endif.
412
generate_route_opts(Schema, Options) ->
413
    #{filter => compose_filters(filter(Options), custom_filter(Schema))}.
23,434✔
414

415
check_parameters(Request, Spec, Module) ->
416
    #{bindings := Bindings, query_string := QueryStr} = Request,
2,512✔
417
    BindingsBin = maps:fold(
2,512✔
418
        fun(Key, Value, Acc) ->
419
            Acc#{atom_to_binary(Key) => Value}
1,223✔
420
        end,
421
        #{},
422
        Bindings
423
    ),
424
    check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
2,512✔
425

426
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
427
    check_parameter(
249✔
428
        [?R_REF(LocalMod, Fields) | Spec],
429
        Bindings,
430
        QueryStr,
431
        LocalMod,
432
        BindingsAcc,
433
        QueryStrAcc
434
    );
435
check_parameter(
436
    [?R_REF(Module, Fields) | Spec],
437
    Bindings,
438
    QueryStr,
439
    LocalMod,
440
    BindingsAcc,
441
    QueryStrAcc
442
) ->
443
    Params = apply(Module, fields, [Fields]),
1,144✔
444
    check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
1,144✔
445
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) ->
446
    {NewBindings, NewQueryStr};
2,447✔
447
check_parameter([{Name, Type} | Spec], Bindings, QueryStr, Module, BindingsAcc, QueryStrAcc) ->
448
    case hocon_schema:field_schema(Type, in) of
4,341✔
449
        path ->
450
            Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
1,220✔
451
            Option = #{atom_key => true},
1,220✔
452
            NewBindings = hocon_tconf:check_plain(Schema, Bindings, Option),
1,220✔
453
            NewBindingsAcc = maps:merge(BindingsAcc, NewBindings),
1,196✔
454
            check_parameter(Spec, Bindings, QueryStr, Module, NewBindingsAcc, QueryStrAcc);
1,196✔
455
        query ->
456
            Type1 = maybe_wrap_array_qs_param(Type),
3,121✔
457
            Schema = ?INIT_SCHEMA#{roots => [{Name, Type1}]},
3,121✔
458
            Option = #{},
3,121✔
459
            NewQueryStr = hocon_tconf:check_plain(Schema, QueryStr, Option),
3,121✔
460
            NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
3,080✔
461
            check_parameter(Spec, Bindings, QueryStr, Module, BindingsAcc, NewQueryStrAcc)
3,080✔
462
    end.
463

464
%% Compatibility layer for minirest 1.4.0 that parses repetitive QS params into lists.
465
%% Previous minirest releases dropped all but the last repetitive params.
466

467
maybe_wrap_array_qs_param(FieldSchema) ->
468
    Conv = hocon_schema:field_schema(FieldSchema, converter),
3,121✔
469
    Type = hocon_schema:field_schema(FieldSchema, type),
3,121✔
470
    case array_or_single_qs_param(Type, Conv) of
3,121✔
471
        any ->
472
            FieldSchema;
135✔
473
        array ->
474
            override_conv(FieldSchema, fun wrap_array_conv/2, Conv);
270✔
475
        single ->
476
            override_conv(FieldSchema, fun unwrap_array_conv/2, Conv)
2,716✔
477
    end.
478

479
array_or_single_qs_param(?ARRAY(_Type), undefined) ->
480
    array;
270✔
481
%% Qs field schema is an array and defines a converter:
482
%% don't change (wrap/unwrap) the original value, and let the converter handle it.
483
%% For example, it can be a CSV list.
484
array_or_single_qs_param(?ARRAY(_Type), _Conv) ->
485
    any;
×
486
array_or_single_qs_param(?UNION(Types), _Conv) ->
487
    HasArray = lists:any(
213✔
488
        fun
489
            (?ARRAY(_)) -> true;
135✔
490
            (_) -> false
337✔
491
        end,
492
        Types
493
    ),
494
    case HasArray of
213✔
495
        true -> any;
135✔
496
        false -> single
78✔
497
    end;
498
array_or_single_qs_param(_, _Conv) ->
499
    single.
2,638✔
500

501
override_conv(FieldSchema, NewConv, OldConv) ->
502
    Conv = compose_converters(NewConv, OldConv),
2,986✔
503
    hocon_schema:override(FieldSchema, FieldSchema#{converter => Conv}).
2,986✔
504

505
compose_converters(NewFun, undefined = _OldFun) ->
506
    NewFun;
2,986✔
507
compose_converters(NewFun, OldFun) ->
508
    case erlang:fun_info(OldFun, arity) of
×
509
        {_, 2} ->
510
            fun(V, Opts) -> OldFun(NewFun(V, Opts), Opts) end;
×
511
        {_, 1} ->
512
            fun(V, Opts) -> OldFun(NewFun(V, Opts)) end
×
513
    end.
514

515
wrap_array_conv(Val, _Opts) when is_list(Val); Val =:= undefined -> Val;
257✔
516
wrap_array_conv(SingleVal, _Opts) -> [SingleVal].
13✔
517

518
unwrap_array_conv([HVal | _], _Opts) -> HVal;
×
519
unwrap_array_conv(SingleVal, _Opts) -> SingleVal.
2,716✔
520

521
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
522
    Type0 = hocon_schema:field_schema(Schema, type),
1,014✔
523
    Type =
1,014✔
524
        case Type0 of
525
            ?REF(StructName) -> ?R_REF(Module, StructName);
47✔
526
            _ -> Type0
967✔
527
        end,
528
    NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]},
1,014✔
529
    Option = #{required => false},
1,014✔
530
    #{<<"root">> := NewBody} = CheckFun(NewSchema, #{<<"root">> => Body}, Option),
1,014✔
531
    NewBody;
956✔
532
%% TODO not support nest object check yet, please use ref!
533
%% 'requestBody' = [ {per_page, mk(integer(), #{}},
534
%%                 {nest_object, [
535
%%                   {good_nest_1, mk(integer(), #{})},
536
%%                   {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
537
%%                ]}
538
%% ]
539
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
540
    lists:foldl(
1,413✔
541
        fun({Name, Type}, Acc) ->
542
            Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
743✔
543
            maps:merge(Acc, CheckFun(Schema, Body, #{}))
743✔
544
        end,
545
        #{},
546
        Spec
547
    );
548
%% requestBody => #{content => #{ 'application/octet-stream' =>
549
%% #{schema => #{ type => string, format => binary}}}
550
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
551
    Body.
20✔
552

553
%% tags, description, summary, security, deprecated
554
meta_to_spec(Meta, Module, Options) ->
555
    {Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module, Options),
33,075✔
556
    {RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module, Options),
33,074✔
557
    {Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
33,074✔
558
    {
33,073✔
559
        generate_method_desc(to_spec(Meta, Params, RequestBody, Responses), Options),
560
        lists:usort(Refs1 ++ Refs2 ++ Refs3)
561
    }.
562

563
to_spec(Meta, Params, [], Responses) ->
564
    Spec = maps:without([parameters, 'requestBody', responses], Meta),
33,073✔
565
    Spec#{parameters => Params, responses => Responses};
33,073✔
566
to_spec(Meta, Params, RequestBody, Responses) ->
567
    Spec = to_spec(Meta, Params, [], Responses),
10,227✔
568
    maps:put('requestBody', RequestBody, Spec).
10,227✔
569

570
generate_method_desc(Spec = #{desc := _Desc}, Options) ->
571
    Spec1 = trans_description(maps:remove(desc, Spec), Spec, Options),
8,672✔
572
    trans_tags(Spec1);
8,672✔
573
generate_method_desc(Spec = #{description := _Desc}, Options) ->
574
    Spec1 = trans_description(Spec, Spec, Options),
24,280✔
575
    trans_tags(Spec1);
24,280✔
576
generate_method_desc(Spec, _Options) ->
577
    trans_tags(Spec).
121✔
578

579
trans_tags(Spec = #{tags := Tags}) ->
580
    Spec#{tags => [string:titlecase(to_bin(Tag)) || Tag <- Tags]};
32,799✔
581
trans_tags(Spec) ->
582
    Spec.
274✔
583

584
parameters(Params, Module, Options) ->
585
    {SpecList, AllRefs} =
38,296✔
586
        lists:foldl(
587
            fun(Param, {Acc, RefsAcc}) ->
588
                case Param of
43,192✔
589
                    ?REF(StructName) ->
590
                        to_ref(Module, StructName, Acc, RefsAcc);
4,459✔
591
                    ?R_REF(RModule, StructName) ->
592
                        to_ref(RModule, StructName, Acc, RefsAcc);
5,423✔
593
                    {Name, Type} ->
594
                        In = hocon_schema:field_schema(Type, in),
33,310✔
595
                        In =:= undefined andalso
33,310✔
596
                            throw({error, <<"missing in:path/query field in parameters">>}),
1✔
597
                        Required = hocon_schema:field_schema(Type, required),
33,309✔
598
                        Default = hocon_schema:field_schema(Type, default),
33,309✔
599
                        HoconType = hocon_schema:field_schema(Type, type),
33,309✔
600
                        SchemaExtras = hocon_extract_map([enum, default], Type),
33,309✔
601
                        Meta = init_meta(Default),
33,309✔
602
                        {ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
33,309✔
603
                        Schema = maps:merge(maps:merge(ParamType, Meta), SchemaExtras),
33,309✔
604
                        Spec0 = init_prop(
33,309✔
605
                            [required | ?DEFAULT_FIELDS],
606
                            #{schema => Schema, name => Name, in => In},
607
                            Type
608
                        ),
609
                        Spec1 = trans_required(Spec0, Required, In),
33,309✔
610
                        Spec2 = trans_description(Spec1, Type, Options),
33,309✔
611
                        {[Spec2 | Acc], Refs ++ RefsAcc}
33,309✔
612
                end
613
            end,
614
            {[], []},
615
            Params
616
        ),
617
    {lists:reverse(SpecList), AllRefs}.
38,295✔
618

619
hocon_extract_map(Keys, Type) ->
620
    lists:foldl(
33,309✔
621
        fun(K, M) ->
622
            case hocon_schema:field_schema(Type, K) of
66,618✔
623
                undefined -> M;
61,387✔
624
                V -> M#{K => V}
5,231✔
625
            end
626
        end,
627
        #{},
628
        Keys
629
    ).
630

631
init_meta(undefined) -> #{};
28,155✔
632
init_meta(Default) -> #{default => Default}.
5,154✔
633

634
init_prop(Keys, Init, Type) ->
635
    lists:foldl(
1,876,232✔
636
        fun(Key, Acc) ->
637
            case hocon_schema:field_schema(Type, Key) of
22,514,784✔
638
                undefined -> Acc;
21,189,093✔
639
                Schema -> Acc#{Key => format_prop(Key, Schema)}
1,325,691✔
640
            end
641
        end,
642
        Init,
643
        Keys
644
    ).
645

646
format_prop(deprecated, Value) when is_boolean(Value) -> Value;
2✔
647
format_prop(deprecated, _) -> true;
66,729✔
648
format_prop(default, []) -> [];
25,676✔
649
format_prop(_, Schema) -> to_bin(Schema).
1,233,284✔
650

651
trans_required(Spec, true, _) -> Spec#{required => true};
11,524✔
652
trans_required(Spec, _, path) -> Spec#{required => true};
6,331✔
653
trans_required(Spec, _, _) -> Spec.
15,454✔
654

655
trans_desc(Init, Hocon, Func, Name, Options) ->
656
    Spec0 = trans_description(Init, Hocon, Options),
1,842,923✔
657
    case Func =:= fun hocon_schema_to_spec/2 of
1,842,923✔
658
        true ->
659
            Spec0;
1,818,748✔
660
        false ->
661
            Spec1 = trans_label(Spec0, Hocon, Name, Options),
24,175✔
662
            case Spec1 of
24,175✔
663
                #{description := _} -> Spec1;
311✔
664
                _ -> Spec1#{description => <<Name/binary, " Description">>}
23,864✔
665
            end
666
    end.
667

668
trans_description(Spec, Hocon, Options) ->
669
    Desc =
1,967,581✔
670
        case desc_struct(Hocon) of
671
            undefined -> undefined;
172,963✔
672
            ?DESC(_, _) = Struct -> get_i18n(<<"desc">>, Struct, undefined, Options);
1,586,091✔
673
            Text -> to_bin(Text)
208,527✔
674
        end,
675
    case Desc =:= undefined of
1,967,581✔
676
        true ->
677
            Spec;
218,623✔
678
        false ->
679
            Desc1 = binary:replace(Desc, [<<"\n">>], <<"<br/>">>, [global]),
1,748,958✔
680
            Spec#{description => Desc1}
1,748,958✔
681
    end.
682

683
get_i18n(Tag, ?DESC(Namespace, Id), Default, Options) ->
684
    Lang = get_lang(Options),
1,608,722✔
685
    case Lang of
1,608,722✔
686
        ?NO_I18N ->
687
            undefined;
45,518✔
688
        _ ->
689
            get_i18n_text(Lang, Namespace, Id, Tag, Default)
1,563,204✔
690
    end.
691

692
get_i18n_text(Lang, Namespace, Id, Tag, Default) ->
693
    case emqx_dashboard_desc_cache:lookup(Lang, Namespace, Id, Tag) of
1,563,204✔
694
        undefined ->
695
            Default;
22,773✔
696
        Text ->
697
            Text
1,540,431✔
698
    end.
699

700
%% So far i18n_lang in options is only used at build time.
701
%% At runtime, it's still the global config which controls the language.
702
get_lang(#{i18n_lang := Lang}) -> Lang;
45,518✔
703
get_lang(_) -> emqx:get_config([dashboard, i18n_lang]).
1,563,204✔
704

705
trans_label(Spec, Hocon, Default, Options) ->
706
    Label =
24,175✔
707
        case desc_struct(Hocon) of
708
            ?DESC(_, _) = Struct -> get_i18n(<<"label">>, Struct, Default, Options);
22,631✔
709
            _ -> Default
1,544✔
710
        end,
711
    case Label =:= undefined of
24,175✔
712
        true ->
713
            Spec;
22,631✔
714
        false ->
715
            Spec#{label => Label}
1,544✔
716
    end.
717

718
desc_struct(Hocon) ->
719
    R =
1,991,756✔
720
        case hocon_schema:field_schema(Hocon, desc) of
721
            undefined ->
722
                case hocon_schema:field_schema(Hocon, description) of
203,349✔
723
                    undefined -> get_ref_desc(Hocon);
174,196✔
724
                    Struct1 -> Struct1
29,153✔
725
                end;
726
            Struct ->
727
                Struct
1,788,407✔
728
        end,
729
    ensure_bin(R).
1,991,756✔
730

731
ensure_bin(undefined) -> undefined;
174,196✔
732
ensure_bin(?DESC(_Namespace, _Id) = Desc) -> Desc;
1,608,722✔
733
ensure_bin(Text) -> to_bin(Text).
208,838✔
734

735
get_ref_desc(?R_REF(Mod, Name)) ->
736
    case erlang:function_exported(Mod, desc, 1) of
503✔
UNCOV
737
        true -> Mod:desc(Name);
×
738
        false -> undefined
503✔
739
    end;
740
get_ref_desc(_) ->
741
    undefined.
173,693✔
742

743
request_body(#{content := _} = Content, _Module, _Options) ->
744
    {Content, []};
624✔
745
request_body([], _Module, _Options) ->
746
    {[], []};
22,847✔
747
request_body(Schema, Module, Options) ->
748
    {{Props, Refs}, Examples} =
9,603✔
749
        case hoconsc:is_schema(Schema) of
750
            true ->
751
                HoconSchema = hocon_schema:field_schema(Schema, type),
7,880✔
752
                SchemaExamples = hocon_schema:field_schema(Schema, examples),
7,880✔
753
                {hocon_schema_to_spec(HoconSchema, Module), SchemaExamples};
7,880✔
754
            false ->
755
                {parse_object(Schema, Module, Options), undefined}
1,723✔
756
        end,
757
    {#{<<"content">> => content(Props, Examples)}, Refs}.
9,603✔
758

759
responses(Responses, Module, Options) ->
760
    {Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses),
33,074✔
761
    {Spec, Refs}.
33,073✔
762

763
response(Status, ?DESC(_Mod, _Id) = Schema, {Acc, RefsAcc, Module, Options}) ->
764
    Desc = trans_description(#{}, #{desc => Schema}, Options),
627✔
765
    {Acc#{integer_to_binary(Status) => Desc}, RefsAcc, Module, Options};
627✔
766
response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) ->
767
    {Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options};
10,797✔
768
%% Support swagger raw object(file download).
769
%% TODO: multi type response(i.e. Support both 'application/json' and 'plain/text')
770
response(Status, #{content := _} = Content, {Acc, RefsAcc, Module, Options}) ->
771
    {Acc#{integer_to_binary(Status) => Content}, RefsAcc, Module, Options};
457✔
772
response(Status, ?REF(StructName), {Acc, RefsAcc, Module, Options}) ->
773
    response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module, Options});
789✔
774
response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module, Options}) ->
775
    SchemaToSpec = schema_converter(Options),
2,661✔
776
    {Spec, Refs} = SchemaToSpec(RRef, Module),
2,661✔
777
    Content = content(Spec),
2,661✔
778
    {
2,661✔
779
        Acc#{
780
            integer_to_binary(Status) =>
781
                #{<<"content">> => Content}
782
        },
783
        Refs ++ RefsAcc,
784
        Module,
785
        Options
786
    };
787
response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
788
    case hoconsc:is_schema(Schema) of
57,771✔
789
        true ->
790
            Hocon = hocon_schema:field_schema(Schema, type),
16,428✔
791
            Examples = hocon_schema:field_schema(Schema, examples),
16,428✔
792
            {Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
16,428✔
793
            Init = trans_description(#{}, Schema, Options),
16,428✔
794
            Content = content(Spec, Examples),
16,428✔
795
            {
16,428✔
796
                Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}},
797
                Refs ++ RefsAcc,
798
                Module,
799
                Options
800
            };
801
        false ->
802
            {Props, Refs} = parse_object(Schema, Module, Options),
41,343✔
803
            Init = trans_description(#{}, Schema, Options),
41,342✔
804
            Content = Init#{<<"content">> => content(Props)},
41,342✔
805
            {Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
41,342✔
806
    end.
807

808
components(Refs, Options) ->
809
    lists:sort(
5,388✔
810
        maps:fold(
811
            fun(K, V, Acc) -> [#{K => V} | Acc] end,
103,185✔
812
            [],
813
            components(Options, Refs, #{}, [])
814
        )
815
    ).
816

817
components(_Options, [], SpecAcc, []) ->
818
    SpecAcc;
5,383✔
819
components(Options, [], SpecAcc, SubRefAcc) ->
820
    components(Options, SubRefAcc, SpecAcc, []);
4,769✔
821
components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
822
    Props = hocon_schema_fields(Module, Field),
210,619✔
823
    Namespace = namespace(Module),
210,619✔
824
    {Object, SubRefs} = parse_object(Props, Module, Options),
210,617✔
825
    NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Object},
210,614✔
826
    components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc);
210,614✔
827
%% parameters in ref only have one value, not array
828
components(Options, [{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) ->
829
    Props = hocon_schema_fields(Module, Field),
5,221✔
830
    {[Param], SubRefs} = parameters(Props, Module, Options),
5,221✔
831
    Namespace = namespace(Module),
5,221✔
832
    NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param},
5,221✔
833
    components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc).
5,221✔
834

835
hocon_schema_fields(Module, StructName) ->
836
    case apply(Module, fields, [StructName]) of
215,840✔
837
        #{fields := Fields, desc := _} ->
838
            %% evil here, as it's match hocon_schema's internal representation
839

840
            %% TODO: make use of desc ?
841
            Fields;
4✔
842
        Other ->
843
            Other
215,836✔
844
    end.
845

846
%% Semantic error at components.schemas.xxx:xx:xx
847
%% Component names can only contain the characters A-Z a-z 0-9 - . _
848
%% So replace ':' by '-'.
849
namespace(Module) ->
850
    case hocon_schema:namespace(Module) of
515,745✔
851
        undefined -> Module;
27,766✔
852
        NameSpace -> re:replace(to_bin(NameSpace), ":", "-", [global])
487,979✔
853
    end.
854

855
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
856
    {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
279,207✔
857
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
858
    {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, [{LocalModule, StructName}]};
8,663✔
859
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
860
    {typename_to_spec(lists:flatten(typerefl:name(Type)), LocalModule), []};
1,476,084✔
861
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
862
    {Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
117,564✔
863
    {#{type => array, items => Schema}, Refs};
117,563✔
864
hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
865
    {#{type => string, enum => Items}, []};
203,245✔
866
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
867
    {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
474✔
868
    {
474✔
869
        #{
870
            <<"type">> => object,
871
            <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}
872
        },
873
        SubRefs
874
    };
875
hocon_schema_to_spec(?UNION(Types, _DisplayName), LocalModule) ->
876
    {OneOf, Refs} = lists:foldl(
88,793✔
877
        fun(Type, {Acc, RefsAcc}) ->
878
            {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
290,611✔
879
            {[Schema | Acc], SubRefs ++ RefsAcc}
290,611✔
880
        end,
881
        {[], []},
882
        hoconsc:union_members(Types)
883
    ),
884
    {#{<<"oneOf">> => OneOf}, Refs};
88,793✔
885
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
886
    {#{type => string, enum => [Atom]}, []}.
113,641✔
887

888
typename_to_spec(TypeStr, Module) ->
889
    emqx_conf_schema_types:readable_swagger(Module, TypeStr).
1,476,084✔
890

891
to_bin(List) when is_list(List) ->
892
    case io_lib:printable_list(List) of
1,347,084✔
893
        true -> unicode:characters_to_binary(List);
1,316,971✔
894
        false -> List
30,113✔
895
    end;
896
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
263,943✔
897
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
1,770,002✔
898
to_bin({Type, Args}) ->
UNCOV
899
    unicode:characters_to_binary(io_lib:format("~ts-~p", [Type, Args]));
×
900
to_bin(X) ->
901
    X.
1,661,412✔
902

903
parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) ->
904
    {Props, Required, Refs} = parse_object_loop(PropList, Module, Options),
254,120✔
905
    Object = #{<<"type">> => object, <<"properties">> => fix_empty_props(Props)},
254,119✔
906
    case Required of
254,119✔
907
        [] -> {Object, Refs};
166,201✔
908
        _ -> {maps:put(required, Required, Object), Refs}
87,918✔
909
    end;
910
parse_object(Other, Module, Options) ->
911
    erlang:throw(
3✔
912
        {error, #{
913
            msg => <<"Object only supports not empty proplists">>,
914
            args => Other,
915
            module => Module,
916
            options => Options
917
        }}
918
    ).
919

920
parse_object_loop(PropList0, Module, Options) ->
921
    PropList = filter_hidden_key(PropList0, Module),
254,120✔
922
    parse_object_loop(PropList, Module, Options, _Props = [], _Required = [], _Refs = []).
254,120✔
923

924
filter_hidden_key(PropList0, Module) ->
925
    {PropList1, _} = lists:foldr(
254,120✔
926
        fun({Key, Hocon} = Prop, {PropAcc, KeyAcc}) ->
927
            NewKeyAcc = assert_no_duplicated_key(Key, KeyAcc, Module),
2,041,214✔
928
            case hoconsc:is_schema(Hocon) andalso is_hidden(Hocon) of
2,041,214✔
929
                true -> {PropAcc, NewKeyAcc};
197,838✔
930
                false -> {[Prop | PropAcc], NewKeyAcc}
1,843,376✔
931
            end
932
        end,
933
        {[], []},
934
        PropList0
935
    ),
936
    PropList1.
254,120✔
937

938
assert_no_duplicated_key(Key, Keys, Module) ->
939
    KeyBin = emqx_utils_conv:bin(Key),
2,041,214✔
940
    case lists:member(KeyBin, Keys) of
2,041,214✔
UNCOV
941
        true -> throw({duplicated_key, #{module => Module, key => KeyBin, keys => Keys}});
×
942
        false -> [KeyBin | Keys]
2,041,214✔
943
    end.
944

945
parse_object_loop([], _Module, _Options, Props, Required, Refs) ->
946
    {lists:reverse(Props), lists:usort(Required), Refs};
254,119✔
947
parse_object_loop([{Name, Hocon} | Rest], Module, Options, Props, Required, Refs) ->
948
    NameBin = to_bin(Name),
1,843,363✔
949
    case hoconsc:is_schema(Hocon) of
1,843,363✔
950
        true ->
951
            HoconType = hocon_schema:field_schema(Hocon, type),
1,842,923✔
952
            Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
1,842,923✔
953
            SchemaToSpec = schema_converter(Options),
1,842,923✔
954
            Init = maps:remove(
1,842,923✔
955
                summary,
956
                trans_desc(Init0, Hocon, SchemaToSpec, NameBin, Options)
957
            ),
958
            {Prop, Refs1} = SchemaToSpec(HoconType, Module),
1,842,923✔
959
            NewRequiredAcc =
1,842,922✔
960
                case is_required(Hocon) of
961
                    true -> [NameBin | Required];
229,639✔
962
                    false -> Required
1,613,283✔
963
                end,
964
            parse_object_loop(
1,842,922✔
965
                Rest,
966
                Module,
967
                Options,
968
                [{NameBin, maps:merge(Prop, Init)} | Props],
969
                NewRequiredAcc,
970
                Refs1 ++ Refs
971
            );
972
        false ->
973
            %% TODO: there is only a handful of such
974
            %% refactor the schema to unify the two cases
975
            {SubObject, SubRefs} = parse_object(Hocon, Module, Options),
440✔
976
            parse_object_loop(
440✔
977
                Rest, Module, Options, [{NameBin, SubObject} | Props], Required, SubRefs ++ Refs
978
            )
979
    end.
980

981
%% return true if the field has 'importance' set to 'hidden'
982
is_hidden(Hocon) ->
983
    hocon_schema:is_hidden(Hocon, #{include_importance_up_from => ?IMPORTANCE_LOW}).
2,040,774✔
984

985
is_required(Hocon) ->
986
    hocon_schema:field_schema(Hocon, required) =:= true.
1,842,922✔
987

988
fix_empty_props([]) ->
989
    #{};
175✔
990
fix_empty_props(Props) ->
991
    Props.
253,944✔
992

993
content(ApiSpec) ->
994
    content(ApiSpec, undefined).
44,003✔
995

996
content(ApiSpec, undefined) ->
997
    #{<<"application/json">> => #{<<"schema">> => ApiSpec}};
59,680✔
998
content(ApiSpec, Examples) when is_map(Examples) ->
999
    #{<<"application/json">> => Examples#{<<"schema">> => ApiSpec}}.
10,354✔
1000

1001
to_ref(Mod, StructName, Acc, RefsAcc) ->
1002
    Ref = #{<<"$ref">> => ?TO_COMPONENTS_PARAM(Mod, StructName)},
9,882✔
1003
    {[Ref | Acc], [{Mod, StructName, parameter} | RefsAcc]}.
9,882✔
1004

1005
schema_converter(Options) ->
1006
    maps:get(schema_converter, Options, fun hocon_schema_to_spec/2).
1,845,584✔
1007

1008
hocon_error_msg(Reason) ->
1009
    emqx_utils:readable_error_msg(Reason).
132✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc