Skip to content

Commit 6c9c12b

Browse files
committed
Use normalized JSON representation internally
Allows the input spec to use either atoms or binaries as map keys. E.g., if parsing an external JSON file instead of using inline Erlang maps in the handler modules, the default behaviour would be not to create atoms for keys. Similarly, all string values are normalized to binaries. This makes it easy to look up a key and check its value.
1 parent bf1e8a0 commit 6c9c12b

3 files changed

Lines changed: 155 additions & 102 deletions

File tree

rebar.config

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,13 @@
7171

7272
{xref_ignores, [{cowboy_swagger, to_json, 1},
7373
{cowboy_swagger, add_definition, 2},
74+
{cowboy_swagger, get_global_spec, 0},
75+
{cowboy_swagger, get_global_spec, 1},
76+
{cowboy_swagger, set_global_spec, 1},
7477
{cowboy_swagger, schema, 1},
7578
{cowboy_swagger, enc_json, 1},
7679
{cowboy_swagger, dec_json, 1},
80+
{cowboy_swagger, normalize_json, 1},
7781
{cowboy_swagger, swagger_paths, 1},
7882
{cowboy_swagger, validate_metadata, 1},
7983
{cowboy_swagger, filter_cowboy_swagger_handler, 1},

src/cowboy_swagger.erl

Lines changed: 140 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
-export([to_json/1, add_definition/1, add_definition/2, add_definition_array/2, schema/1]).
77

88
%% Utilities
9-
-export([enc_json/1, dec_json/1]).
9+
-export([enc_json/1, dec_json/1, normalize_json/1]).
1010
-export([swagger_paths/1, validate_metadata/1]).
1111
-export([filter_cowboy_swagger_handler/1]).
12-
-export([get_existing_definitions/2]).
12+
-export([get_existing_definitions/2,
13+
get_global_spec/0, get_global_spec/1, set_global_spec/1]).
1314

1415
% is_visible is used as a maps:filter/2 predicate, which requires a /2 arity function
1516
-hank([{unnecessary_function_arguments, [is_visible/2]}]).
@@ -19,12 +20,12 @@
1920
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2021

2122
-opaque parameter_obj() ::
22-
#{ name => iodata()
23-
, in => iodata()
24-
, description => iodata()
23+
#{ name => binary()
24+
, in => binary()
25+
, description => binary()
2526
, required => boolean()
26-
, type => iodata()
27-
, schema => iodata()
27+
, type => binary()
28+
, schema => binary()
2829
}.
2930
-export_type([parameter_obj/0]).
3031

@@ -62,12 +63,12 @@
6263

6364
%% Swagger map spec
6465
-opaque swagger_map() ::
65-
#{ description => iodata()
66-
, summary => iodata()
66+
#{ description => binary()
67+
, summary => binary()
6768
, parameters => [parameter_obj()]
68-
, tags => [iodata()]
69-
, consumes => [iodata()]
70-
, produces => [iodata()]
69+
, tags => [binary()]
70+
, consumes => [binary()]
71+
, produces => [binary()]
7172
, responses => responses_definitions()
7273
}.
7374
-type metadata() :: trails:metadata(swagger_map()).
@@ -88,8 +89,7 @@
8889
-spec to_json([trails:trail()]) -> jsx:json_text().
8990
to_json(Trails) ->
9091
Default = #{info => #{title => <<"API-DOCS">>}},
91-
GlobalSpec = normalize_map_values(
92-
application:get_env(cowboy_swagger, global_spec, Default)),
92+
GlobalSpec = get_global_spec(Default),
9393
SanitizeTrails = filter_cowboy_swagger_handler(Trails),
9494
SwaggerSpec = create_swagger_spec(GlobalSpec, SanitizeTrails),
9595
enc_json(SwaggerSpec).
@@ -115,19 +115,20 @@ add_definition(Name, Properties) ->
115115
) ->
116116
ok.
117117
add_definition(Definition) ->
118-
CurrentSpec = application:get_env(cowboy_swagger, global_spec, #{}),
119-
Type = definition_type(Definition),
118+
CurrentSpec = get_global_spec(),
119+
NormDefinition = normalize_json(Definition),
120+
Type = definition_type(NormDefinition),
120121
NewDefinitions = maps:merge( get_existing_definitions(CurrentSpec, Type)
121-
, Definition
122+
, normalize_json(NormDefinition)
122123
),
123124
NewSpec = prepare_new_global_spec(CurrentSpec, NewDefinitions, Type),
124-
application:set_env(cowboy_swagger, global_spec, NewSpec).
125+
set_global_spec(NewSpec).
125126

126127
definition_type(Definition) ->
127128
case maps:values(Definition) of
128-
[#{in := In}] when In =:= query orelse In =:= path orelse In =:= header ->
129-
parameters;
130-
_ -> schemas
129+
[#{<<"in">> := In}] when In =:= <<"query">>; In =:= <<"path">>; In =:= <<"header">> ->
130+
<<"parameters">>;
131+
_ -> <<"schemas">>
131132
end.
132133

133134
-spec schema(DefinitionName::parameter_definition_name()) ->
@@ -159,6 +160,62 @@ dec_json(Data) ->
159160
throw(bad_json)
160161
end.
161162

163+
%% We assume the jsx representation of JSON as Erlang terms:
164+
%% true/false/null: 'true' | 'false' | 'null'
165+
%% number: integer() | float()
166+
%% string: binary() | atom()
167+
%% array: [ JSON ]
168+
%% object: #{ Label => JSON, ... } | [{ Label, JSON }] | [{}]
169+
%% date string: {{Year, Month, Day}, {Hour, Min, Sec}}
170+
%% where
171+
%% Label: binary() | atom() | integer()
172+
%%
173+
%% We also detect lists of printable characters (plain Erlang strings) and
174+
%% convert them into binaries. This use is deprecated and should be
175+
%% removed (for example, a json array [64] becomes <<"@">>).
176+
%%
177+
%% When normalizing, we make all strings and labels be binaries,
178+
%% and all objects be maps, not proplists.
179+
180+
%% @hidden
181+
-spec normalize_json(jsx:json_term()) -> jsx:json_term().
182+
normalize_json(Json) when is_map(Json) ->
183+
normalize_json_proplist(maps:to_list(Json));
184+
normalize_json([]) -> []; % empty array
185+
normalize_json([{}]) -> #{}; % special case in jsx for empty map as list
186+
normalize_json([{_K, _V} | _] = Json) ->
187+
normalize_json_proplist(Json); % map as proplist
188+
normalize_json(Json) when is_list(Json) ->
189+
case io_lib:printable_list(Json) of
190+
true -> unicode:characters_to_binary(Json);
191+
false -> normalize_json_list(Json)
192+
end;
193+
normalize_json(true) -> true;
194+
normalize_json(false) -> false;
195+
normalize_json(null) -> null;
196+
normalize_json(Json) when is_atom(Json) -> erlang:atom_to_binary(Json, utf8);
197+
normalize_json(Json) ->
198+
Json.
199+
200+
normalize_json_key(K) when is_atom(K) ->
201+
erlang:atom_to_binary(K, utf8);
202+
normalize_json_key(K) when is_integer(K) ->
203+
erlang:integer_to_binary(K);
204+
normalize_json_key(K) ->
205+
K.
206+
207+
normalize_json_proplist(Proplist) ->
208+
F = fun({K, V}, Acc) ->
209+
maps:put(normalize_json_key(K), normalize_json(V), Acc)
210+
end,
211+
lists:foldl(F, #{}, Proplist).
212+
213+
normalize_json_list(List) ->
214+
F = fun(V, Acc) ->
215+
[normalize_json(V) | Acc]
216+
end,
217+
lists:foldr(F, [], List).
218+
162219
%% @hidden
163220
-spec swagger_paths([trails:trail()]) -> map().
164221
swagger_paths(Trails) ->
@@ -178,49 +235,69 @@ validate_metadata(Metadata) ->
178235
-spec filter_cowboy_swagger_handler([trails:trail()]) -> [trails:trail()].
179236
filter_cowboy_swagger_handler(Trails) ->
180237
F = fun(Trail) ->
181-
MD = trails:metadata(Trail),
238+
MD = get_metadata(Trail),
182239
maps:size(maps:filter(fun is_visible/2, MD)) /= 0
183240
end,
184241
lists:filter(F, Trails).
185242

186-
-spec get_existing_definitions(CurrentSpec :: map(), Type :: scheams | parameters) ->
243+
-spec get_existing_definitions(CurrentSpec :: jsx:json_term(), Type :: atom() | binary()) ->
187244
Definition :: parameters_definitions()
188245
| parameters_definition_array().
189-
get_existing_definitions(CurrentSpec, Type) ->
246+
get_existing_definitions(CurrentSpec, Type) when is_atom(Type) ->
247+
get_existing_definitions(CurrentSpec, atom_to_binary(Type, utf8));
248+
get_existing_definitions(CurrentSpec, Type) when is_binary(Type) ->
190249
case swagger_version() of
191250
swagger_2_0 ->
192-
maps:get(definitions, CurrentSpec, #{});
251+
maps:get(<<"definitions">>, CurrentSpec, #{});
193252
openapi_3_0_0 ->
194253
case CurrentSpec of
195-
#{components :=
254+
#{<<"components">> :=
196255
#{Type := Def }} -> Def;
197256
_Other -> #{}
198257
end
199258
end.
200259

260+
-spec get_global_spec() -> jsx:json_term().
261+
get_global_spec() ->
262+
get_global_spec(#{}).
263+
264+
-spec get_global_spec(jsx:json_term()) -> jsx:json_term().
265+
get_global_spec(Default) ->
266+
normalize_json(application:get_env(cowboy_swagger, global_spec, Default)).
267+
268+
-spec set_global_spec(jsx:json_term()) -> ok.
269+
set_global_spec(NewSpec) ->
270+
application:set_env(cowboy_swagger, global_spec, normalize_json(NewSpec)).
271+
272+
273+
-spec get_metadata(trails:trail()) -> jsx:json_term().
274+
get_metadata(Trail) ->
275+
normalize_json(trails:metadata(Trail)).
276+
277+
201278
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
202279
%% Private API.
203280
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
204281

205282
%% @private
206283
-spec swagger_version() -> swagger_version().
207284
swagger_version() ->
208-
case application:get_env(cowboy_swagger, global_spec, #{}) of
209-
#{openapi := "3.0.0"} -> openapi_3_0_0;
210-
#{swagger := "2.0"} -> swagger_2_0;
285+
case get_global_spec() of
286+
#{<<"openapi">> := <<"3.0.0">>} -> openapi_3_0_0;
287+
#{<<"swagger">> := <<"2.0">>} -> swagger_2_0;
211288
_Other -> swagger_2_0
212289
end.
213290

214291
%% @private
215292
is_visible(_Method, Metadata) ->
216-
not maps:get(hidden, Metadata, false).
293+
not maps:get(<<"hidden">>, Metadata, false).
217294

218295
%% @private
219296
translate_swagger_paths([], Acc) ->
220297
Acc;
221298
translate_swagger_paths([Trail | T], Acc) ->
222299
Path = normalize_path(trails:path_match(Trail)),
223-
Metadata = normalize_map_values(validate_metadata(trails:metadata(Trail))),
300+
Metadata = validate_metadata(get_metadata(Trail)),
224301
translate_swagger_paths(T, maps:put(Path, Metadata, Acc)).
225302

226303
%% @private
@@ -255,117 +332,89 @@ normalize_path(Path) ->
255332
"\\[|\\]|\\:", "", [{return, binary}, global]).
256333

257334
%% @private
258-
normalize_map_values(Map) when is_map(Map) ->
259-
normalize_map_values(maps:to_list(Map));
260-
normalize_map_values(Proplist) ->
261-
F = fun({K, []}, Acc) ->
262-
maps:put(K, normalize_list_values([]), Acc);
263-
({K, V}, Acc) when is_list(V) ->
264-
case io_lib:printable_list(V) of
265-
true -> maps:put(K, list_to_binary(V), Acc);
266-
false -> maps:put(K, normalize_list_values(V), Acc)
267-
end;
268-
({K, V}, Acc) when is_map(V) ->
269-
maps:put(K, normalize_map_values(V), Acc);
270-
({K, V}, Acc) ->
271-
maps:put(K, V, Acc)
272-
end,
273-
lists:foldl(F, #{}, Proplist).
274-
275-
%% @private
276-
normalize_list_values(List) ->
277-
F = fun(V, Acc) when is_list(V) ->
278-
case io_lib:printable_list(V) of
279-
true -> [list_to_binary(V) | Acc];
280-
false -> [normalize_list_values(V) | Acc]
281-
end;
282-
(V, Acc) when is_map(V) ->
283-
[normalize_map_values(V) | Acc];
284-
(V, Acc) ->
285-
[V | Acc]
286-
end,
287-
lists:foldr(F, [], List).
288-
289-
%% @private
290-
create_swagger_spec(#{swagger := _Version} = GlobalSpec, SanitizeTrails) ->
291-
BasePath = maps:get(basePath, GlobalSpec, undefined),
335+
create_swagger_spec(#{<<"swagger">> := _Version} = GlobalSpec, SanitizeTrails) ->
336+
BasePath = maps:get(<<"basePath">>, GlobalSpec, undefined),
292337
SwaggerPaths = swagger_paths(SanitizeTrails, BasePath),
293-
GlobalSpec#{paths => SwaggerPaths};
294-
create_swagger_spec(#{openapi := _Version} = GlobalSpec, SanitizeTrails) ->
338+
GlobalSpec#{<<"paths">> => SwaggerPaths};
339+
create_swagger_spec(#{<<"openapi">> := _Version} = GlobalSpec, SanitizeTrails) ->
295340
BasePath = deconstruct_openapi_url(GlobalSpec),
296341
SwaggerPaths = swagger_paths(SanitizeTrails, BasePath),
297-
GlobalSpec#{paths => SwaggerPaths};
342+
GlobalSpec#{<<"paths">> => SwaggerPaths};
298343
create_swagger_spec(GlobalSpec, SanitizeTrails) ->
299-
create_swagger_spec(GlobalSpec#{openapi => <<"3.0.0">>}, SanitizeTrails).
344+
create_swagger_spec(GlobalSpec#{<<"openapi">> => <<"3.0.0">>}, SanitizeTrails).
300345

301346
%% @private
302347
deconstruct_openapi_url(GlobalSpec) ->
303-
[Server|_] = maps:get(servers, GlobalSpec, [#{}]),
304-
Url = maps:get(url, Server, <<"">>),
348+
[Server|_] = maps:get(<<"servers">>, GlobalSpec, [#{}]),
349+
Url = maps:get(<<"url">>, Server, <<"">>),
305350
maps:get(path, uri_string:parse(Url)).
306351

307352
%% @private
308353
validate_swagger_map(Map) ->
309354
F = fun(_K, V) ->
310-
Params = validate_swagger_map_params(maps:get(parameters, V, [])),
311-
Responses = validate_swagger_map_responses(maps:get(responses, V, #{})),
312-
V#{parameters => Params, responses => Responses}
355+
Params = validate_swagger_map_params(maps:get(<<"parameters">>, V, [])),
356+
Responses = validate_swagger_map_responses(maps:get(<<"responses">>, V, #{})),
357+
V#{<<"parameters">> => Params, <<"responses">> => Responses}
313358
end,
314359
maps:map(F, Map).
315360

316361
%% @private
317362
validate_swagger_map_params(Params) ->
318363
ValidateParams =
319364
fun(E) ->
320-
case maps:get(name, E, undefined) of
365+
case maps:get(<<"name">>, E, undefined) of
321366
undefined -> maps:is_key(<<"$ref">>, E);
322-
_ -> {true, E#{in => maps:get(in, E, <<"path">>)}}
367+
_ -> {true, E#{in => maps:get(<<"in">>, E, <<"path">>)}}
323368
end
324369
end,
325370
lists:filtermap(ValidateParams, Params).
326371

327372
%% @private
328373
validate_swagger_map_responses(Responses) ->
329-
F = fun(_K, V) -> V#{description => maps:get(description, V, <<"">>)} end,
374+
F = fun(_K, V) -> V#{<<"description">> => maps:get(<<"description">>, V, <<"">>)} end,
330375
maps:map(F, Responses).
331376

332377
%% @private
333378
-spec build_definition( Name::parameter_definition_name()
334379
, Properties::property_obj()
335380
) ->
336381
parameters_definitions().
337-
build_definition(Name, Properties) ->
338-
#{Name => #{ type => <<"object">>
339-
, properties => Properties
382+
build_definition(Name, Properties) when is_atom(Name) ->
383+
build_definition(erlang:atom_to_binary(Name, utf8), Properties);
384+
build_definition(Name, Properties) when is_binary(Name) ->
385+
#{Name => #{ <<"type">> => <<"object">>
386+
, <<"properties">> => Properties
340387
}}.
341388

342389
%% @private
343390
-spec build_definition_array( Name::parameter_definition_name()
344391
, Properties::property_obj()
345392
) ->
346393
parameters_definition_array().
347-
build_definition_array(Name, Properties) ->
348-
#{Name => #{ type => <<"array">>
349-
, items => #{ type => <<"object">>
350-
, properties => Properties
351-
}
394+
build_definition_array(Name, Properties) when is_atom(Name) ->
395+
build_definition_array(erlang:atom_to_binary(Name, utf8), Properties);
396+
build_definition_array(Name, Properties) when is_binary(Name) ->
397+
#{Name => #{ <<"type">> => <<"array">>
398+
, <<"items">> => #{ <<"type">> => <<"object">>
399+
, <<"properties">> => Properties
400+
}
352401
}}.
353402

354403
%% @private
355-
-spec prepare_new_global_spec( CurrentSpec :: map()
404+
-spec prepare_new_global_spec( CurrentSpec :: jsx:json_term()
356405
, Definitions :: parameters_definitions()
357406
| parameters_definition_array()
358-
, Type ::schemas|parameters
407+
, Type ::binary()
359408
) ->
360-
NewSpec :: map().
409+
NewSpec :: jsx:json_term().
361410
prepare_new_global_spec(CurrentSpec, Definitions, Type) ->
362411
case swagger_version() of
363412
swagger_2_0 ->
364-
CurrentSpec#{definitions => Definitions
413+
CurrentSpec#{<<"definitions">> => Definitions
365414
};
366415
openapi_3_0_0 ->
367-
Components = maps:get(components, CurrentSpec, #{}),
368-
CurrentSpec#{components =>
416+
Components = maps:get(<<"components">>, CurrentSpec, #{}),
417+
CurrentSpec#{<<"components">> =>
369418
Components#{ Type => Definitions
370419
}
371420
}

0 commit comments

Comments
 (0)