From bb14564488a233f1090d11419ade3d7ea88b5c05 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Wed, 16 Jul 2025 16:45:40 +0300 Subject: [PATCH 01/12] fix a crash when ordinary field behind discriminator has wrong format --- src/openapi_schema.erl | 5 +++-- test/openapi_schema_SUITE.erl | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index 123e0f6..3253d46 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -208,10 +208,11 @@ encode3(#{discriminator := #{propertyName := DKey, mapping := DMap}} = Schema, O #{} -> undefined end end, - ADvalue1 = DefaultFun(Input), + DiscrInput = maps:with([DKey, ADKey], Input), + ADvalue1 = DefaultFun(DiscrInput), ADvalue2 = case ADvalue1 of undefined -> - Try = encode3(hd(Types), Opts#{apply_defaults => true, required_obj_keys => drop}, Input, Path), + Try = encode3(hd(Types), Opts#{apply_defaults => true, required_obj_keys => drop}, DiscrInput, Path), DefaultFun(Try); _ -> ADvalue1 diff --git a/test/openapi_schema_SUITE.erl b/test/openapi_schema_SUITE.erl index 04a03f9..3f2676a 100644 --- a/test/openapi_schema_SUITE.erl +++ b/test/openapi_schema_SUITE.erl @@ -17,6 +17,7 @@ groups() -> nullable_by_oneof, discriminator, discriminator_default_missing_fields, + discriminator_default_wrong_format, recursive_discriminator, non_object_validate, regexp_pattern, @@ -232,6 +233,26 @@ discriminator_default_missing_fields(_) -> ok. +discriminator_default_wrong_format(_) -> + FooProp = #{dis => #{type => <<"string">>}, k1 => #{type => <<"integer">>}, k3 => #{type => <<"integer">>}}, + BarProp = #{dis => #{type => <<"string">>}, k2 => #{type => <<"integer">>}, k3 => #{type => <<"integer">>}}, + FooOwn = #{type => <<"object">>, properties => FooProp, required => [<<"k1">>, <<"k3">>]}, + BarOwn = #{type => <<"object">>, properties => BarProp, required => [<<"k2">>, <<"k3">>]}, + FooType = #{allOf => [#{'$ref' => <<"#/components/schemas/discr_t">>}, FooOwn]}, + BarType = #{allOf => [#{'$ref' => <<"#/components/schemas/discr_t">>}, BarOwn]}, + Mapping = #{foo => <<"#/components/schemas/foo_t">>, bar => <<"#/components/schemas/bar_t">>}, + DSchemaForDis = fun(DisSchema) -> + BaseProp = #{dis => DisSchema}, + DType = #{type => <<"object">>, properties => BaseProp, discriminator => #{propertyName => <<"dis">>, mapping => Mapping}}, + #{components => #{schemas => #{discr_t => DType, foo_t => FooType, bar_t => BarType}}} + end, + DSchema_foo = DSchemaForDis(#{type => <<"string">>, default => <<"foo">>}), + {error, #{error := not_integer}} = openapi_schema:process(#{k3 => [<<"BAD">>]}, #{type => discr_t, whole_schema => DSchema_foo, required_obj_keys => error}), + DSchema_bar = DSchemaForDis(#{type => <<"string">>, default => <<"bar">>}), + {error, #{error := not_integer}} = openapi_schema:process(#{k3 => [<<"BAD">>]}, #{type => discr_t, whole_schema => DSchema_bar, required_obj_keys => error}), + ok. + + non_object_validate(_) -> {error, #{error := not_object}} = openapi_schema:process([<<"123">>], #{schema => #{type => <<"object">>}}), ok. From 2021872523e433a503437cc5fd17039508c72176 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Thu, 28 Aug 2025 16:24:12 +0300 Subject: [PATCH 02/12] openapi_schema: boolean values should not be valid strings --- src/openapi_schema.erl | 1 + test/openapi_schema_SUITE.erl | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index 3253d46..24ba28a 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -466,6 +466,7 @@ encode3(#{enum := Choices, type := <<"string">>}, #{auto_convert := Convert}, In encode3(#{type := <<"string">>} = Spec, #{auto_convert := Convert} = Options, Input, Path) -> {Input1, InputForValidation} = case Input of _ when is_binary(Input) -> {Input, Input}; + _ when is_boolean(Input) -> {{error, #{error => not_string, path => Path, input => Input}}, undefined}; _ when is_atom(Input) andalso Convert -> {atom_to_binary(Input), atom_to_binary(Input)}; _ when is_atom(Input) -> {Input, atom_to_binary(Input)}; _ -> {{error, #{error => not_string, path => Path, input => Input}}, undefined} diff --git a/test/openapi_schema_SUITE.erl b/test/openapi_schema_SUITE.erl index 3f2676a..4584f07 100644 --- a/test/openapi_schema_SUITE.erl +++ b/test/openapi_schema_SUITE.erl @@ -15,6 +15,7 @@ groups() -> extra_keys_drop, null_in_array, nullable_by_oneof, + boolean_vs_string, discriminator, discriminator_default_missing_fields, discriminator_default_wrong_format, @@ -117,6 +118,12 @@ nullable_by_oneof(_) -> Expect2 = openapi_schema:process(#{nk => null}, #{schema => Schema, extra_obj_key => error, apply_defaults => true}), ok. +boolean_vs_string(_) -> + % Even when auto_convert is enabled (default behaviour) consider boolean values not valid strings + {error, _} = openapi_schema:process(true, #{schema => #{type => <<"string">>}}), + {error, _} = openapi_schema:process(false, #{schema => #{type => <<"string">>}}), + ok. + discriminator(_) -> FooProp = #{dis => #{type => <<"string">>}, k1 => #{type => <<"integer">>}, k3 => #{type => <<"integer">>}}, BarProp = #{dis => #{type => <<"string">>}, k2 => #{type => <<"integer">>}, k3 => #{type => <<"integer">>}}, From fc12b1319bb6e259c683033d63d9edab6cdc20a9 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Mon, 1 Sep 2025 13:02:19 +0300 Subject: [PATCH 03/12] fix format validation of array items --- src/openapi_schema.erl | 2 ++ test/openapi_schema_SUITE.erl | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index 24ba28a..e923d91 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -88,6 +88,8 @@ prepare_type(#{oneOf := Types} = Type0) -> Type0#{oneOf := [prepare_type(T) || T <- Types]}; prepare_type(#{type := <<"object">>, properties := Props} = Type0) -> Type0#{properties => maps:map(fun(_, T) -> prepare_type(T) end, Props)}; +prepare_type(#{type := <<"array">>, items := Items} = Type0) -> + Type0#{items => prepare_type(Items)}; prepare_type(#{} = Type0) -> % Convert format name to atom. This matches validators syntax Type1 = case Type0 of diff --git a/test/openapi_schema_SUITE.erl b/test/openapi_schema_SUITE.erl index 4584f07..c3d642c 100644 --- a/test/openapi_schema_SUITE.erl +++ b/test/openapi_schema_SUITE.erl @@ -49,6 +49,8 @@ init_per_suite(Config) -> openapi_handler:load_schema(SchemaPath, test_openapi), BigOpenapiPath = code:lib_dir(openapi_handler) ++ "/test/redocly-big-openapi.json", openapi_handler:load_schema(BigOpenapiPath, big_openapi), + ExamplePath = code:lib_dir(openapi_handler) ++ "/test/example-openapi.json", + openapi_handler:load_schema(ExamplePath, example_openapi), Config. end_per_suite(Config) -> @@ -302,10 +304,18 @@ external_validators(_) -> {error, Err2} = openapi_schema:process(<<" ab cd">>, #{schema => Schema, validators => Validators}), #{error := wrong_format, format := no_space, detail := leading_space} = Err2, - % format validators work with loaded schema - % big_openapi.digest is a composition of allOf and object, so it needs schema to be properly prepared - #{password := <<"ab_cd">>} = openapi_schema:process( - #{username => <<"Joe">>, password => <<"ab cd">>}, #{name => big_openapi, type => digest, validators => #{password => fun no_space_validator/1}}), + % format validators work with loaded schema (to ensure openapi_schema:prepare_type works well) + % as simple value + <<"ab_cd">> = openapi_schema:process(<<"ab cd">>, #{name => example_openapi, type => external_validators_simple, validators => Validators}), + {error, _} = openapi_schema:process(<<" cd">>, #{name => example_openapi, type => external_validators_simple, validators => Validators}), + % as array item (nested) + [[<<"ab_cd">>]] = openapi_schema:process([[<<"ab cd">>]], #{name => example_openapi, type => external_validators_array, validators => Validators}), + {error, _} = openapi_schema:process([[<<" cd">>]], #{name => example_openapi, type => external_validators_array, validators => Validators}), + % as object key (nested) + #{in_object := #{prop1 := <<"ab_cd">>}} = + openapi_schema:process(#{in_object => #{prop1 => <<"ab cd">>}}, #{name => example_openapi, type => external_validators_object, validators => Validators}), + {error, _} = + openapi_schema:process(#{in_object => #{prop1 => <<" cd">>}}, #{name => example_openapi, type => external_validators_object, validators => Validators}), % format validator and pattern work simultaneously Schema2 = #{type => <<"string">>, format => no_space, pattern => <<"^[a-z]+$">>}, From 550b373a1a78b135e9b8e7a25d93e2683878b230 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Mon, 1 Sep 2025 19:18:11 +0300 Subject: [PATCH 04/12] openapi_schema: introduce access_type=raw, fix writeOnly property behaviour --- test/example-openapi.json | 41 ++++++++++++++++++++++++++++++++++ test/openapi_handler_SUITE.erl | 12 +++------- test/openapi_schema_SUITE.erl | 22 ++++++++++++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 test/example-openapi.json diff --git a/test/example-openapi.json b/test/example-openapi.json new file mode 100644 index 0000000..6e997ad --- /dev/null +++ b/test/example-openapi.json @@ -0,0 +1,41 @@ +{ + "openapi": "3.1.1", + "info": { + "contact": { "email": "hello@exampl.com", "name": "Test", "url": "https://example.com/" }, + "description": "dummy desc", + "title": "Test API", + "version": "0.1.0" + }, + "components": { + "schemas": { + "external_validators_simple": { + "type": "string", + "format": "no_space" + }, + "external_validators_array": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string", + "format": "no_space" + } + } + }, + "external_validators_object": { + "type": "object", + "properties": { + "in_object": { + "type": "object", + "properties": { + "prop1": { + "type": "string", + "format": "no_space" + } + } + } + } + } + } + } +} diff --git a/test/openapi_handler_SUITE.erl b/test/openapi_handler_SUITE.erl index b6b227b..027dabb 100644 --- a/test/openapi_handler_SUITE.erl +++ b/test/openapi_handler_SUITE.erl @@ -438,18 +438,12 @@ required_keys_filter(_) -> select_not_filters_required_keys(_) -> + % p3 is 'writeOnly', so it should be dropped in results #{elements := [Elem1,Elem1]} = openapi_client:call(test_schema_api, selectCollectionFields, #{json_body => #{p1 => 1, p2 => 2, p3 => 3, p4 => 4, p5 => 5}}), - #{p1 := 1, p2 := 2, p3 := 3, p4 := 4, p5 :=5} = Elem1, + #{p1 := 1, p2 := 2, p4 := 4, p5 :=5} = Elem1, - % p1, p2 are 'readOnly' required keys - #{elements := [Elem2,Elem2]} = openapi_client:call(test_schema_api, selectCollectionFields, - #{json_body => #{p1 => 1, p2 => 2, p3 => 3, p4 => 4, p5 => 5}, select => <<"p3">>}), - #{p1 := 1, p2 := 2, p3 := 3} = Elem2, - undefined = maps:get(p4, Elem2, undefined), - undefined = maps:get(p5, Elem2, undefined), - - % p3 is 'writeOnly' required key + % p1, p2 are required keys, so they are returned despite not explicitly requested #{elements := [Elem3,Elem3]} = openapi_client:call(test_schema_api, selectCollectionFields, #{json_body => #{p1 => 1, p2 => 2, p3 => 3, p4 => 4, p5 => 5}, select => <<"p4">>}), #{p1 := 1, p2 := 2, p4 := 4} = Elem3, diff --git a/test/openapi_schema_SUITE.erl b/test/openapi_schema_SUITE.erl index c3d642c..9f5df55 100644 --- a/test/openapi_schema_SUITE.erl +++ b/test/openapi_schema_SUITE.erl @@ -35,6 +35,7 @@ groups() -> check_explain_on_error, one_of_integer_const, one_of_const_default, + drop_unidirectional_keys, filter_read_only_props ]}, {introspection, [], [ @@ -432,6 +433,27 @@ one_of_const_default(_) -> #{schema => FooType, apply_defaults => true}), ok. + +drop_unidirectional_keys(_) -> + Schema = #{ + type => <<"object">>, + properties => #{ + pn => #{type => <<"integer">>}, + pr => #{type => <<"integer">>, readOnly => true}, + pw => #{type => <<"integer">>, writeOnly => true} + } + }, + % 'raw' access does not involve any read/write filtering logic + ObjRaw = openapi_schema:process(#{pn => 1, pr => 2, pw => 3}, #{schema => Schema, extra_obj_key => drop, access_type => raw}), + [pn, pr, pw] = lists:sort(maps:keys(ObjRaw)), + % 'write' access drops readOnly properties + ObjW = openapi_schema:process(#{pn => 1, pr => 2, pw => 3}, #{schema => Schema, extra_obj_key => drop, access_type => write}), + [pn, pw] = lists:sort(maps:keys(ObjW)), + % 'read' access drops writeOnly properties + ObjR = openapi_schema:process(#{pn => 1, pr => 2, pw => 3}, #{schema => Schema, extra_obj_key => drop, access_type => read}), + [pn, pr] = lists:sort(maps:keys(ObjR)), + ok. + filter_read_only_props(_) -> Schema = persistent_term:get({openapi_handler_schema,test_openapi}), Json = #{ From f0b66cafd57e8613e9a83f5b00942f91bbb10b96 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Mon, 1 Sep 2025 19:19:24 +0300 Subject: [PATCH 05/12] format validation test: add forgotten fixture --- src/openapi_schema.erl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index e923d91..de32c38 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -47,7 +47,7 @@ process(Input, #{} = Opts) -> (patch,Flag) when Flag == true; Flag == false -> ok; (extra_obj_key,Flag) when Flag == drop; Flag == error -> ok; (required_obj_keys,Flag) when Flag == drop; Flag == error -> ok; - (access_type,Flag) when Flag == read; Flag == write -> ok; + (access_type,Flag) when Flag == read; Flag == write; Flag == raw -> ok; (explain,FlagList) -> check_explain_keys(FlagList); (K,V) -> error({unknown_option,K,V}) end, Opts), @@ -285,9 +285,11 @@ encode3(#{type := <<"object">>, properties := Properties} = Schema, #{query := Q RequiredKeys = get_required_keys(Schema, Opts), IsReadOnly = maps:get(readOnly, Prop, false), + IsWriteOnly = maps:get(writeOnly, Prop, false), IsPrimary = maps:get('x-primary-key', Prop, false), IsRequired = (lists:member(FieldBin, RequiredKeys) orelse IsPrimary), - IsWriteAccess = maps:get(access_type, Opts, read) == write, + IsReadAccess = maps:get(access_type, Opts, raw) == read, + IsWriteAccess = maps:get(access_type, Opts, raw) == write, ApplyDefaults = maps:get(apply_defaults, Opts, false), EffectiveValue = case {Input, Prop} of @@ -318,9 +320,12 @@ encode3(#{type := <<"object">>, properties := Properties} = Schema, #{query := Q {ok, Null} when (Null == null orelse Null == undefined) andalso not NullableProp andalso not Patching -> Obj; - % Silently drop read only fields with write access + % Silently drop readOnly fields on write {ok, _Value} when IsWriteAccess andalso IsReadOnly andalso (not IsRequired) -> Obj; + % Silently drop writeOnly fields on read + {ok, _Value} when IsReadAccess andalso IsWriteOnly andalso (not IsRequired) -> + Obj; {ok, Value} -> case encode3(Prop#{nullable => NullableProp}, Opts, Value, Path ++ [Field]) of {error, _} = E -> From 17aa3830e830477027755dc0d09be407d7c92394 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Thu, 4 Sep 2025 18:16:22 +0300 Subject: [PATCH 06/12] openapi_schema: fix error when passed discriminator has wrong type --- src/openapi_schema.erl | 6 +++++- test/openapi_schema_SUITE.erl | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index de32c38..95bfd6e 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -207,7 +207,8 @@ encode3(#{discriminator := #{propertyName := DKey, mapping := DMap}} = Schema, O try binary_to_existing_atom(DValue) catch error:badarg -> DValue end; #{ADKey := DValue} when is_binary(DValue) -> try binary_to_existing_atom(DValue) catch error:badarg -> DValue end; - #{} -> undefined + #{} -> undefined; + {error, _} -> Input_ end end, DiscrInput = maps:with([DKey, ADKey], Input), @@ -225,6 +226,9 @@ encode3(#{discriminator := #{propertyName := DKey, mapping := DMap}} = Schema, O encode3(maps:without([discriminator], Schema), Opts, Input, Path); {undefined, _} -> {error, #{error => discriminator_missing, path => Path, propertyName => DKey}}; + {{error, _}, _} -> + % this error comes from processing discriminator with first possible type, and may contain useful type error (e.g. not_string) + ADvalue2; {_, undefined} -> {error, #{error => discriminator_unmapped, path => Path, propertyName => DKey, value => ADvalue2}}; {_, _} -> diff --git a/test/openapi_schema_SUITE.erl b/test/openapi_schema_SUITE.erl index 9f5df55..1000973 100644 --- a/test/openapi_schema_SUITE.erl +++ b/test/openapi_schema_SUITE.erl @@ -165,6 +165,8 @@ discriminator(_) -> Foo5 = openapi_schema:process(#{k1 => 12, k2 => 34, k4 => 56}, #{type => discr_t, whole_schema => DSchema1}), [k1, k4] = lists:sort(maps:keys(Foo5)), % apply default + {error, #{error := not_string}} = openapi_schema:process(#{dis => 42}, #{type => discr_t, query => true, whole_schema => DSchema}), + ok. % See example without oneOf at https://spec.openapis.org/oas/v3.1.0.html#discriminator-object @@ -215,6 +217,8 @@ recursive_discriminator(_) -> DSchema3b = DSchemaForDis(#{type => <<"string">>, default => <<"bar">>}), #{dis := bar, k2 := 22} = openapi_schema:process(#{}, #{type => discr_t, whole_schema => DSchema3b, apply_defaults => true}), + {error, #{error := not_string}} = openapi_schema:process(#{dis => 42}, #{type => discr_t, whole_schema => DSchema3b, apply_defaults => true}), + [] = maps:keys(openapi_schema:process(#{}, #{type => discr_t, whole_schema => DSchema3b})), [k2, k3] = maps:keys(openapi_schema:process(#{k2 => 11, k3 => 435}, #{type => discr_t, whole_schema => DSchema3b})), % k1 from foo_t is handled like dis=bar From fa555b6f6b87d0b0b061e318fc59c3bafc94864b Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 22 Sep 2025 14:59:49 +0700 Subject: [PATCH 07/12] openapi_schema: extend error message in string length restriction cases * too_short * too_long --- src/openapi_schema.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index 95bfd6e..3dfc933 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -488,9 +488,9 @@ encode3(#{type := <<"string">>} = Spec, #{auto_convert := Convert} = Options, In _ -> case Spec of #{minLength := MinLength} when size(InputForValidation) < MinLength -> - {error, #{error => too_short, path => Path, input => Input, min_length => MinLength}}; + {error, #{error => too_short, path => Path, input => Input, detail => length(Input), min_length => MinLength}}; #{maxLength := MaxLength} when size(InputForValidation) > MaxLength -> - {error, #{error => too_long, path => Path, input => Input, max_length => MaxLength}}; + {error, #{error => too_long, path => Path, input => Input, detail => length(Input), max_length => MaxLength}}; #{} -> Format = maps:get(format, Spec, undefined), Validators = maps:get(validators, Options), From f09a14f13e98ff3cbcb5bff0417577a17123b58c Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Fri, 26 Sep 2025 18:45:10 +0300 Subject: [PATCH 08/12] openapi_schema: fix crash when validating array/map vs const --- src/openapi_schema.erl | 3 +++ test/openapi_schema_SUITE.erl | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index 3dfc933..c9a6d27 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -462,6 +462,9 @@ encode3(#{const := Value}, #{auto_convert := Convert}, Input, Path) when is_atom _ -> {error, #{error => not_const2, path => Path, input => Input, value => Value}} end; +encode3(#{const := Value}, #{}, Input, Path) -> + {error, #{error => not_const, path => Path, input => Input, value => Value}}; + encode3(#{enum := Choices, type := <<"string">>}, #{auto_convert := Convert}, Input, Path) -> InputValue = case Input of _ when is_binary(Input) -> Input; diff --git a/test/openapi_schema_SUITE.erl b/test/openapi_schema_SUITE.erl index 1000973..39efc6e 100644 --- a/test/openapi_schema_SUITE.erl +++ b/test/openapi_schema_SUITE.erl @@ -34,6 +34,7 @@ groups() -> check_explain, check_explain_on_error, one_of_integer_const, + one_of_const_wrong_type, one_of_const_default, drop_unidirectional_keys, filter_read_only_props @@ -437,6 +438,12 @@ one_of_const_default(_) -> #{schema => FooType, apply_defaults => true}), ok. +one_of_const_wrong_type(_) -> + FooProp = #{k1 => #{oneOf => [#{const => <<"hello">>}, #{const => <<"world">>}]}}, + FooType = #{type => <<"object">>, properties => FooProp}, + {error, #{error := not_const}} = openapi_schema:process(#{k1 => #{}}, #{schema => FooType}), + {error, #{error := not_const}} = openapi_schema:process(#{k1 => []}, #{schema => FooType}), + ok. drop_unidirectional_keys(_) -> Schema = #{ From 3ba37b427c668e1e5c8c861ee2b7492cb3016f99 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Fri, 26 Sep 2025 19:05:17 +0300 Subject: [PATCH 09/12] openapi_schema: count UTF8 code points when checking string length constraints --- src/openapi_schema.erl | 9 +++++---- test/openapi_schema_SUITE.erl | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/openapi_schema.erl b/src/openapi_schema.erl index 3dfc933..a2af761 100644 --- a/src/openapi_schema.erl +++ b/src/openapi_schema.erl @@ -486,11 +486,12 @@ encode3(#{type := <<"string">>} = Spec, #{auto_convert := Convert} = Options, In {error, _} -> Input1; _ -> + Length = string:length(InputForValidation), case Spec of - #{minLength := MinLength} when size(InputForValidation) < MinLength -> - {error, #{error => too_short, path => Path, input => Input, detail => length(Input), min_length => MinLength}}; - #{maxLength := MaxLength} when size(InputForValidation) > MaxLength -> - {error, #{error => too_long, path => Path, input => Input, detail => length(Input), max_length => MaxLength}}; + #{minLength := MinLength} when Length < MinLength -> + {error, #{error => too_short, path => Path, input => Input, detail => Length, min_length => MinLength}}; + #{maxLength := MaxLength} when Length > MaxLength -> + {error, #{error => too_long, path => Path, input => Input, detail => Length, max_length => MaxLength}}; #{} -> Format = maps:get(format, Spec, undefined), Validators = maps:get(validators, Options), diff --git a/test/openapi_schema_SUITE.erl b/test/openapi_schema_SUITE.erl index 1000973..cd27b87 100644 --- a/test/openapi_schema_SUITE.erl +++ b/test/openapi_schema_SUITE.erl @@ -333,10 +333,21 @@ external_validators(_) -> min_max_length(_) -> - {error, #{error := too_short}} = + {error, #{error := too_short, detail := 3}} = openapi_schema:process(<<"123">>, #{schema => #{type => <<"string">>, minLength => 5}}), - {error, #{error := too_long}} = + {error, #{error := too_long, detail := 3}} = openapi_schema:process(<<"123">>, #{schema => #{type => <<"string">>, maxLength => 2}}), + + {error, #{error := too_short, detail := 4}} = + openapi_schema:process(atom, #{schema => #{type => <<"string">>, minLength => 5}}), + {error, #{error := too_long, detail := 4}} = + openapi_schema:process(atom, #{schema => #{type => <<"string">>, maxLength => 2}}), + + UTFString = unicode:characters_to_binary([128578,128579]), % 2 code points, but 8 bytes + {error, #{error := too_short, detail := 2}} = + openapi_schema:process(UTFString, #{schema => #{type => <<"string">>, minLength => 3}}), + {error, #{error := too_long, detail := 2}} = + openapi_schema:process(UTFString, #{schema => #{type => <<"string">>, maxLength => 1}}), ok. From 600a2438072a9f1fbd41fcf99bc6e36e65bd9ece Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Fri, 26 Sep 2025 22:16:26 +0300 Subject: [PATCH 10/12] reduce test dependencies --- rebar.config_ | 3 --- src/openapi_client.erl | 11 +++++++++-- test/openapi_handler_SUITE.erl | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/rebar.config_ b/rebar.config_ index fc2641b..afc56eb 100644 --- a/rebar.config_ +++ b/rebar.config_ @@ -5,10 +5,7 @@ {profiles, [ {dev, [ {deps, [ - jsx, - yamerl, {cowboy, "2.12.0"}, - {lhttpc, {git, "https://github.com/erlyvideo/lhttpc.git", {branch, "master"}}}, redbug ]}, {ct_opts, [{ct_hooks, [cth_surefire]}]} diff --git a/src/openapi_client.erl b/src/openapi_client.erl index b0ec0b1..f1ea680 100644 --- a/src/openapi_client.erl +++ b/src/openapi_client.erl @@ -62,8 +62,15 @@ call(#{schema := Schema, uri := URI} = State, OperationId, Args0, Opts) when is_ _ -> RequestBody end]), Timeout = proplists:get_value(timeout, Opts, 50000), - Result = case lhttpc:request(RequestURL, Method, RequestHeaders, RequestBody, Timeout) of - {ok, {{Code0,_},ResponseHeaders0,Bin0}} -> + Request = case Method of + get -> + {RequestURL, RequestHeaders}; + _ -> + ContentType = proplists:get_value("Content-Type", RequestHeaders, "text/plain"), + {RequestURL, RequestHeaders, ContentType, RequestBody} + end, + Result = case httpc:request(Method, Request, [{timeout, Timeout}], [{body_format, binary}]) of + {ok, {{_, Code0, _}, ResponseHeaders0, Bin0}} -> {ok, Code0, [{string:to_lower(K),V} || {K,V} <- ResponseHeaders0], Bin0}; {error, E0} -> {error, E0} diff --git a/test/openapi_handler_SUITE.erl b/test/openapi_handler_SUITE.erl index 027dabb..66e6c8b 100644 --- a/test/openapi_handler_SUITE.erl +++ b/test/openapi_handler_SUITE.erl @@ -42,8 +42,8 @@ start_http(Routes, ApiName) -> init_per_suite(Config) -> + inets:start(), {ok, _} = application:ensure_all_started(cowboy), - {ok, _} = application:ensure_all_started(lhttpc), PetstorePath = filename:join(code:lib_dir(openapi_handler),"test/redocly-petstore.json"), TestSchemaPath = filename:join(code:lib_dir(openapi_handler),"test/test_schema.json"), @@ -183,9 +183,9 @@ json_body_parameters(_) -> broken_json(_) -> Port = integer_to_list(ranch:get_port(petstore_api_server)), JSON = "{\"key\":\"value\"]}", - {ok, {{400,_},Headers,Body}} = lhttpc:request("http://127.0.0.1:"++Port++"/test/yml/store/order", post, - [{"Content-Type", "application/json"}], JSON, 5000), - "application/json" = proplists:get_value("Content-Type", Headers), + Request = {"http://127.0.0.1:"++Port++"/test/yml/store/order", [], "application/json", JSON}, + {ok, {{_, 400, _}, Headers, Body}} = httpc:request(post, Request, [{timeout, 5000}], [{body_format, binary}]), + "application/json" = proplists:get_value("content-type", Headers), #{<<"error">> := <<"broken_json">>} = openapi_json:decode(Body), ok. From 56e562bd13132cae5e1802e4cc0b87554010de84 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Fri, 26 Sep 2025 03:04:08 +0300 Subject: [PATCH 11/12] define tests workflow --- .github/workflows/tests.yml | 51 +++++++++++++++++++++++++++++++++++++ rebar.config.pre27_ | 14 ++++++++++ 2 files changed, 65 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 rebar.config.pre27_ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..819b668 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,51 @@ +name: Erlang CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + common_test: + runs-on: ubuntu-latest + strategy: + matrix: + otp_ver: [27, 28, latest] + container: + image: erlang:${{ matrix.otp_ver }} + + steps: + - uses: actions/checkout@v4 + - name: Run tests + env: + REBAR_CONFIG: rebar.config_ + run: rebar3 as dev ct --logdir test-logs --readable true -vv + - name: upload CT report + uses: actions/upload-artifact@v4 + with: + name: ct_report_${{ matrix.otp_ver }} + path: test-logs/ct_run*/ + + common_test_legacy: + runs-on: ubuntu-latest + strategy: + matrix: + otp_ver: [24, 25, 26] + container: + image: erlang:${{ matrix.otp_ver }} + + steps: + - uses: actions/checkout@v4 + - name: Run tests + env: + REBAR_CONFIG: rebar.config.pre27_ + run: rebar3 as dev ct --logdir test-logs --readable true -vv + - name: upload CT report + uses: actions/upload-artifact@v4 + with: + name: ct_report_${{ matrix.otp_ver }} + path: test-logs/ct_run*/ diff --git a/rebar.config.pre27_ b/rebar.config.pre27_ new file mode 100644 index 0000000..78913c2 --- /dev/null +++ b/rebar.config.pre27_ @@ -0,0 +1,14 @@ +% No deps by default to prevent conflicts +{deps, [ +]}. + +{profiles, [ + {dev, [ + {deps, [ + jsx, + {cowboy, "2.12.0"}, + redbug + ]}, + {ct_opts, [{ct_hooks, [cth_surefire]}]} + ]} +]}. From a8dc196355074432f6af0dabb34b2ae94cadfcd9 Mon Sep 17 00:00:00 2001 From: Danil Zagoskin Date: Fri, 10 Oct 2025 15:53:44 +0300 Subject: [PATCH 12/12] openapi_client: configurable http request function --- src/openapi_client.erl | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/openapi_client.erl b/src/openapi_client.erl index f1ea680..da560d4 100644 --- a/src/openapi_client.erl +++ b/src/openapi_client.erl @@ -61,20 +61,8 @@ call(#{schema := Schema, uri := URI} = State, OperationId, Args0, Opts) when is_ #{raw_body := _} -> <<"raw_file_upload">>; _ -> RequestBody end]), - Timeout = proplists:get_value(timeout, Opts, 50000), - Request = case Method of - get -> - {RequestURL, RequestHeaders}; - _ -> - ContentType = proplists:get_value("Content-Type", RequestHeaders, "text/plain"), - {RequestURL, RequestHeaders, ContentType, RequestBody} - end, - Result = case httpc:request(Method, Request, [{timeout, Timeout}], [{body_format, binary}]) of - {ok, {{_, Code0, _}, ResponseHeaders0, Bin0}} -> - {ok, Code0, [{string:to_lower(K),V} || {K,V} <- ResponseHeaders0], Bin0}; - {error, E0} -> - {error, E0} - end, + HttpRequestFun = proplists:get_value(http_request_fun, Opts, fun http_request_httpc/5), + Result = HttpRequestFun(Method, RequestURL, RequestHeaders, RequestBody, Opts), case Result of {ok, Code,ResponseHeaders,Bin} when is_map_key(Code, Responses) -> @@ -137,6 +125,31 @@ call(#{} = State, OperationId, Args, Opts) -> end. +-spec http_request_httpc( + Method :: get | post | put | patch | delete, + URL :: uri_string:uri_string(), + Headers :: [{string(), string()}], + Body :: binary(), + Opts :: proplists:proplist() +) -> + {ok, Status :: integer(), Headers :: [{NameLowercase :: string(), string()}], Body :: binary()} | + {error, any()}. + +http_request_httpc(Method, RequestURL, RequestHeaders, RequestBody, Opts) -> + Timeout = proplists:get_value(timeout, Opts, 50000), + Request = case Method of + get -> + {RequestURL, RequestHeaders}; + _ -> + ContentType = proplists:get_value("Content-Type", RequestHeaders, "text/plain"), + {RequestURL, RequestHeaders, ContentType, RequestBody} + end, + case httpc:request(Method, Request, [{timeout, Timeout}], [{body_format, binary}]) of + {ok, {{_, Code0, _}, ResponseHeaders0, Bin0}} -> + {ok, Code0, [{string:to_lower(K), V} || {K, V} <- ResponseHeaders0], Bin0}; + {error, E0} -> + {error, E0} + end. search_operation(OperationId, #{paths := Paths}) ->