Skip to content

Commit 0c45030

Browse files
Merge pull request #143 from richcarl/normalize-json
Use normalized JSON representation internally
2 parents bf1e8a0 + 6c9c12b commit 0c45030

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)