Skip to content

Commit bfa5545

Browse files
authored
Merge pull request #19 from stolen/add-github-ci-workflow
define tests workflow, drop obsolete deps
2 parents 5cdce18 + a8dc196 commit bfa5545

8 files changed

Lines changed: 255 additions & 39 deletions

File tree

.github/workflows/tests.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Erlang CI
2+
3+
on:
4+
push:
5+
branches: [ "master" ]
6+
pull_request:
7+
branches: [ "master" ]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
common_test:
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
otp_ver: [27, 28, latest]
18+
container:
19+
image: erlang:${{ matrix.otp_ver }}
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Run tests
24+
env:
25+
REBAR_CONFIG: rebar.config_
26+
run: rebar3 as dev ct --logdir test-logs --readable true -vv
27+
- name: upload CT report
28+
uses: actions/upload-artifact@v4
29+
with:
30+
name: ct_report_${{ matrix.otp_ver }}
31+
path: test-logs/ct_run*/
32+
33+
common_test_legacy:
34+
runs-on: ubuntu-latest
35+
strategy:
36+
matrix:
37+
otp_ver: [24, 25, 26]
38+
container:
39+
image: erlang:${{ matrix.otp_ver }}
40+
41+
steps:
42+
- uses: actions/checkout@v4
43+
- name: Run tests
44+
env:
45+
REBAR_CONFIG: rebar.config.pre27_
46+
run: rebar3 as dev ct --logdir test-logs --readable true -vv
47+
- name: upload CT report
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: ct_report_${{ matrix.otp_ver }}
51+
path: test-logs/ct_run*/

rebar.config.pre27_

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
% No deps by default to prevent conflicts
2+
{deps, [
3+
]}.
4+
5+
{profiles, [
6+
{dev, [
7+
{deps, [
8+
jsx,
9+
{cowboy, "2.12.0"},
10+
redbug
11+
]},
12+
{ct_opts, [{ct_hooks, [cth_surefire]}]}
13+
]}
14+
]}.

