Skip to content

Commit 8a86dd3

Browse files
author
Yuriy Zhloba
committed
Merge branch 'kn-51937-extra-object-key-pass' into 'master'
kn-51937-extra-object-key-pass See merge request flussonic/openapi_handler!59
2 parents 6573b92 + 0cccc1c commit 8a86dd3

9 files changed

Lines changed: 160 additions & 6 deletions

.gitlab-ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,19 @@ run-test:
2222
expire_in: 1 week
2323
paths:
2424
- test-logs
25+
26+
run-test-legacy:
27+
stage: test
28+
needs:
29+
- prepare-src
30+
script:
31+
- mkdir -p test-logs-legacy
32+
- docker run --rm -v ${PWD}/test-logs-legacy:/openapi_handler/test-logs openapi_handler make ci-test-legacy
33+
artifacts:
34+
reports:
35+
junit: test-logs-legacy/*/junit_report.xml
36+
when: always
37+
name: "${CI_JOB_STAGE}_${BRANCH_NAME}-test-legacy"
38+
expire_in: 1 week
39+
paths:
40+
- test-logs-legacy

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ all:
66
ci-test:
77
REBAR_CONFIG=rebar.config_ ./rebar3 as dev ct --logdir test-logs --readable true
88

9+
ci-test-legacy:
10+
REBAR_CONFIG=rebar.config_ ./rebar3 as dev_legacy ct --logdir test-logs --readable true
11+
912
clean:
1013
rm -rf _build rebar.lock

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Also see validation quirks below.
8787
You can customize the way objects are processed.
8888
Available options:
8989
* `#{extra_obj_key => drop}` -- the original object may contain keys not described by schema, just ignore them instead of raising an error
90+
* `#{extra_obj_key => pass}` -- the original object may contain keys not described by schema, just pass them as is instead of raising an error
9091
* `#{required_obj_keys => error}` -- raise an error when original object misses some required keys
9192
* `#{validators => #{Format :: atom() => Validator}}`
9293
`Validator :: fun(Value) -> {ok, ConvertedValue} | {error, #{}}`

rebar.config_

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,16 @@
1212
redbug
1313
]},
1414
{ct_opts, [{ct_hooks, [cth_surefire]}]}
15+
]},
16+
{dev_legacy, [
17+
{deps, [
18+
jsx,
19+
yamerl,
20+
{cowboy, "1.0.1"},
21+
{lhttpc, {git, "https://github.com/erlyvideo/lhttpc.git", {branch, "master"}}},
22+
redbug
23+
]},
24+
{erl_opts, [{d,legacy}]},
25+
{ct_opts, [{ct_hooks, [cth_surefire]}]}
1526
]}
1627
]}.

src/openapi_handler.erl

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
-export([init/2, handle/2, terminate/3]).
6-
-export([routes/1, load_schema/2]).
6+
-export([routes/1, load_schema/2, choose_module/0]).
77
-export([read_schema/1]).
88
-export([routes_sort/1]). % for tests
99

@@ -14,6 +14,8 @@
1414
routes(#{schema := SchemaPath, module := Module, name := Name, prefix := Prefix} = Config) ->
1515
#{} = Schema = load_schema(SchemaPath, Name),
1616
SchemaOpts = get_schema_opts(Config),
17+
HandlerModule = choose_module(),
18+
1719
#{paths := Paths} = Schema,
1820
Routes = lists:map(fun({Path,PathSpec}) ->
1921
<<"/", _/binary>> = CowboyPath = re:replace(atom_to_list(Path), "{([^\\}]+)}", ":\\1",[global,{return,binary}]),
@@ -25,13 +27,23 @@ routes(#{schema := SchemaPath, module := Module, name := Name, prefix := Prefix}
2527
% It is too bad to pass all this stuff through cowboy options because it starts suffering
2628
% from GC on big state. Either ETS, either persistent_term, either compilation of custom code
2729
persistent_term:put({openapi_handler_route,Name,CowboyPath}, PathSpec1#{name => Name, module => Module, schema_opts => SchemaOpts}),
28-
{<<Prefix/binary, CowboyPath/binary>>, ?MODULE, {Name,CowboyPath}}
30+
{<<Prefix/binary, CowboyPath/binary>>, HandlerModule, {Name,CowboyPath}}
2931
end, maps:to_list(Paths)),
3032
% After sorting, bindings (starting with ":") must be after constant path segments, so generic routes only work after specific ones.
3133
% E.g. "/api/users/admin" must be before "/api/users/:id"
3234
% Thus special sorting function
3335
routes_sort(Routes).
3436

37+
choose_module() ->
38+
try
39+
% Check if cowboy uses modern Req (map)
40+
1234 = cowboy_req:port(#{port => 1234}),
41+
?MODULE
42+
catch
43+
error:{badrecord,_}:_ ->
44+
% cowboy uses record for Req, use legacy wrapper
45+
openapi_handler_legacy
46+
end.
3547

3648
prepare_operation_fm(_M, #{operationId := OperationId_} = Operation, Parameters) ->
3749
Op1 = maps:remove(description,Operation),
@@ -528,7 +540,7 @@ terminate(_,_,_) ->
528540

529541
get_schema_opts(#{schema_opts := #{} = SchemaOpts}) ->
530542
maps:map(fun
531-
(extra_obj_key,Flag) when Flag == drop; Flag == error -> ok;
543+
(extra_obj_key,Flag) when Flag == drop; Flag == error; Flag == pass -> ok;
532544
(K,V) -> error({bad_schema_opt,K,V})
533545
end, SchemaOpts),
534546
SchemaOpts;

src/openapi_handler_legacy.erl

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
-module(openapi_handler_legacy).
2+
3+
-export([init/3, handle/2, terminate/3]).
4+
5+
-export([method/1, peer/1, qs/1, parse_qs/1, bindings/1]).
6+
-export([read_body/1, reply/4, headers/1, header/2, header/3, parse_header/2]).
7+
-export([read_multipart_files/1]).
8+
9+
init(_, Req, {Name, CowboyPath}) ->
10+
openapi_handler:do_init(Req, Name, CowboyPath, ?MODULE, #{ok => shutdown, no_handle => true}).
11+
12+
handle(Req, #{} = Request) ->
13+
openapi_handler:do_handle(Req, Request, ?MODULE).
14+
15+
terminate(_,_,_) ->
16+
ok.
17+
18+
19+
%% Cowboy compat wrapper. Provides Cowboy 2.9 API for Cowboy 1.0
20+
method(Req) ->
21+
{Method, _Req1} = cowboy_req:method(Req),
22+
Method.
23+
24+
peer(Req) ->
25+
{Peer, _Req1} = cowboy_req:peer(Req),
26+
Peer.
27+
28+
qs(Req) ->
29+
{QS, _Req1} = cowboy_req:qs(Req),
30+
QS.
31+
32+
parse_qs(Req) ->
33+
try
34+
cow_qs:parse_qs(qs(Req))
35+
catch _:_:Stacktrace ->
36+
erlang:raise(exit, {request_error, qs,
37+
'Malformed query string; application/x-www-form-urlencoded expected.'
38+
}, Stacktrace)
39+
end.
40+
41+
bindings(Req) ->
42+
{Bindings, _Req1} = cowboy_req:bindings(Req),
43+
maps:from_list(Bindings).
44+
45+
read_body(Req) ->
46+
cowboy_req:body(Req).
47+
48+
reply(Code, Headers, Body, Req) ->
49+
{ok, Req1} = cowboy_req:reply(Code, maps:to_list(Headers), Body, Req),
50+
Req1.
51+
52+
headers(Req) ->
53+
{Headers, _Req1} = cowboy_req:headers(Req),
54+
maps:from_list(Headers).
55+
56+
header(Name, Req) ->
57+
{Value, _Req1} = cowboy_req:header(Name, Req),
58+
Value.
59+
60+
header(Name, Req, Default) ->
61+
{Value, _Req1} = cowboy_req:header(Name, Req, Default),
62+
Value.
63+
64+
parse_header(Name, Req) ->
65+
{ok, Value, _Req1} = cowboy_req:parse_header(Name, Req),
66+
Value.
67+
68+
69+
70+
read_multipart_files(Req) ->
71+
% This is a copy of openapi_handler:read_multipart_files/1 with
72+
% cowboy_req:part/1, cowboy_req:part_body/1 and different number of cow_multipart:form_data/1
73+
% results.
74+
% Despite other methods are cowboy_req methods in this module, it is much simpler to keep such
75+
% method instead of multiple call mocks to read_part/1 and read_part_body/1 in tests
76+
do_read_multipart_files(Req, []).
77+
78+
do_read_multipart_files(Req0, Files) ->
79+
case cowboy_req:part(Req0) of
80+
{ok, Headers, Req1} ->
81+
{file, _FieldName, Filename, _CType, _TE} = cow_multipart:form_data(Headers),
82+
{Bin, Req2} = read_multipart_file(Req1, <<>>),
83+
do_read_multipart_files(Req2, [{Filename, Bin}| Files]);
84+
{done, Req1} ->
85+
{ok, Files, Req1}
86+
end.
87+
88+
read_multipart_file(Req0, Bin) ->
89+
case cowboy_req:part_body(Req0) of
90+
{ok, LastBodyChunk, Req} -> {<<Bin/binary, LastBodyChunk/binary>>, Req};
91+
{more, BodyChunk, Req} -> read_multipart_file(Req, <<Bin/binary, BodyChunk/binary>>)
92+
end.

src/openapi_schema.erl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ process(Input, #{} = Opts) ->
4545
(query,Flag) when Flag == true; Flag == false -> ok;
4646
(apply_defaults,Flag) when Flag == true; Flag == false -> ok;
4747
(patch,Flag) when Flag == true; Flag == false -> ok;
48-
(extra_obj_key,Flag) when Flag == drop; Flag == error -> ok;
48+
(extra_obj_key,Flag) when Flag == drop; Flag == error; Flag == pass -> ok;
4949
(required_obj_keys,Flag) when Flag == drop; Flag == error -> ok;
5050
(access_type,Flag) when Flag == read; Flag == write -> ok;
5151
(explain,FlagList) -> check_explain_keys(FlagList);
@@ -593,6 +593,11 @@ check_extra_keys(Input, Encoded, #{extra_obj_key := error} = Opts) when is_map(I
593593
_ -> Encoded
594594
end;
595595

596+
check_extra_keys(Input, Encoded, #{extra_obj_key := pass}) when is_map(Input) andalso is_map(Encoded) andalso map_size(Encoded) > 0 ->
597+
EncodedKeys = [K || K <- maps:keys(Encoded), is_atom(K)],
598+
ExtraKeys = maps:keys(Input) -- [atom_to_binary(K) || K <- EncodedKeys],
599+
maps:merge(maps:with(EncodedKeys, Encoded), maps:with(ExtraKeys, Input));
600+
596601
check_extra_keys(_Input, Encoded, _Opts) ->
597602
Encoded.
598603

test/openapi_handler_SUITE.erl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ groups() ->
3636
]}
3737
].
3838

39+
-ifdef(legacy).
40+
start_http(Routes, ApiName) ->
41+
cowboy:start_http(ApiName, 1, [{port, 0}],
42+
[{env, [{dispatch, cowboy_router:compile([{'_', Routes}])}]}]).
43+
-else.
3944
start_http(Routes, ApiName) ->
4045
cowboy:start_clear(ApiName, [{port, 0}],
4146
#{env => #{dispatch => cowboy_router:compile([{'_', Routes}])}}).
42-
47+
-endif.
4348

4449
init_per_suite(Config) ->
4550
{ok, _} = application:ensure_all_started(cowboy),
@@ -280,7 +285,7 @@ json_array_ok(_) ->
280285
ok.
281286

282287
putFile(#{req := Req, '$cowboy_req' := CowboyReq}) ->
283-
<<"PUT">> = cowboy_req:method(CowboyReq),
288+
<<"PUT">> = ?MODULE:method(CowboyReq),
284289
Body = <<"{\"size\":100}">>,
285290
Req1 = reply(200, #{<<"content-length">> => byte_size(Body), <<"content-type">> => <<"application/json">>}, Body, Req),
286291
{done, Req1}.

test/openapi_schema_SUITE.erl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ groups() ->
1313
read_default,
1414
extra_keys_error,
1515
extra_keys_drop,
16+
extra_keys_pass,
1617
null_in_array,
1718
nullable_by_oneof,
1819
discriminator,
@@ -84,6 +85,14 @@ extra_keys_drop(_) ->
8485
#{inputs := [],name := <<"read_default">>,static := true} = openapi_schema:process(Json, #{type => stream_config, whole_schema => Schema, apply_defaults => true}),
8586
ok.
8687

88+
extra_keys_pass(_) ->
89+
Json = #{<<"name">> => <<"read_default">>, extra_key1 => <<"abc">>, <<"extrakey2">> => def},
90+
Schema = persistent_term:get({openapi_handler_schema,test_openapi}),
91+
#{inputs := [],name := <<"read_default">>,static := true,
92+
extra_key1 := <<"abc">>,<<"extrakey2">> := def
93+
} = openapi_schema:process(Json, #{type => stream_config, whole_schema => Schema, apply_defaults => true, extra_obj_key => pass}),
94+
ok.
95+
8796

8897
null_in_array(_) ->
8998
% When items are not nullable, passing null|undefined as a list element should return an error

0 commit comments

Comments
 (0)