rebar.config_

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
{profiles, [
66
{dev, [
77
{deps, [
8-
jsx,
9-
yamerl,
108
{cowboy, "2.12.0"},
11-
{lhttpc, {git, "https://github.com/erlyvideo/lhttpc.git", {branch, "master"}}},
129
redbug
1310
]},
1411
{ct_opts, [{ct_hooks, [cth_surefire]}]}

src/openapi_client.erl

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,8 @@ call(#{schema := Schema, uri := URI} = State, OperationId, Args0, Opts) when is_
6161
#{raw_body := _} -> <<"raw_file_upload">>;
6262
_ -> RequestBody
6363
end]),
64-
Timeout = proplists:get_value(timeout, Opts, 50000),
65-
Result = case lhttpc:request(RequestURL, Method, RequestHeaders, RequestBody, Timeout) of
66-
{ok, {{Code0,_},ResponseHeaders0,Bin0}} ->
67-
{ok, Code0, [{string:to_lower(K),V} || {K,V} <- ResponseHeaders0], Bin0};
68-
{error, E0} ->
69-
{error, E0}
70-
end,
64+
HttpRequestFun = proplists:get_value(http_request_fun, Opts, fun http_request_httpc/5),
65+
Result = HttpRequestFun(Method, RequestURL, RequestHeaders, RequestBody, Opts),
7166

7267
case Result of
7368
{ok, Code,ResponseHeaders,Bin} when is_map_key(Code, Responses) ->
@@ -130,6 +125,31 @@ call(#{} = State, OperationId, Args, Opts) ->
130125
end.
131126

132127

128+
-spec http_request_httpc(
129+
Method :: get | post | put | patch | delete,
130+
URL :: uri_string:uri_string(),
131+
Headers :: [{string(), string()}],
132+
Body :: binary(),
133+
Opts :: proplists:proplist()
134+
) ->
135+
{ok, Status :: integer(), Headers :: [{NameLowercase :: string(), string()}], Body :: binary()} |
136+
{error, any()}.
137+
138+
http_request_httpc(Method, RequestURL, RequestHeaders, RequestBody, Opts) ->
139+
Timeout = proplists:get_value(timeout, Opts, 50000),
140+
Request = case Method of
141+
get ->
142+
{RequestURL, RequestHeaders};
143+
_ ->
144+
ContentType = proplists:get_value("Content-Type", RequestHeaders, "text/plain"),
145+
{RequestURL, RequestHeaders, ContentType, RequestBody}
146+
end,
147+
case httpc:request(Method, Request, [{timeout, Timeout}], [{body_format, binary}]) of
148+
{ok, {{_, Code0, _}, ResponseHeaders0, Bin0}} ->
149+
{ok, Code0, [{string:to_lower(K), V} || {K, V} <- ResponseHeaders0], Bin0};
150+
{error, E0} ->
151+
{error, E0}
152+
end.
133153

134154

135155
search_operation(OperationId, #{paths := Paths}) ->

src/openapi_schema.erl

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ process(Input, #{} = Opts) ->
4747
(patch,Flag) when Flag == true; Flag == false -> ok;
4848
(extra_obj_key,Flag) when Flag == drop; Flag == error -> ok;
4949
(required_obj_keys,Flag) when Flag == drop; Flag == error -> ok;
50-
(access_type,Flag) when Flag == read; Flag == write -> ok;
50+
(access_type,Flag) when Flag == read; Flag == write; Flag == raw -> ok;
5151
(explain,FlagList) -> check_explain_keys(FlagList);
5252
(K,V) -> error({unknown_option,K,V})
5353
end, Opts),
@@ -88,6 +88,8 @@ prepare_type(#{oneOf := Types} = Type0) ->
8888
Type0#{oneOf := [prepare_type(T) || T <- Types]};
8989
prepare_type(#{type := <<"object">>, properties := Props} = Type0) ->
9090
Type0#{properties => maps:map(fun(_, T) -> prepare_type(T) end, Props)};
91+
prepare_type(#{type := <<"array">>, items := Items} = Type0) ->
92+
Type0#{items => prepare_type(Items)};
9193
prepare_type(#{} = Type0) ->
9294
% Convert format name to atom. This matches validators syntax
9395
Type1 = case Type0 of
@@ -205,13 +207,15 @@ encode3(#{discriminator := #{propertyName := DKey, mapping := DMap}} = Schema, O
205207
try binary_to_existing_atom(DValue) catch error:badarg -> DValue end;
206208
#{ADKey := DValue} when is_binary(DValue) ->
207209
try binary_to_existing_atom(DValue) catch error:badarg -> DValue end;
208-
#{} -> undefined
210+
#{} -> undefined;
211+
{error, _} -> Input_
209212
end
210213
end,
211-
ADvalue1 = DefaultFun(Input),
214+
DiscrInput = maps:with([DKey, ADKey], Input),
215+
ADvalue1 = DefaultFun(DiscrInput),
212216
ADvalue2 = case ADvalue1 of
213217
undefined ->
214-
Try = encode3(hd(Types), Opts#{apply_defaults => true, required_obj_keys => drop}, Input, Path),
218+
Try = encode3(hd(Types), Opts#{apply_defaults => true, required_obj_keys => drop}, DiscrInput, Path),
215219
DefaultFun(Try);
216220
_ ->
217221
ADvalue1
@@ -222,6 +226,9 @@ encode3(#{discriminator := #{propertyName := DKey, mapping := DMap}} = Schema, O
222226
encode3(maps:without([discriminator], Schema), Opts, Input, Path);
223227
{undefined, _} ->
224228
{error, #{error => discriminator_missing, path => Path, propertyName => DKey}};
229+
{{error, _}, _} ->
230+
% this error comes from processing discriminator with first possible type, and may contain useful type error (e.g. not_string)
231+
ADvalue2;
225232
{_, undefined} ->
226233
{error, #{error => discriminator_unmapped, path => Path, propertyName => DKey, value => ADvalue2}};
227234
{_, _} ->
@@ -282,9 +289,11 @@ encode3(#{type := <<"object">>, properties := Properties} = Schema, #{query := Q
282289

283290
RequiredKeys = get_required_keys(Schema, Opts),
284291
IsReadOnly = maps:get(readOnly, Prop, false),
292+
IsWriteOnly = maps:get(writeOnly, Prop, false),
285293
IsPrimary = maps:get('x-primary-key', Prop, false),
286294
IsRequired = (lists:member(FieldBin, RequiredKeys) orelse IsPrimary),
287-
IsWriteAccess = maps:get(access_type, Opts, read) == write,
295+
IsReadAccess = maps:get(access_type, Opts, raw) == read,
296+
IsWriteAccess = maps:get(access_type, Opts, raw) == write,
288297

289298
ApplyDefaults = maps:get(apply_defaults, Opts, false),
290299
EffectiveValue = case {Input, Prop} of
@@ -315,9 +324,12 @@ encode3(#{type := <<"object">>, properties := Properties} = Schema, #{query := Q
315324
{ok, Null} when (Null == null orelse Null == undefined) andalso
316325
not NullableProp andalso not Patching ->
317326
Obj;
318-
% Silently drop read only fields with write access
327+
% Silently drop readOnly fields on write
319328
{ok, _Value} when IsWriteAccess andalso IsReadOnly andalso (not IsRequired) ->
320329
Obj;
330+
% Silently drop writeOnly fields on read
331+
{ok, _Value} when IsReadAccess andalso IsWriteOnly andalso (not IsRequired) ->
332+
Obj;
321333
{ok, Value} ->
322334
case encode3(Prop#{nullable => NullableProp}, Opts, Value, Path ++ [Field]) of
323335
{error, _} = E ->
@@ -450,6 +462,9 @@ encode3(#{const := Value}, #{auto_convert := Convert}, Input, Path) when is_atom
450462
_ -> {error, #{error => not_const2, path => Path, input => Input, value => Value}}
451463
end;
452464

465+
encode3(#{const := Value}, #{}, Input, Path) ->
466+
{error, #{error => not_const, path => Path, input => Input, value => Value}};
467+
453468
encode3(#{enum := Choices, type := <<"string">>}, #{auto_convert := Convert}, Input, Path) ->
454469
InputValue = case Input of
455470
_ when is_binary(Input) -> Input;
@@ -465,6 +480,7 @@ encode3(#{enum := Choices, type := <<"string">>}, #{auto_convert := Convert}, In
465480
encode3(#{type := <<"string">>} = Spec, #{auto_convert := Convert} = Options, Input, Path) ->
466481
{Input1, InputForValidation} = case Input of
467482
_ when is_binary(Input) -> {Input, Input};
483+
_ when is_boolean(Input) -> {{error, #{error => not_string, path => Path, input => Input}}, undefined};
468484
_ when is_atom(Input) andalso Convert -> {atom_to_binary(Input), atom_to_binary(Input)};
469485
_ when is_atom(Input) -> {Input, atom_to_binary(Input)};
470486
_ -> {{error, #{error => not_string, path => Path, input => Input}}, undefined}
@@ -473,11 +489,12 @@ encode3(#{type := <<"string">>} = Spec, #{auto_convert := Convert} = Options, In
473489
{error, _} ->
474490
Input1;
475491
_ ->
492+
Length = string:length(InputForValidation),
476493
case Spec of
477-
#{minLength := MinLength} when size(InputForValidation) < MinLength ->
478-
{error, #{error => too_short, path => Path, input => Input, min_length => MinLength}};
479-
#{maxLength := MaxLength} when size(InputForValidation) > MaxLength ->
480-
{error, #{error => too_long, path => Path, input => Input, max_length => MaxLength}};
494+
#{minLength := MinLength} when Length < MinLength ->
495+
{error, #{error => too_short, path => Path, input => Input, detail => Length, min_length => MinLength}};
496+
#{maxLength := MaxLength} when Length > MaxLength ->
497+
{error, #{error => too_long, path => Path, input => Input, detail => Length, max_length => MaxLength}};
481498
#{} ->
482499
Format = maps:get(format, Spec, undefined),
483500
Validators = maps:get(validators, Options),

test/example-openapi.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"openapi": "3.1.1",
3+
"info": {
4+
"contact": { "email": "hello@exampl.com", "name": "Test", "url": "https://example.com/" },
5+
"description": "dummy desc",
6+
"title": "Test API",
7+
"version": "0.1.0"
8+
},
9+
"components": {
10+
"schemas": {
11+
"external_validators_simple": {
12+
"type": "string",
13+
"format": "no_space"
14+
},
15+
"external_validators_array": {
16+
"type": "array",
17+
"items": {
18+
"type": "array",
19+
"items": {
20+
"type": "string",
21+
"format": "no_space"
22+
}
23+
}
24+
},
25+
"external_validators_object": {
26+
"type": "object",
27+
"properties": {
28+
"in_object": {
29+
"type": "object",
30+
"properties": {
31+
"prop1": {
32+
"type": "string",
33+
"format": "no_space"
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}
40+
}
41+
}

test/openapi_handler_SUITE.erl

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ start_http(Routes, ApiName) ->
4242

4343

4444
init_per_suite(Config) ->
45+
inets:start(),
4546
{ok, _} = application:ensure_all_started(cowboy),
46-
{ok, _} = application:ensure_all_started(lhttpc),
4747

4848
PetstorePath = filename:join(code:lib_dir(openapi_handler),"test/redocly-petstore.json"),
4949
TestSchemaPath = filename:join(code:lib_dir(openapi_handler),"test/test_schema.json"),
@@ -183,9 +183,9 @@ json_body_parameters(_) ->
183183
broken_json(_) ->
184184
Port = integer_to_list(ranch:get_port(petstore_api_server)),
185185
JSON = "{\"key\":\"value\"]}",
186-
{ok, {{400,_},Headers,Body}} = lhttpc:request("http://127.0.0.1:"++Port++"/test/yml/store/order", post,
187-
[{"Content-Type", "application/json"}], JSON, 5000),
188-
"application/json" = proplists:get_value("Content-Type", Headers),
186+
Request = {"http://127.0.0.1:"++Port++"/test/yml/store/order", [], "application/json", JSON},
187+
{ok, {{_, 400, _}, Headers, Body}} = httpc:request(post, Request, [{timeout, 5000}], [{body_format, binary}]),
188+
"application/json" = proplists:get_value("content-type", Headers),
189189
#{<<"error">> := <<"broken_json">>} = openapi_json:decode(Body),
190190
ok.
191191

@@ -438,18 +438,12 @@ required_keys_filter(_) ->
438438

439439

440440
select_not_filters_required_keys(_) ->
441+
% p3 is 'writeOnly', so it should be dropped in results
441442
#{elements := [Elem1,Elem1]} = openapi_client:call(test_schema_api, selectCollectionFields,
442443
#{json_body => #{p1 => 1, p2 => 2, p3 => 3, p4 => 4, p5 => 5}}),
443-
#{p1 := 1, p2 := 2, p3 := 3, p4 := 4, p5 :=5} = Elem1,
444+
#{p1 := 1, p2 := 2, p4 := 4, p5 :=5} = Elem1,
444445

445-
% p1, p2 are 'readOnly' required keys
446-
#{elements := [Elem2,Elem2]} = openapi_client:call(test_schema_api, selectCollectionFields,
447-
#{json_body => #{p1 => 1, p2 => 2, p3 => 3, p4 => 4, p5 => 5}, select => <<"p3">>}),
448-
#{p1 := 1, p2 := 2, p3 := 3} = Elem2,
449-
undefined = maps:get(p4, Elem2, undefined),
450-
undefined = maps:get(p5, Elem2, undefined),
451-
452-
% p3 is 'writeOnly' required key
446+
% p1, p2 are required keys, so they are returned despite not explicitly requested
453447
#{elements := [Elem3,Elem3]} = openapi_client:call(test_schema_api, selectCollectionFields,
454448
#{json_body => #{p1 => 1, p2 => 2, p3 => 3, p4 => 4, p5 => 5}, select => <<"p4">>}),
455449
#{p1 := 1, p2 := 2, p4 := 4} = Elem3,

0 commit comments

Comments
 (0)