diff --git a/rebar.config b/rebar.config index 9d1d2b1..f7f9e74 100644 --- a/rebar.config +++ b/rebar.config @@ -4,31 +4,12 @@ {deps, [ {buoy, {git, "https://github.com/lpgauth/buoy.git", {branch, "master"}}}, - {erf, {git, "https://github.com/nomasystems/erf.git", {tag, "0.1.1"}}}, - {ndto, {git, "https://github.com/nomasystems/ndto.git", {tag, "0.1.0"}}}, + {erf, {git, "https://github.com/nomasystems/erf.git", {tag, "0.1.2"}}}, + {ndto, {git, "https://github.com/nomasystems/ndto.git", {tag, "0.3.1"}}}, {njson, {git, "https://github.com/nomasystems/njson.git", {branch, "main"}}}, {triq, {git, "https://github.com/nomasystems/triq.git", {branch, "master"}}} ]}. -{overrides, [ - {override, erf, [ - {plugins, [ - {rebar3_ndto, {git, "https://github.com/nomasystems/rebar3_ndto.git", {tag, "0.1.0"}}} - ]} - ]}, - {override, rebar3_ndto, [ - {deps, [ - {ndto, {git, "https://github.com/nomasystems/ndto.git", {tag, "0.1.0"}}} - ]} - ]}, - {override, ndto, [ - {deps, [ - {ncalendar, {git, "https://github.com/nomasystems/ncalendar.git", {tag, "0.3.0"}}}, - {njson, {git, "https://github.com/nomasystems/njson.git", {tag, "1.0.0"}}} - ]} - ]} -]}. - {project_plugins, [ {erlfmt, {git, "https://github.com/WhatsApp/erlfmt.git", {branch, "main"}}}, {gradualizer, {git, "https://github.com/josefs/Gradualizer.git", {branch, "master"}}}, @@ -90,13 +71,9 @@ restcheck_triq ]}. -%% TODO: address this {gradualizer_opts, [ {exclude, [ "src/restcheck.erl", - "src/restcheck_client.erl", - "src/restcheck_schema.erl", - "src/restcheck_suite.erl", - "src/restcheck_triq.erl" + "src/restcheck_client.erl" ]} ]}. diff --git a/rebar.lock b/rebar.lock index 661b4bf..1073ee9 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,7 +1,7 @@ {"1.2.0", [{<<"buoy">>, {git,"https://github.com/lpgauth/buoy.git", - {ref,"410180cecd8e279a0b2d9dad291925516920e965"}}, + {ref,"ed7ec83750600325dc0ac0016303092d8d21cbf3"}}, 0}, {<<"elli">>, {git,"https://github.com/elli-lib/elli.git", @@ -9,40 +9,40 @@ 1}, {<<"erf">>, {git,"https://github.com/nomasystems/erf.git", - {ref,"494a47b9fc638d64900cb96abbdea94fae62766f"}}, + {ref,"0dac504f9988d3b96154c1e80b8af3defb772b58"}}, 0}, - {<<"foil">>, - {git,"https://github.com/lpgauth/foil.git", - {ref,"b55c30ff8b21daf89a85d1565d4b836c7f9e549b"}}, - 1}, - {<<"granderl">>, - {git,"https://github.com/tokenrove/granderl.git", - {ref,"baafd1bc825cb1fc022760eae913f774fa6af91b"}}, - 2}, - {<<"metal">>,{pkg,<<"metal">>,<<"0.1.1">>},2}, + {<<"foil">>,{pkg,<<"foil">>,<<"0.1.6">>},1}, + {<<"knot">>,{pkg,<<"knot">>,<<"0.1.2">>},2}, + {<<"metal">>,{pkg,<<"metal">>,<<"0.1.2">>},2}, {<<"ncalendar">>, {git,"https://github.com/nomasystems/ncalendar.git", - {ref,"0237766de898145bbd55bb26eef8917535f341ca"}}, + {ref,"aa5615f6723585e45e82fa5524cb976cdfe3d7f7"}}, 1}, {<<"ndto">>, {git,"https://github.com/nomasystems/ndto.git", - {ref,"491a2441e43afa2fb037c6e7e826c45a383e3bd9"}}, + {ref,"295281b72ea4ac85e7c4d5ca42337c68b1aac137"}}, 0}, {<<"njson">>, {git,"https://github.com/nomasystems/njson.git", - {ref,"76ab40033ee977f876e7b3addca5de981ff4a9ef"}}, + {ref,"541754472e8d9713fe8a320a7007c15015095c45"}}, 0}, - {<<"shackle">>, - {git,"https://github.com/lpgauth/shackle.git", - {ref,"a4f7d82d10115cf0d676582b638a56260be685d5"}}, - 1}, + {<<"shackle">>,{pkg,<<"shackle">>,<<"0.7.3">>},1}, + {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.4.2">>},1}, {<<"triq">>, {git,"https://github.com/nomasystems/triq.git", {ref,"18b8198de2f19ac3a99236eead774f9934d727be"}}, 0}]}. [ {pkg_hash,[ - {<<"metal">>, <<"5D3D1322DA7BCD34B94FED5486F577973685298883954F7A3E517EF5EF6953F5">>}]}, + {<<"foil">>, <<"31A9048E8EC6E8AC951ED3C4E4EB8F549465920C70F19E92CD9FC8A6867E7253">>}, + {<<"knot">>, <<"3B6C07CD24831D62752D879F031BCEDCFADB343738679EB1774A029D0A8B7D48">>}, + {<<"metal">>, <<"7501F14B075380F829498C4CE605643D6C7F0C55F60EBFCFBF694C33A558DF98">>}, + {<<"shackle">>, <<"DEDE0777BC297B212DE3DBE4377B391DA991FC011B4D5201960A5C4F11E83B2C">>}, + {<<"telemetry">>, <<"A0CB522801DFFB1C49FE6E30561BADFFC7B6D0E180DB1300DF759FAA22062855">>}]}, {pkg_hash_ext,[ - {<<"metal">>, <<"88B82B634998A1A768DEDCD372C2F7E657B19445325C0AF5CCBAC62C77210F1D">>}]} + {<<"foil">>, <<"8D34B12D712FFB30AD9E192F57885ABB1A61510CD3D9F6AA44470510A50B979E">>}, + {<<"knot">>, <<"42CB91A8024A8857D70A61B33E83D31370D5EB48681B366AE1203CB7C7CCFB38">>}, + {<<"metal">>, <<"CAC84E5CF2875B56E68874F3F0B3D598AF3EC30397EC2151D5C81C408961E6E4">>}, + {<<"shackle">>, <<"056944FEEB85ED0F3E1B7505FBC7174CDE0788993A19E9ADA85137A23CA56349">>}, + {<<"telemetry">>, <<"928F6495066506077862C0D1646609EED891A4326BEE3126BA54B60AF61FEBB1">>}]} ]. diff --git a/src/restcheck.erl b/src/restcheck.erl index 97c9551..5baa369 100644 --- a/src/restcheck.erl +++ b/src/restcheck.erl @@ -33,11 +33,14 @@ spec_parser => module(), pbt_backend => restcheck_pbt:backend(), host => binary(), + base_path => binary(), port => inet:port_number(), ssl => boolean(), auth => restcheck_client:auth(), timeout => non_neg_integer(), num_requests => pos_integer(), + max_string_length => pos_integer(), + max_array_items => non_neg_integer(), output_fun => restcheck_pbt:output_fun() }. -type test_result() :: {OperationId :: binary(), Result :: ok | {error, Reason :: term()}}. @@ -83,7 +86,7 @@ init(State) -> "usage:\n" "{restcheck, [\n" " {spec_path, string()}, % path to spec file\n" - " {spec_parser, module()}, % spec to API AST parser, defaults to erf_oas_3_0\n" + " {spec_parser, module()}, % spec to API AST parser, defaults to erf_parser_oas_3_0\n" " {pbt_backend, module()}, % restcheck_pbt backend, defaults to restcheck_triq\n" " {host, string()}, % server host, defaults to \"localhost\"\n" " {port, inet:port_number()}, % server port number, defaults to 8080\n" @@ -91,6 +94,8 @@ init(State) -> " {auth, restcheck_client:auth()}, % includes auth headers in the request\n" " {timeout, pos_integer()} % timeout per request in ms, defaults to 5000\n" " {num_requests, pos_integer()} % number of requests per operation, defaults to 5000\n" + " {max_string_length, pos_integer()} % default max length for strings without maxLength, defaults to 255\n" + " {max_array_items, non_neg_integer()} % default max items for arrays without maxItems, defaults to 3\n" " {log_file, string()} % path to file where timestamped events are logged, disabled if no path is provided\n" "]}."} ]), @@ -106,14 +111,17 @@ do(State) -> RawConf = rebar_state:get(State, restcheck, []), Conf = #{ spec_path => unicode:characters_to_binary(proplists:get_value(spec_path, RawConf)), - spec_parser => proplists:get_value(spec_parser, RawConf, erf_oas_3_0), + spec_parser => proplists:get_value(spec_parser, RawConf, erf_parser_oas_3_0), pbt_backend => proplists:get_value(pbt_backend, RawConf, restcheck_triq), host => unicode:characters_to_binary(proplists:get_value(host, RawConf, "localhost")), port => proplists:get_value(port, RawConf, 8080), ssl => proplists:get_value(ssl, RawConf, false), + base_path => unicode:characters_to_binary(proplists:get_value(base_path, RawConf, "")), timeout => proplists:get_value(timeout, RawConf, 5000), num_requests => proplists:get_value(num_requests, RawConf, 100), - auth => proplists:get_value(auth, RawConf, undefined) + auth => proplists:get_value(auth, RawConf, undefined), + max_string_length => proplists:get_value(max_string_length, RawConf, undefined), + max_array_items => proplists:get_value(max_array_items, RawConf, undefined) }, LogFile = proplists:get_value(log_file, RawConf, undefined), LogEnabled = @@ -142,7 +150,8 @@ do(State) -> ClientConf = #{ host => Host, port => Port, - ssl => SSL + ssl => SSL, + base_path => maps:get(base_path, Conf, <<>>) }, {ok, _Pid} = restcheck_client:start_link(ClientName, ClientConf), case LogEnabled of @@ -289,7 +298,8 @@ run(Conf) -> ClientConf = #{ host => Host, port => Port, - ssl => SSL + ssl => SSL, + base_path => maps:get(base_path, Conf, <<>>) }, {ok, _Pid} = restcheck_client:start_link(ClientName, ClientConf), TestResults = lists:map( @@ -333,8 +343,9 @@ file_log(LogFile, Format, Args) -> LazyTest :: fun(() -> true | {false, Reason :: term()}), Reason :: term(). generate_and_load_suite(Conf) -> + ok = apply_generation_conf(Conf), SpecPath = maps:get(spec_path, Conf), - SpecParser = maps:get(spec_format, Conf, erf_oas_3_0), + SpecParser = maps:get(spec_format, Conf, erf_parser_oas_3_0), case erf_parser:parse(SpecPath, SpecParser) of {ok, API} -> APIName = maps:get(name, API), @@ -377,6 +388,21 @@ generate_and_load_suite(Conf) -> {error, Reason} end. +-spec apply_generation_conf(Conf) -> ok when + Conf :: conf(). +apply_generation_conf(Conf) -> + lists:foreach( + fun(Key) -> + case maps:get(Key, Conf, undefined) of + undefined -> + application:unset_env(restcheck, Key); + Value -> + application:set_env(restcheck, Key, Value) + end + end, + [max_string_length, max_array_items] + ). + -spec load_dtos(DTOs) -> Result when DTOs :: [{erf_parser:ref(), ndto:schema()}], Result :: ok. diff --git a/src/restcheck_client.erl b/src/restcheck_client.erl index c9dfdff..e0fd570 100644 --- a/src/restcheck_client.erl +++ b/src/restcheck_client.erl @@ -51,7 +51,8 @@ -type client_config() :: #{ host := binary(), port => integer(), - ssl => boolean() + ssl => boolean(), + base_path => binary() }. -type req_config() :: #{ headers => [{binary(), binary()}], @@ -96,6 +97,8 @@ {send_timeout, 50}, {send_timeout_close, true} ]). +-define(BUOY_MAX_RETRIES, 20). +-define(BUOY_RETRY_INTERVAL, 100). %%%----------------------------------------------------------------------------- %%% START/STOP EXPORTS @@ -110,8 +113,7 @@ start_link(Name, ClientConfig) -> -spec stop(Name) -> Result when Name :: atom(), - Result :: ok | {error, Reason}, - Reason :: term(). + Result :: ok | {error, term()}. %% @doc Stops the client with given name. stop(Name) -> case persistent_term:get(?PERSISTENT_TERM(Name), undefined) of @@ -156,9 +158,8 @@ stop(Name) -> Name :: atom(), Config :: req_config(), Opts :: req_opts(), - Result :: {ok, Response} | {error, Reason}, - Response :: response(), - Reason :: term(). + Result :: {ok, Response} | {error, term()}, + Response :: response(). %% @doc Sends a request. request(Name, Config, Opts) -> case persistent_term:get(?PERSISTENT_TERM(Name), undefined) of @@ -170,7 +171,8 @@ request(Name, Config, Opts) -> Host = maps:get(host, ClientConfig), Port = maps:get(port, ClientConfig), Protocol = protocol(maps:get(ssl, ClientConfig, false)), - RawPath = maps:get(path, Config, <<"/">>), + BasePath = maps:get(base_path, ClientConfig, <<>>), + RawPath = <>))/binary>>, Path = case maps:get(query_parameters, Config, undefined) of undefined -> @@ -248,23 +250,7 @@ request(Name, Config, Opts) -> headers => Headers, timeout => Timeout }, - case buoy:request(Method, BuoyUrl, BuoyOpts) of - {ok, BuoyResp} -> - RespStatus = BuoyResp#buoy_resp.status_code, - RespHeaders = headers(BuoyResp#buoy_resp.headers), - RespBody = body( - proplists:get_value(<<"content-type">>, RespHeaders, undefined), - BuoyResp#buoy_resp.body - ), - Response = #{ - status => RespStatus, - headers => RespHeaders, - body => RespBody - }, - {ok, Response}; - {error, Reason} -> - {error, Reason} - end + buoy_request(Method, BuoyUrl, BuoyOpts, ?BUOY_MAX_RETRIES) end. %%%----------------------------------------------------------------------------- @@ -289,7 +275,7 @@ init([Name, ClientConfig]) -> http -> ?SOCKET_OPTIONS; https -> - [{log_level, error} | ?SOCKET_OPTIONS] + [{verify, verify_none}, {log_level, error} | ?SOCKET_OPTIONS] end, case buoy_pool:start(BuoyUrl, [{socket_options, SocketOptions}]) of ok -> @@ -301,13 +287,43 @@ init([Name, ClientConfig]) -> %%%----------------------------------------------------------------------------- %%% INTERNAL FUNCTIONS %%%----------------------------------------------------------------------------- +buoy_request(Method, BuoyUrl, BuoyOpts, RetriesLeft) -> + case buoy:request(Method, BuoyUrl, BuoyOpts) of + {ok, BuoyResp} -> + RespStatus = BuoyResp#buoy_resp.status_code, + RespHeaders = headers(BuoyResp#buoy_resp.headers), + RespBody = body( + proplists:get_value(<<"content-type">>, RespHeaders, undefined), + BuoyResp#buoy_resp.body + ), + Response = #{ + status => RespStatus, + headers => RespHeaders, + body => RespBody + }, + {ok, Response}; + {error, Reason} when + RetriesLeft > 0 andalso (Reason =:= no_server orelse Reason =:= no_socket) + -> + timer:sleep(?BUOY_RETRY_INTERVAL), + buoy_request(Method, BuoyUrl, BuoyOpts, RetriesLeft - 1); + {error, Reason} -> + {error, Reason} + end. + body(Body) -> - njson:encode(Body). + {ok, Encoded} = njson:encode(Body), + Encoded. body(_ContentType, undefined) -> undefined; body(<<"application/json">>, BuoyBody) -> - njson:decode(BuoyBody); + case njson:decode(BuoyBody) of + {ok, Decoded} -> + Decoded; + _Error -> + BuoyBody + end; body(_ContentType, BuoyBody) -> BuoyBody. diff --git a/src/restcheck_schema.erl b/src/restcheck_schema.erl index 093c7f1..b0a9ad0 100644 --- a/src/restcheck_schema.erl +++ b/src/restcheck_schema.erl @@ -45,69 +45,63 @@ complement(true) -> false; complement(false) -> #{}; -complement(#{<<"allOf">> := AllOf}) -> +complement(#{all_of := AllOf}) -> union([complement(Schema) || Schema <- AllOf]); -complement(#{<<"anyOf">> := AnyOf}) -> +complement(#{any_of := AnyOf}) -> intersection([complement(Schema) || Schema <- AnyOf]); -complement(#{<<"oneOf">> := OneOf}) -> +complement(#{one_of := OneOf}) -> Schema1 = symmetric_difference(OneOf), complement(Schema1); -complement(#{<<"not">> := Not}) -> +complement(#{'not' := Not}) -> Not; -complement(#{<<"enum">> := _Values}) -> +complement(#{enum := _Values}) -> %% TODO: mutation undefined; -complement(#{<<"type">> := <<"boolean">>}) -> - union(lists:delete(#{<<"type">> => <<"boolean">>}, ?BASIC_SCHEMAS)); -complement(#{<<"type">> := <<"number">>} = Schema) -> +complement(#{type := boolean}) -> + union(lists:delete(#{type => boolean}, ?BASIC_SCHEMAS)); +complement(#{type := float} = Schema) -> Minimum = - case maps:get(<<"minimum">>, Schema, undefined) of + case maps:get(minimum, Schema, undefined) of undefined -> undefined; Min -> - ExclusiveMin = maps:get(<<"exclusiveMinimum">>, Schema, false), + ExclusiveMin = maps:get(exclusive_minimum, Schema, false), #{ - <<"type">> => <<"number">>, - <<"maximum">> => Min, - <<"exclusiveMaximum">> => not ExclusiveMin + type => float, + maximum => Min, + exclusive_maximum => not ExclusiveMin } end, Maximum = - case maps:get(<<"maximum">>, Schema, undefined) of + case maps:get(maximum, Schema, undefined) of undefined -> undefined; Max -> - ExclusiveMax = maps:get(<<"exclusiveMaximum">>, Schema, false), + ExclusiveMax = maps:get(exclusive_maximum, Schema, false), #{ - <<"type">> => <<"number">>, - <<"minimum">> => Max, - <<"exclusiveMinimum">> => not ExclusiveMax + type => float, + minimum => Max, + exclusive_minimum => not ExclusiveMax } end, Schemas = lists:filter(fun(S) -> S =/= undefined end, [Minimum, Maximum]), union( Schemas ++ - lists:subtract( - ?BASIC_SCHEMAS, - [ - #{<<"type">> => <<"number">>}, - #{<<"type">> => <<"integer">>} - ] - ) + lists:delete(#{type => float}, ?BASIC_SCHEMAS) ); -complement(#{<<"type">> := <<"integer">>} = Schema) -> - Min = maps:get(<<"minimum">>, Schema, undefined), - Max = maps:get(<<"maximum">>, Schema, undefined), +complement(#{type := integer} = Schema) -> + Min = maps:get(minimum, Schema, undefined), + Max = maps:get(maximum, Schema, undefined), Minimum = case Min of undefined -> undefined; Min -> - ExclusiveMin = maps:get(<<"exclusiveMinimum">>, Schema, false), + ExclusiveMin = maps:get(exclusive_minimum, Schema, false), #{ - <<"type">> => <<"number">>, - <<"maximum">> => Min, - <<"exclusiveMaximum">> => not ExclusiveMin + type => integer, + maximum => Min, + exclusive_maximum => not ExclusiveMin } end, Maximum = @@ -115,15 +109,15 @@ complement(#{<<"type">> := <<"integer">>} = Schema) -> undefined -> undefined; Max -> - ExclusiveMax = maps:get(<<"exclusiveMaximum">>, Schema, false), + ExclusiveMax = maps:get(exclusive_maximum, Schema, false), #{ - <<"type">> => <<"number">>, - <<"minimum">> => Max, - <<"exclusiveMinimum">> => not ExclusiveMax + type => integer, + minimum => Max, + exclusive_minimum => not ExclusiveMax } end, Intervals = - case maps:get(<<"multipleOf">>, Schema, undefined) of + case maps:get(multiple_of, Schema, undefined) of undefined -> [Minimum, Maximum]; Mult -> @@ -134,89 +128,83 @@ complement(#{<<"type">> := <<"integer">>} = Schema) -> %% TODO: remove integers in numbers instead of fully removing the numbers domain union( Schemas ++ - lists:subtract( - ?BASIC_SCHEMAS, - [ - #{<<"type">> => <<"number">>}, - #{<<"type">> => <<"integer">>} - ] - ) + lists:delete(#{type => integer}, ?BASIC_SCHEMAS) ); -complement(#{<<"type">> := <<"string">>} = Schema) -> +complement(#{type := string} = Schema) -> MinLength = - case maps:get(<<"maxLength">>, Schema, undefined) of + case maps:get(max_length, Schema, undefined) of undefined -> undefined; Max -> #{ - <<"type">> => <<"string">>, - <<"minLength">> => Max + 1 + type => string, + min_length => Max + 1 } end, MaxLength = - case maps:get(<<"minLength">>, Schema, 0) of + case maps:get(min_length, Schema, 0) of 0 -> undefined; Min -> #{ - <<"type">> => <<"string">>, - <<"maxLength">> => Min - 1 + type => string, + max_length => Min - 1 } end, Format = %% NOTE: mutation %% TODO: replace mutation with regex - case maps:get(<<"format">>, Schema, undefined) of + case maps:get(format, Schema, undefined) of undefined -> undefined; F -> Formats = lists:delete(F, ?FORMATS), - Schema#{<<"format">> => random_pick(Formats)} + Schema#{format => random_pick(Formats)} end, Pattern = - case maps:get(<<"pattern">>, Schema, undefined) of + case maps:get(pattern, Schema, undefined) of undefined -> undefined; P -> - Schema#{<<"pattern">> => <<"^(?!.*", P/binary, ").*">>} + Schema#{pattern => <<"^(?!.*", P/binary, ").*">>} end, Schemas = lists:filter(fun(S) -> S =/= undefined end, [MinLength, MaxLength, Format, Pattern]), union( Schemas ++ - lists:delete(#{<<"type">> => <<"string">>}, ?BASIC_SCHEMAS) + lists:delete(#{type => string}, ?BASIC_SCHEMAS) ); -complement(#{<<"type">> := <<"array">>} = Schema) -> +complement(#{type := array} = Schema) -> Items = - case maps:get(<<"items">>, Schema, undefined) of + case maps:get(items, Schema, undefined) of undefined -> undefined; I -> - Schema#{<<"type">> => <<"array">>, <<"items">> => complement(I)} + Schema#{type => array, items => complement(I)} end, MinItems = - case maps:get(<<"minItems">>, Schema, undefined) of + case maps:get(min_items, Schema, undefined) of undefined -> undefined; Min -> - #{<<"type">> => <<"array">>, <<"maxItems">> => Min - 1} + #{type => array, max_items => Min - 1} end, MaxItems = - case maps:get(<<"maxItems">>, Schema, undefined) of + case maps:get(max_items, Schema, undefined) of undefined -> undefined; Max -> - #{<<"type">> => <<"array">>, <<"minItems">> => Max + 1} + #{type => array, min_items => Max + 1} end, %% TODO: mutation to enum with repeated items within min and max if max_size is at least 2 UniqueItems = undefined, Schemas = lists:filter(fun(S) -> S =/= undefined end, [Items, MinItems, MaxItems, UniqueItems]), union( Schemas ++ - lists:delete(#{<<"type">> => <<"array">>}, ?BASIC_SCHEMAS) + lists:delete(#{type => array}, ?BASIC_SCHEMAS) ); -complement(#{<<"type">> := <<"object">>} = Schema) -> - Required = maps:get(<<"required">>, Schema, []), - Properties = maps:get(<<"properties">>, Schema, #{}), +complement(#{type := object} = Schema) -> + Required = maps:get(required, Schema, []), + Properties = maps:get(properties, Schema, #{}), PropertiesSchemas = lists:map( fun({PropertyName, PropertySchema}) -> @@ -228,8 +216,8 @@ complement(#{<<"type">> := <<"object">>} = Schema) -> [PropertyName | Required] end, Schema#{ - <<"required">> => NewRequired, - <<"properties">> => Properties#{ + required => NewRequired, + properties => Properties#{ PropertyName => complement(PropertySchema) } } @@ -237,47 +225,47 @@ complement(#{<<"type">> := <<"object">>} = Schema) -> maps:to_list(Properties) ), MinProperties = - case maps:get(<<"minProperties">>, Schema, undefined) of + case maps:get(min_properties, Schema, undefined) of undefined -> undefined; Min -> #{ - <<"type">> => <<"object">>, - <<"maxProperties">> => Min - 1 + type => object, + max_properties => Min - 1 } end, MaxProperties = - case maps:get(<<"maxProperties">>, Schema, undefined) of + case maps:get(max_properties, Schema, undefined) of undefined -> undefined; Max -> #{ - <<"type">> => <<"object">>, - <<"minProperties">> => Max + 1 + type => object, + min_properties => Max + 1 } end, AdditionalProperties = %% NOTE: mutation - case maps:get(<<"additionalProperties">>, Schema, true) of + case maps:get(additional_properties, Schema, true) of true -> undefined; false -> - OldRequired = maps:get(<<"required">>, Schema, []), + OldRequired = maps:get(required, Schema, []), PropertyName = new_property_name(maps:keys(Properties)), Schema#{ - <<"properties">> => Properties#{ + properties => Properties#{ PropertyName => #{} }, - <<"required">> => [PropertyName | OldRequired] + required => [PropertyName | OldRequired] }; AdditionalSchema -> - OldRequired = maps:get(<<"required">>, Schema, []), + OldRequired = maps:get(required, Schema, []), PropertyName = new_property_name(maps:keys(Properties)), Schema#{ - <<"properties">> => Properties#{ + properties => Properties#{ PropertyName => complement(AdditionalSchema) }, - <<"required">> => [PropertyName | OldRequired] + required => [PropertyName | OldRequired] } end, Schemas = lists:filter( @@ -291,7 +279,7 @@ complement(#{<<"type">> := <<"object">>} = Schema) -> ), union( Schemas ++ - lists:delete(#{<<"type">> => <<"object">>}, ?BASIC_SCHEMAS) + lists:delete(#{type => object}, ?BASIC_SCHEMAS) ). -spec empty_schema() -> EmptySchema when @@ -325,59 +313,59 @@ intersection(true, Schema2) -> Schema2; intersection(Schema1, true) -> Schema1; -intersection(#{<<"allOf">> := AllOf1}, Schema2) -> +intersection(#{all_of := AllOf1}, Schema2) -> Schema1 = intersection(AllOf1), intersection(Schema1, Schema2); -intersection(Schema1, #{<<"allOf">> := AllOf}) -> +intersection(Schema1, #{all_of := AllOf}) -> Schema2 = intersection(AllOf), intersection(Schema1, Schema2); -intersection(#{<<"anyOf">> := AnyOf}, Schema2) -> +intersection(#{any_of := AnyOf}, Schema2) -> union([intersection(Schema2, AnyOfSchema) || AnyOfSchema <- AnyOf]); -intersection(Schema1, #{<<"anyOf">> := AnyOf}) -> +intersection(Schema1, #{any_of := AnyOf}) -> union([intersection(Schema1, AnyOfSchema) || AnyOfSchema <- AnyOf]); -intersection(#{<<"oneOf">> := OneOf}, Schema2) -> +intersection(#{one_of := OneOf}, Schema2) -> Schema1 = symmetric_difference(OneOf), intersection(Schema1, Schema2); -intersection(Schema1, #{<<"oneOf">> := OneOf}) -> +intersection(Schema1, #{one_of := OneOf}) -> Schema2 = symmetric_difference(OneOf), intersection(Schema1, Schema2); -intersection(#{<<"not">> := Not}, Schema2) -> +intersection(#{'not' := Not}, Schema2) -> Schema1 = complement(Not), intersection(Schema1, Schema2); -intersection(Schema1, #{<<"not">> := Not}) -> +intersection(Schema1, #{'not' := Not}) -> Schema2 = complement(Not), intersection(Schema1, Schema2); -intersection(#{<<"enum">> := Enum1}, #{<<"enum">> := Enum2}) -> +intersection(#{enum := Enum1}, #{enum := Enum2}) -> NewEnum = sets:to_list( sets:intersection( sets:from_list(Enum1), sets:from_list(Enum2) ) ), - #{<<"enum">> => NewEnum}; -intersection(Schema1, #{<<"enum">> := _Enum} = Schema2) -> + #{enum => NewEnum}; +intersection(Schema1, #{enum := _Enum} = Schema2) -> intersection(Schema2, Schema1); -intersection(#{<<"enum">> := Enum}, Schema2) -> +intersection(#{enum := Enum}, Schema2) -> Name = erlang:binary_to_atom( <<"intersection_enum_", (erlang:integer_to_binary(erlang:unique_integer()))/binary>> ), DTO = ndto:generate(Name, Schema2), ndto:load(DTO), - NewEnum = lists:filter(fun Name:is_valid/1, Enum), - #{<<"enum">> => NewEnum}; -intersection(#{<<"type">> := <<"boolean">>} = Schema1, #{<<"type">> := <<"boolean">>}) -> + NewEnum = lists:filter(fun(Value) -> Name:is_valid(Value) =:= true end, Enum), + #{enum => NewEnum}; +intersection(#{type := boolean} = Schema1, #{type := boolean}) -> Schema1; -intersection(#{<<"type">> := <<"integer">>} = Schema1, #{<<"type">> := <<"number">>} = Schema2) -> - intersection(Schema1, Schema2#{<<"type">> => <<"integer">>}); -intersection(#{<<"type">> := <<"number">>} = Schema1, #{<<"type">> := <<"integer">>} = Schema2) -> - intersection(Schema1#{<<"type">> => <<"integer">>}, Schema2); -intersection(#{<<"type">> := Type} = Schema1, #{<<"type">> := Type} = Schema2) when - Type =:= <<"integer">> orelse Type =:= <<"number">> +intersection(#{type := integer} = Schema1, #{type := float} = Schema2) -> + intersection(Schema1, Schema2#{type => integer}); +intersection(#{type := float} = Schema1, #{type := integer} = Schema2) -> + intersection(Schema1#{type => integer}, Schema2); +intersection(#{type := Type} = Schema1, #{type := Type} = Schema2) when + Type =:= integer orelse Type =:= float -> - Minimum1 = maps:get(<<"minimum">>, Schema1, undefined), - Minimum2 = maps:get(<<"minimum">>, Schema2, undefined), - ExclusiveMinimum1 = maps:get(<<"exclusiveMinimum">>, Schema1, undefined), - ExclusiveMinimum2 = maps:get(<<"exclusiveMinimum">>, Schema2, undefined), + Minimum1 = maps:get(minimum, Schema1, undefined), + Minimum2 = maps:get(minimum, Schema2, undefined), + ExclusiveMinimum1 = maps:get(exclusive_minimum, Schema1, undefined), + ExclusiveMinimum2 = maps:get(exclusive_minimum, Schema2, undefined), {Minimum, ExclusiveMinimum} = case {Minimum1, Minimum2} of {Minimum1, undefined} -> @@ -401,10 +389,10 @@ intersection(#{<<"type">> := Type} = Schema1, #{<<"type">> := Type} = Schema2) w {Minimum2, ExclusiveMinimum2} end, - Maximum1 = maps:get(<<"maximum">>, Schema1, undefined), - Maximum2 = maps:get(<<"maximum">>, Schema2, undefined), - ExclusiveMaximum1 = maps:get(<<"exclusiveMaximum">>, Schema1, undefined), - ExclusiveMaximum2 = maps:get(<<"exclusiveMaximum">>, Schema2, undefined), + Maximum1 = maps:get(maximum, Schema1, undefined), + Maximum2 = maps:get(maximum, Schema2, undefined), + ExclusiveMaximum1 = maps:get(exclusive_maximum, Schema1, undefined), + ExclusiveMaximum2 = maps:get(exclusive_maximum, Schema2, undefined), {Maximum, ExclusiveMaximum} = case {Maximum1, Maximum2} of {Maximum1, undefined} -> @@ -427,8 +415,8 @@ intersection(#{<<"type">> := Type} = Schema1, #{<<"type">> := Type} = Schema2) w {Maximum1, Maximum2} -> {Maximum2, ExclusiveMaximum2} end, - MultipleOf1 = maps:get(<<"multipleOf">>, Schema1, undefined), - MultipleOf2 = maps:get(<<"multipleOf">>, Schema2, undefined), + MultipleOf1 = maps:get(multiple_of, Schema1, undefined), + MultipleOf2 = maps:get(multiple_of, Schema2, undefined), MultipleOf = case {MultipleOf1, MultipleOf2} of {MultipleOf1, undefined} -> @@ -439,16 +427,16 @@ intersection(#{<<"type">> := Type} = Schema1, #{<<"type">> := Type} = Schema2) w lcm(MultipleOf1, MultipleOf2) end, clean(#{ - <<"type">> => Type, - <<"minimum">> => Minimum, - <<"exclusiveMinimum">> => ExclusiveMinimum, - <<"maximum">> => Maximum, - <<"exclusiveMaximum">> => ExclusiveMaximum, - <<"multipleOf">> => MultipleOf + type => Type, + minimum => Minimum, + exclusive_minimum => ExclusiveMinimum, + maximum => Maximum, + exclusive_maximum => ExclusiveMaximum, + multiple_of => MultipleOf }); -intersection(#{<<"type">> := <<"string">>} = Schema1, #{<<"type">> := <<"string">>} = Schema2) -> - MinLength1 = maps:get(<<"minLength">>, Schema1, undefined), - MinLength2 = maps:get(<<"minLength">>, Schema2, undefined), +intersection(#{type := string} = Schema1, #{type := string} = Schema2) -> + MinLength1 = maps:get(min_length, Schema1, undefined), + MinLength2 = maps:get(min_length, Schema2, undefined), MinLength = case {MinLength1, MinLength2} of @@ -462,8 +450,8 @@ intersection(#{<<"type">> := <<"string">>} = Schema1, #{<<"type">> := <<"string" MinLength2 end, - MaxLength1 = maps:get(<<"maxLength">>, Schema1, undefined), - MaxLength2 = maps:get(<<"maxLength">>, Schema2, undefined), + MaxLength1 = maps:get(max_length, Schema1, undefined), + MaxLength2 = maps:get(max_length, Schema2, undefined), MaxLength = case {MaxLength1, MaxLength2} of @@ -477,8 +465,8 @@ intersection(#{<<"type">> := <<"string">>} = Schema1, #{<<"type">> := <<"string" MaxLength2 end, - Pattern1 = maps:get(<<"pattern">>, Schema1, undefined), - Pattern2 = maps:get(<<"pattern">>, Schema2, undefined), + Pattern1 = maps:get(pattern, Schema1, undefined), + Pattern2 = maps:get(pattern, Schema2, undefined), Pattern = case {Pattern1, Pattern2} of @@ -490,8 +478,8 @@ intersection(#{<<"type">> := <<"string">>} = Schema1, #{<<"type">> := <<"string" <<"^(?=.*", Pattern1/binary, ")(?=.*", Pattern2/binary, ").*">> end, - Format1 = maps:get(<<"format">>, Schema1, undefined), - Format2 = maps:get(<<"format">>, Schema2, undefined), + Format1 = maps:get(format, Schema1, undefined), + Format2 = maps:get(format, Schema2, undefined), Format = case {Format1, Format2} of @@ -502,15 +490,15 @@ intersection(#{<<"type">> := <<"string">>} = Schema1, #{<<"type">> := <<"string" end, clean(#{ - <<"type">> => <<"string">>, - <<"minLength">> => MinLength, - <<"maxLength">> => MaxLength, - <<"pattern">> => Pattern, - <<"format">> => Format + type => string, + min_length => MinLength, + max_length => MaxLength, + pattern => Pattern, + format => Format }); -intersection(#{<<"type">> := <<"array">>} = Schema1, #{<<"type">> := <<"array">>} = Schema2) -> - Items1 = maps:get(<<"items">>, Schema1, undefined), - Items2 = maps:get(<<"items">>, Schema2, undefined), +intersection(#{type := array} = Schema1, #{type := array} = Schema2) -> + Items1 = maps:get(items, Schema1, undefined), + Items2 = maps:get(items, Schema2, undefined), Items = case {Items1, Items2} of @@ -522,8 +510,8 @@ intersection(#{<<"type">> := <<"array">>} = Schema1, #{<<"type">> := <<"array">> intersection(Items1, Items2) end, - MinItems1 = maps:get(<<"minItems">>, Schema1, undefined), - MinItems2 = maps:get(<<"minItems">>, Schema2, undefined), + MinItems1 = maps:get(min_items, Schema1, undefined), + MinItems2 = maps:get(min_items, Schema2, undefined), MinItems = case {MinItems1, MinItems2} of @@ -537,8 +525,8 @@ intersection(#{<<"type">> := <<"array">>} = Schema1, #{<<"type">> := <<"array">> MinItems2 end, - MaxItems1 = maps:get(<<"maxItems">>, Schema1, undefined), - MaxItems2 = maps:get(<<"maxItems">>, Schema2, undefined), + MaxItems1 = maps:get(max_items, Schema1, undefined), + MaxItems2 = maps:get(max_items, Schema2, undefined), MaxItems = case {MaxItems1, MaxItems2} of @@ -552,8 +540,8 @@ intersection(#{<<"type">> := <<"array">>} = Schema1, #{<<"type">> := <<"array">> MaxItems2 end, - UniqueItems1 = maps:get(<<"uniqueItems">>, Schema1, undefined), - UniqueItems2 = maps:get(<<"uniqueItems">>, Schema2, undefined), + UniqueItems1 = maps:get(unique_items, Schema1, undefined), + UniqueItems2 = maps:get(unique_items, Schema2, undefined), UniqueItems = case {UniqueItems1, UniqueItems2} of @@ -565,15 +553,15 @@ intersection(#{<<"type">> := <<"array">>} = Schema1, #{<<"type">> := <<"array">> UniqueItems1 orelse UniqueItems2 end, clean(#{ - <<"type">> => <<"array">>, - <<"items">> => Items, - <<"minItems">> => MinItems, - <<"maxItems">> => MaxItems, - <<"uniqueItems">> => UniqueItems + type => array, + items => Items, + min_items => MinItems, + max_items => MaxItems, + unique_items => UniqueItems }); -intersection(#{<<"type">> := <<"object">>} = Schema1, #{<<"type">> := <<"object">>} = Schema2) -> - Properties1 = maps:get(<<"properties">>, Schema1, undefined), - Properties2 = maps:get(<<"properties">>, Schema2, undefined), +intersection(#{type := object} = Schema1, #{type := object} = Schema2) -> + Properties1 = maps:get(properties, Schema1, undefined), + Properties2 = maps:get(properties, Schema2, undefined), Properties = case {Properties1, Properties2} of {Properties1, undefined} -> @@ -601,8 +589,8 @@ intersection(#{<<"type">> := <<"object">>} = Schema1, #{<<"type">> := <<"object" maps:from_list(PropertyList) end, - Required1 = maps:get(<<"required">>, Schema1, undefined), - Required2 = maps:get(<<"required">>, Schema2, undefined), + Required1 = maps:get(required, Schema1, undefined), + Required2 = maps:get(required, Schema2, undefined), Required = case {Required1, Required2} of {Required1, undefined} -> @@ -613,8 +601,8 @@ intersection(#{<<"type">> := <<"object">>} = Schema1, #{<<"type">> := <<"object" lists:uniq(lists:append(Required1, Required2)) end, - MinProperties1 = maps:get(<<"minProperties">>, Schema1, undefined), - MinProperties2 = maps:get(<<"minProperties">>, Schema2, undefined), + MinProperties1 = maps:get(min_properties, Schema1, undefined), + MinProperties2 = maps:get(min_properties, Schema2, undefined), MinProperties = case {MinProperties1, MinProperties2} of {MinProperties1, undefined} -> @@ -627,8 +615,8 @@ intersection(#{<<"type">> := <<"object">>} = Schema1, #{<<"type">> := <<"object" MinProperties2 end, - MaxProperties1 = maps:get(<<"maxProperties">>, Schema1, undefined), - MaxProperties2 = maps:get(<<"maxProperties">>, Schema2, undefined), + MaxProperties1 = maps:get(max_properties, Schema1, undefined), + MaxProperties2 = maps:get(max_properties, Schema2, undefined), MaxProperties = case {MaxProperties1, MaxProperties2} of {MaxProperties1, undefined} -> @@ -641,8 +629,8 @@ intersection(#{<<"type">> := <<"object">>} = Schema1, #{<<"type">> := <<"object" MaxProperties2 end, - AdditionalProperties1 = maps:get(<<"additionalProperties">>, Schema1, undefined), - AdditionalProperties2 = maps:get(<<"additionalProperties">>, Schema2, undefined), + AdditionalProperties1 = maps:get(additional_properties, Schema1, undefined), + AdditionalProperties2 = maps:get(additional_properties, Schema2, undefined), AdditionalProperties = case {AdditionalProperties1, AdditionalProperties2} of {AdditionalProperties1, undefined} -> @@ -654,12 +642,12 @@ intersection(#{<<"type">> := <<"object">>} = Schema1, #{<<"type">> := <<"object" end, clean(#{ - <<"type">> => <<"object">>, - <<"properties">> => Properties, - <<"required">> => Required, - <<"minProperties">> => MinProperties, - <<"maxProperties">> => MaxProperties, - <<"additionalProperties">> => AdditionalProperties + type => object, + properties => Properties, + required => Required, + min_properties => MinProperties, + max_properties => MaxProperties, + additional_properties => AdditionalProperties }); intersection(_Schema1, _Schema2) -> false. @@ -716,58 +704,58 @@ union(Schema1, false) -> Schema1; union(false, Schema2) -> Schema2; -union(#{<<"allOf">> := AllOf1}, Schema2) -> +union(#{all_of := AllOf1}, Schema2) -> Schema1 = intersection(AllOf1), union(Schema1, Schema2); -union(Schema1, #{<<"allOf">> := AllOf}) -> +union(Schema1, #{all_of := AllOf}) -> Schema2 = intersection(AllOf), union(Schema1, Schema2); -union(#{<<"anyOf">> := AnyOf1}, #{<<"anyOf">> := AnyOf2}) -> +union(#{any_of := AnyOf1}, #{any_of := AnyOf2}) -> case lists:sort(lists:uniq(AnyOf1 ++ AnyOf2)) of [] -> false; [Schema] -> Schema; AnyOf -> - #{<<"anyOf">> => AnyOf} + #{any_of => AnyOf} end; -union(#{<<"anyOf">> := AnyOf1}, Schema2) -> +union(#{any_of := AnyOf1}, Schema2) -> case lists:sort(lists:uniq([Schema2 | AnyOf1])) of [] -> false; [Schema] -> Schema; AnyOf -> - #{<<"anyOf">> => AnyOf} + #{any_of => AnyOf} end; -union(Schema1, #{<<"anyOf">> := AnyOf2}) -> +union(Schema1, #{any_of := AnyOf2}) -> case lists:sort(lists:uniq([Schema1 | AnyOf2])) of [] -> false; [Schema] -> Schema; AnyOf -> - #{<<"anyOf">> => AnyOf} + #{any_of => AnyOf} end; -union(#{<<"oneOf">> := OneOf}, Schema2) -> +union(#{one_of := OneOf}, Schema2) -> Schema1 = symmetric_difference(OneOf), union(Schema1, Schema2); -union(Schema1, #{<<"oneOf">> := OneOf}) -> +union(Schema1, #{one_of := OneOf}) -> Schema2 = symmetric_difference(OneOf), union(Schema1, Schema2); -union(#{<<"not">> := Not}, Schema2) -> +union(#{'not' := Not}, Schema2) -> Schema1 = complement(Not), union(Schema1, Schema2); -union(Schema1, #{<<"not">> := Not}) -> +union(Schema1, #{'not' := Not}) -> Schema2 = complement(Not), union(Schema1, Schema2); -union(#{<<"enum">> := Enum1}, #{<<"enum">> := Enum2}) -> +union(#{enum := Enum1}, #{enum := Enum2}) -> NewEnum = lists:sort(lists:uniq(Enum1 ++ Enum2)), - #{<<"enum">> => NewEnum}; -union(#{<<"type">> := <<"boolean">>}, #{<<"type">> := <<"boolean">>}) -> - #{<<"type">> => <<"boolean">>}; + #{enum => NewEnum}; +union(#{type := boolean}, #{type := boolean}) -> + #{type => boolean}; union(Schema1, Schema2) -> - #{<<"anyOf">> => lists:sort([Schema1, Schema2])}. + #{any_of => lists:sort([Schema1, Schema2])}. -spec universal_schema() -> UniversalSchema when UniversalSchema :: ndto:universal_schema(). @@ -801,8 +789,8 @@ multiples(MultipleOf, Max, Current, Acc) -> %%% INTERNAL FUNCTIONS %%%----------------------------------------------------------------------------- -spec clean(Map) -> Clear when - Map :: #{binary() => undefined | term()}, - Clear :: #{binary() => term()}. + Map :: #{atom() => undefined | term()}, + Clear :: #{atom() => term()}. clean(Schema) -> maps:filter( fun(_K, V) -> V =/= undefined end, @@ -813,29 +801,29 @@ clean(Schema) -> Integers :: [integer()], Schemas :: [ndto:schema()]. exclude_integers([]) -> - [#{<<"type">> => <<"integer">>}]; + [#{type => integer}]; exclude_integers([Integer | Integers]) -> Interval = #{ - <<"type">> => <<"integer">>, - <<"maximum">> => Integer, - <<"exclusiveMaximum">> => true + type => integer, + maximum => Integer, + exclusive_maximum => true }, exclude_integers(Integers, [Interval]). -exclude_integers([], [#{<<"maximum">> := Previous} | _Tl] = Acc) -> +exclude_integers([], [#{maximum := Previous} | _Tl] = Acc) -> Interval = #{ - <<"type">> => <<"integer">>, - <<"minimum">> => Previous, - <<"exclusiveMinimum">> => true + type => integer, + minimum => Previous, + exclusive_minimum => true }, [Interval | Acc]; -exclude_integers([Next | Rest], [#{<<"maximum">> := Previous} | _Tl] = Acc) -> +exclude_integers([Next | Rest], [#{maximum := Previous} | _Tl] = Acc) -> Interval = #{ - <<"type">> => <<"integer">>, - <<"minimum">> => Previous, - <<"exclusiveMinimum">> => true, - <<"maximum">> => Next, - <<"exclusiveMaximum">> => true + type => integer, + minimum => Previous, + exclusive_minimum => true, + maximum => Next, + exclusive_maximum => true }, exclude_integers(Rest, [Interval | Acc]). @@ -875,5 +863,5 @@ new_property_name(ExcludedNames) -> -spec random_pick(List) -> Element when List :: [term(), ...], Element :: term(). -random_pick(List) -> - lists:nth(rand:uniform(erlang:length(List)), List). +random_pick([_ | Tail] = List) -> + lists:nth(rand:uniform(1 + erlang:length(Tail)), List). diff --git a/src/restcheck_schema.hrl b/src/restcheck_schema.hrl index afc537d..5fc2774 100644 --- a/src/restcheck_schema.hrl +++ b/src/restcheck_schema.hrl @@ -16,16 +16,16 @@ %%% MACROS -define(BASIC_SCHEMAS, [ - #{<<"type">> => <<"boolean">>}, - #{<<"type">> => <<"integer">>}, - #{<<"type">> => <<"number">>}, - #{<<"type">> => <<"string">>}, - #{<<"type">> => <<"array">>}, - #{<<"type">> => <<"object">>} + #{type => boolean}, + #{type => integer}, + #{type => float}, + #{type => string}, + #{type => array}, + #{type => object} ]). -define(FORMATS, [ - <<"base64">>, - <<"iso8601-datetime">> + base64, + iso8601 ]). % https://www.erlang.org/doc/efficiency_guide/advanced.html -define(MAX_INT, 134217728). diff --git a/src/restcheck_suite.erl b/src/restcheck_suite.erl index b0cb03f..606c3b9 100644 --- a/src/restcheck_suite.erl +++ b/src/restcheck_suite.erl @@ -59,8 +59,13 @@ generate(API) -> Method = maps:get(method, Operation), OperationParameters = maps:get(parameters, Operation), Parameters = EndpointParameters ++ OperationParameters, - RequestBody = maps:get(request_body, Operation), - Responses = maps:get(responses, Operation), + RequestBody = maps:get(ref, maps:get(body, maps:get(request, Operation))), + Responses = maps:map( + fun(_Status, Response) -> + maps:get(ref, maps:get(body, Response)) + end, + maps:get(responses, Operation) + ), {RawValues, RawGenerators} = lists:foldl( fun(#{ref := Ref, name := Name}, {ValuesAcc, GeneratorsAcc}) -> @@ -69,7 +74,7 @@ generate(API) -> erf_util:to_pascal_case(Name) ) ), - Schema = schema_ast(maps:get(Ref, Schemas)), + Schema = schema_ast(maps:get(Ref, Schemas), Schemas), Generator = erl_syntax:application( erl_syntax:atom(restcheck_pbt), @@ -96,7 +101,7 @@ generate(API) -> erf_util:to_pascal_case(RequestBody) ) ), - RequestBodySchema = schema_ast(maps:get(RequestBody, Schemas)), + RequestBodySchema = schema_ast(maps:get(RequestBody, Schemas), Schemas), RequestBodyGenerator = erl_syntax:application( erl_syntax:atom(restcheck_pbt), erl_syntax:atom(noshrink), @@ -224,8 +229,7 @@ load(Suite, Options) -> -spec load_binary(ModuleName, Bin) -> Result when ModuleName :: atom(), Bin :: binary(), - Result :: ok | {error, What}, - What :: term(). + Result :: ok | {error, term()}. load_binary(ModuleName, Bin) -> case code:load_binary( @@ -340,7 +344,7 @@ prop_ast(RawPath, Method, Parameters, RequestBody, Responses) -> ), [ erl_syntax:clause( - [erl_syntax:atom(false)], + [erl_syntax:tuple([erl_syntax:atom(false), erl_syntax:underscore()])], none, [ erl_syntax:tuple([ @@ -585,7 +589,12 @@ prop_ast(RawPath, Method, Parameters, RequestBody, Responses) -> ), [ erl_syntax:clause( - [erl_syntax:atom(false)], + [ + erl_syntax:tuple([ + erl_syntax:atom(false), + erl_syntax:underscore() + ]) + ], none, [ erl_syntax:tuple([ @@ -642,8 +651,21 @@ prop_ast(RawPath, Method, Parameters, RequestBody, Responses) -> ) ]. --spec schema_ast(Schema) -> SchemaAST when +-spec schema_ast(Schema, Schemas) -> SchemaAST when Schema :: erf_parser:schema(), + Schemas :: #{binary() => erf_parser:schema()}, SchemaAST :: erl_syntax:syntaxTree(). -schema_ast(Schema) -> - erl_syntax:abstract(Schema). +schema_ast(Schema, Schemas) -> + erl_syntax:abstract(inline_refs(Schema, Schemas, [])). + +inline_refs(#{ref := Ref}, Schemas, Seen) -> + case lists:member(Ref, Seen) of + true -> #{}; + false -> inline_refs(maps:get(Ref, Schemas), Schemas, [Ref | Seen]) + end; +inline_refs(Schema, Schemas, Seen) when is_map(Schema) -> + maps:map(fun(_Key, Value) -> inline_refs(Value, Schemas, Seen) end, Schema); +inline_refs(Schema, Schemas, Seen) when is_list(Schema) -> + [inline_refs(Element, Schemas, Seen) || Element <- Schema]; +inline_refs(Schema, _Schemas, _Seen) -> + Schema. diff --git a/src/restcheck_triq.erl b/src/restcheck_triq.erl index 9986de5..3913916 100644 --- a/src/restcheck_triq.erl +++ b/src/restcheck_triq.erl @@ -34,6 +34,10 @@ report/3 ]). +%%% MACROS +-define(DEFAULT_MAX_STRING_LENGTH, 255). +-define(DEFAULT_MAX_ARRAY_ITEMS, 3). + %%% TYPES -type recursion_max_depth() :: non_neg_integer(). @@ -52,27 +56,27 @@ dto(Schema) -> Schema :: restcheck_pbt:schema(), Generator :: restcheck_pbt:generator(). %% @doc Returns a triq generator of DTOs from a given schema and maximum recursion depth. -dto(#{<<"enum">> := _Enum} = Schema, _MaxDepth) -> +dto(#{enum := _Enum} = Schema, _MaxDepth) -> enum(Schema); -dto(#{<<"type">> := <<"boolean">>} = Schema, _MaxDepth) -> +dto(#{type := boolean} = Schema, _MaxDepth) -> boolean(Schema); -dto(#{<<"type">> := <<"integer">>} = Schema, _MaxDepth) -> +dto(#{type := integer} = Schema, _MaxDepth) -> integer(Schema); -dto(#{<<"type">> := <<"number">>} = Schema, _MaxDepth) -> +dto(#{type := float} = Schema, _MaxDepth) -> number(Schema); -dto(#{<<"type">> := <<"string">>} = Schema, _MaxDepth) -> +dto(#{type := string} = Schema, _MaxDepth) -> string(Schema); -dto(#{<<"type">> := <<"array">>} = Schema, MaxDepth) -> +dto(#{type := array} = Schema, MaxDepth) -> array(Schema, MaxDepth); -dto(#{<<"type">> := <<"object">>} = Schema, MaxDepth) -> +dto(#{type := object} = Schema, MaxDepth) -> object(Schema, MaxDepth); -dto(#{<<"allOf">> := _Subschemas} = Schema, MaxDepth) -> +dto(#{all_of := _Subschemas} = Schema, MaxDepth) -> all_of(Schema, MaxDepth); -dto(#{<<"anyOf">> := _Subschemas} = Schema, MaxDepth) -> +dto(#{any_of := _Subschemas} = Schema, MaxDepth) -> any_of(Schema, MaxDepth); -dto(#{<<"oneOf">> := _Subschemas} = Schema, MaxDepth) -> +dto(#{one_of := _Subschemas} = Schema, MaxDepth) -> one_of(Schema, MaxDepth); -dto(#{<<"not">> := _Subschemas} = Schema, MaxDepth) -> +dto(#{'not' := _Subschemas} = Schema, MaxDepth) -> 'not'(Schema, MaxDepth); dto(_Schema, MaxDepth) -> any(MaxDepth). @@ -131,7 +135,7 @@ report(Subject, Data, false) -> report(Subject, Data). MaxDepth :: recursion_max_depth(), Schema :: ndto:intersection_schema(), Dom :: restcheck_pbt:generator(). -all_of(#{<<"allOf">> := Subschemas}, MaxDepth) -> +all_of(#{all_of := Subschemas}, MaxDepth) -> Schema = restcheck_schema:intersection(Subschemas), dto(Schema, MaxDepth). @@ -140,20 +144,20 @@ all_of(#{<<"allOf">> := Subschemas}, MaxDepth) -> Dom :: restcheck_pbt:generator(). any(0) -> Schema = #{ - <<"anyOf">> => lists:subtract(?BASIC_SCHEMAS, [ - #{<<"type">> => <<"array">>}, #{<<"type">> => <<"object">>} + any_of => lists:subtract(?BASIC_SCHEMAS, [ + #{type => array}, #{type => object} ]) }, dto(Schema, 0); any(MaxDepth) -> - Schema = #{<<"anyOf">> => ?BASIC_SCHEMAS}, + Schema = #{any_of => ?BASIC_SCHEMAS}, dto(Schema, MaxDepth). -spec any_of(Schema, MaxDepth) -> Dom when Schema :: ndto:union_schema(), MaxDepth :: recursion_max_depth(), Dom :: restcheck_pbt:generator(). -any_of(#{<<"anyOf">> := Subschemas} = _Schema, MaxDepth) -> +any_of(#{any_of := Subschemas} = _Schema, MaxDepth) -> triq_dom:oneof( [ dto(Subschema, MaxDepth) @@ -166,14 +170,14 @@ any_of(#{<<"anyOf">> := Subschemas} = _Schema, MaxDepth) -> MaxDepth :: recursion_max_depth(), Dom :: restcheck_pbt:generator(). array(Schema, MaxDepth) -> - Items = maps:get(<<"items">>, Schema, #{}), - MinItems = maps:get(<<"minItems">>, Schema, 0), - MaxItems = maps:get(<<"maxItems">>, Schema, 3), - UniqueItems = maps:get(<<"uniqueItems">>, Schema, false), + Items = maps:get(items, Schema, #{}), + MinItems = maps:get(min_items, Schema, 0), + MaxItems = erlang:max(MinItems, maps:get(max_items, Schema, default_max_array_items())), + UniqueItems = maps:get(unique_items, Schema, false), triq_dom:bind( triq_dom:int(MinItems, MaxItems), fun(Length) -> - DTO = dto(Items, MaxDepth - 1), + DTO = dto(Items, dec_depth(MaxDepth)), Array = triq_dom:vector(Length, DTO), case UniqueItems of false -> @@ -201,7 +205,7 @@ boolean(_Schema) -> -spec enum(Schema) -> Dom when Schema :: ndto:enum_schema(), Dom :: restcheck_pbt:generator(). -enum(#{<<"enum">> := Enum}) -> +enum(#{enum := [_ | _] = Enum}) -> triq_dom:elements(Enum). -spec integer(Schema) -> Dom when @@ -209,13 +213,13 @@ enum(#{<<"enum">> := Enum}) -> Dom :: restcheck_pbt:generator(). integer(Schema) -> RawMin = - case maps:get(<<"minimum">>, Schema, ?MIN_INT) of + case maps:get(minimum, Schema, ?MIN_INT) of MinFloat when is_float(MinFloat) -> erlang:trunc(MinFloat); Minimum -> Minimum end, - ExclusiveMin = maps:get(<<"exclusiveMinimum">>, Schema, false), + ExclusiveMin = maps:get(exclusive_minimum, Schema, false), Min = case ExclusiveMin of true -> @@ -224,13 +228,13 @@ integer(Schema) -> RawMin end, RawMax = - case maps:get(<<"maximum">>, Schema, ?MAX_INT) of + case maps:get(maximum, Schema, ?MAX_INT) of MaxFloat when is_float(MaxFloat) -> erlang:trunc(MaxFloat); Maximum -> Maximum end, - ExclusiveMax = maps:get(<<"exclusiveMaximum">>, Schema, false), + ExclusiveMax = maps:get(exclusive_maximum, Schema, false), Max = case ExclusiveMax of true -> @@ -238,7 +242,7 @@ integer(Schema) -> false -> RawMax end, - MultipleOf = maps:get(<<"multipleOf">>, Schema, undefined), + MultipleOf = maps:get(multiple_of, Schema, undefined), case MultipleOf of undefined -> triq_dom:int(Min, Max); @@ -255,18 +259,18 @@ integer(Schema) -> Schema :: ndto:complement_schema(), MaxDepth :: recursion_max_depth(), Dom :: restcheck_pbt:generator(). -'not'(#{<<"not">> := Subschema}, MaxDepth) -> +'not'(#{'not' := Subschema}, MaxDepth) -> Schema = restcheck_schema:complement(Subschema), dto(Schema, MaxDepth). -spec number(Schema) -> Dom when - Schema :: ndto:number_schema(), + Schema :: ndto:float_schema(), Dom :: restcheck_pbt:generator(). number(Schema) -> - Min = maps:get(<<"minimum">>, Schema, ?MIN_INT), - ExclusiveMin = maps:get(<<"exclusiveMinimum">>, Schema, false), - Max = maps:get(<<"maximum">>, Schema, ?MAX_INT), - ExclusiveMax = maps:get(<<"exclusiveMaximum">>, Schema, false), + Min = maps:get(minimum, Schema, ?MIN_INT), + ExclusiveMin = maps:get(exclusive_minimum, Schema, false), + Max = maps:get(maximum, Schema, ?MAX_INT), + ExclusiveMax = maps:get(exclusive_maximum, Schema, false), Integer = triq_dom:int(erlang:trunc(Min), erlang:trunc(Max)), Float = triq_dom:bind( triq_dom:int(?MAX_INT), @@ -294,10 +298,10 @@ number(Schema) -> MaxDepth :: recursion_max_depth(), Dom :: restcheck_pbt:generator(). object(Schema, MaxDepth) -> - Properties = maps:get(<<"properties">>, Schema, #{}), - RequiredKeys = maps:get(<<"required">>, Schema, []), - RawMinProperties = maps:get(<<"minProperties">>, Schema, 0), - AdditionalProperties = maps:get(<<"additionalProperties">>, Schema, true), + Properties = maps:get(properties, Schema, #{}), + RequiredKeys = maps:get(required, Schema, []), + RawMinProperties = maps:get(min_properties, Schema, 0), + AdditionalProperties = maps:get(additional_properties, Schema, true), MinProperties = case erlang:length(RequiredKeys) of @@ -306,7 +310,7 @@ object(Schema, MaxDepth) -> GreaterThanOrEqualTo -> GreaterThanOrEqualTo end, - MaxProperties = maps:get(<<"maxProperties">>, Schema, MinProperties + 1), + MaxProperties = maps:get(max_properties, Schema, MinProperties + 1), Required = [{PropertyName, maps:get(PropertyName, Properties)} || PropertyName <- RequiredKeys], NotRequired = maps:to_list(maps:without(RequiredKeys, Properties)), @@ -318,7 +322,7 @@ object(Schema, MaxDepth) -> Required ++ NotRequired, AdditionalProperties, MissingSize, - MaxDepth - 1, + dec_depth(MaxDepth), triq_dom:return(#{}) ) end @@ -340,7 +344,11 @@ object([], true, Missing, MaxDepth, Acc) -> object([], ExtraSchema, Missing, MaxDepth, Acc) -> NewAcc = triq_dom:bind( - {triq_dom:non_empty(triq_dom:unicode_binary()), dto(ExtraSchema, MaxDepth - 1), Acc}, + { + triq_dom:non_empty(triq_dom:unicode_binary()), + dto(ExtraSchema, dec_depth(MaxDepth)), + Acc + }, fun({PropertyName, PropertyValue, AccValue}) -> maps:put(PropertyName, PropertyValue, AccValue) end @@ -349,7 +357,7 @@ object([], ExtraSchema, Missing, MaxDepth, Acc) -> object([{PropertyName, PropertySchema} | Properties], ExtraSchema, Missing, MaxDepth, Acc) -> NewAcc = triq_dom:bind( - {dto(PropertySchema, MaxDepth - 1), Acc}, + {dto(PropertySchema, dec_depth(MaxDepth)), Acc}, fun({PropertyValue, AccValue}) -> maps:put(PropertyName, PropertyValue, AccValue) end @@ -360,20 +368,26 @@ object([{PropertyName, PropertySchema} | Properties], ExtraSchema, Missing, MaxD Schema :: ndto:symmetric_difference_schema(), MaxDepth :: recursion_max_depth(), Dom :: restcheck_pbt:generator(). -one_of(#{<<"oneOf">> := Subschemas}, MaxDepth) -> +one_of(#{one_of := Subschemas}, MaxDepth) -> Schema = restcheck_schema:symmetric_difference(Subschemas), dto(Schema, MaxDepth). -spec string(Schema) -> Dom when Schema :: ndto:string_schema(), Dom :: restcheck_pbt:generator(). -string(#{<<"pattern">> := _Pattern}) -> - %% TODO: implement pattern - erlang:throw({restcheck_triq, pattern, not_implemented}); +string(#{pattern := Pattern}) -> + Regex = pattern_strip_anchors(unicode:characters_to_list(Pattern)), + {AST, _Rest} = pattern_parse_alt(Regex), + triq_dom:bind( + pattern_gen(AST), + fun(Codepoints) -> + unicode:characters_to_binary(Codepoints, utf8, utf8) + end + ); string(Schema) -> - MinLength = maps:get(<<"minLength">>, Schema, 1), - MaxLength = maps:get(<<"maxLength">>, Schema, 255), - Format = maps:get(<<"format">>, Schema, undefined), + MinLength = maps:get(min_length, Schema, 1), + MaxLength = erlang:max(MinLength, maps:get(max_length, Schema, default_max_string_length())), + Format = maps:get(format, Schema, undefined), triq_dom:bind( triq_dom:int(MinLength, MaxLength), fun(Length) -> @@ -387,18 +401,18 @@ string(Schema) -> ). -spec string_format(Format, Length) -> FormatGenerator when - Format :: undefined | binary(), + Format :: undefined | ndto:format(), Length :: non_neg_integer(), FormatGenerator :: restcheck_pbt:generator(). string_format(undefined, Length) -> triq_dom:unicode_binary(Length); -string_format(<<"base64">>, Length) -> +string_format(base64, Length) -> 0 = (Length rem 4), triq_dom:vector( Length, triq_dom:elements(base64_chars()) ); -string_format(<<"iso8601-datetime">>, _Length) -> +string_format(iso8601, _Length) -> triq_dom:bind( { triq_dom:int(9999), @@ -444,6 +458,181 @@ string_format(<<"iso8601-datetime">>, _Length) -> %%%----------------------------------------------------------------------------- %%% INTERNAL FUNCTIONS %%%----------------------------------------------------------------------------- +-spec dec_depth(MaxDepth) -> Decremented when + MaxDepth :: recursion_max_depth(), + Decremented :: recursion_max_depth(). +dec_depth(MaxDepth) when MaxDepth > 0 -> + MaxDepth - 1; +dec_depth(_MaxDepth) -> + 0. + +-spec default_max_string_length() -> MaxLength when + MaxLength :: pos_integer(). +default_max_string_length() -> + application:get_env(restcheck, max_string_length, ?DEFAULT_MAX_STRING_LENGTH). + +-spec default_max_array_items() -> MaxItems when + MaxItems :: non_neg_integer(). +default_max_array_items() -> + application:get_env(restcheck, max_array_items, ?DEFAULT_MAX_ARRAY_ITEMS). + +pattern_strip_anchors(Chars0) -> + Chars1 = + case Chars0 of + [$^ | Rest] -> Rest; + _ -> Chars0 + end, + case lists:reverse(Chars1) of + [$$ | RevRest] -> lists:reverse(RevRest); + _ -> Chars1 + end. + +pattern_parse_alt(Chars) -> + {Seq, Rest} = pattern_parse_seq(Chars), + case Rest of + [$| | Rest1] -> + {Next, Rest2} = pattern_parse_alt(Rest1), + Alts = + case Next of + {alt, More} -> [Seq | More]; + _ -> [Seq, Next] + end, + {{alt, Alts}, Rest2}; + _ -> + {Seq, Rest} + end. + +pattern_parse_seq(Chars) -> + pattern_parse_seq(Chars, []). + +pattern_parse_seq([], Acc) -> + {{seq, lists:reverse(Acc)}, []}; +pattern_parse_seq([C | _] = Chars, Acc) when C =:= $| orelse C =:= $) -> + {{seq, lists:reverse(Acc)}, Chars}; +pattern_parse_seq(Chars, Acc) -> + {Term, Rest} = pattern_parse_term(Chars), + pattern_parse_seq(Rest, [Term | Acc]). + +pattern_parse_term(Chars) -> + {Atom, Rest} = pattern_parse_atom(Chars), + pattern_parse_quantifier(Atom, Rest). + +pattern_parse_quantifier(Atom, [$* | Rest]) -> + {{repeat, Atom, 0, 6}, Rest}; +pattern_parse_quantifier(Atom, [$+ | Rest]) -> + {{repeat, Atom, 1, 6}, Rest}; +pattern_parse_quantifier(Atom, [$? | Rest]) -> + {{repeat, Atom, 0, 1}, Rest}; +pattern_parse_quantifier(Atom, [${ | Rest]) -> + pattern_parse_brace(Atom, Rest); +pattern_parse_quantifier(Atom, Rest) -> + {Atom, Rest}. + +pattern_parse_brace(Atom, Chars) -> + {Min, Rest1} = pattern_parse_int(Chars), + case Rest1 of + [$} | Rest2] -> + {{repeat, Atom, Min, Min}, Rest2}; + [$,, $} | Rest2] -> + {{repeat, Atom, Min, Min + 6}, Rest2}; + [$, | Rest2] -> + {Max, Rest3} = pattern_parse_int(Rest2), + [$} | Rest4] = Rest3, + {{repeat, Atom, Min, Max}, Rest4} + end. + +pattern_parse_int(Chars) -> + pattern_parse_int(Chars, []). + +pattern_parse_int([C | Rest], Acc) when C >= $0 andalso C =< $9 -> + pattern_parse_int(Rest, [C | Acc]); +pattern_parse_int(Rest, Acc) -> + {erlang:list_to_integer(lists:reverse(Acc)), Rest}. + +pattern_parse_atom([$( | Rest0]) -> + Rest1 = + case Rest0 of + [$?, $: | R] -> R; + _ -> Rest0 + end, + {AST, Rest2} = pattern_parse_alt(Rest1), + [$) | Rest3] = Rest2, + {{group, AST}, Rest3}; +pattern_parse_atom([$[ | Rest]) -> + pattern_parse_class(Rest); +pattern_parse_atom([$\\, Escaped | Rest]) -> + {pattern_escape(Escaped), Rest}; +pattern_parse_atom([$. | Rest]) -> + {{class, pattern_printable()}, Rest}; +pattern_parse_atom([C | Rest]) -> + {{lit, C}, Rest}. + +pattern_parse_class([$^ | Rest]) -> + {Set, Rest1} = pattern_parse_class_body(Rest, []), + {{class, pattern_printable() -- Set}, Rest1}; +pattern_parse_class(Rest) -> + {Set, Rest1} = pattern_parse_class_body(Rest, []), + {{class, Set}, Rest1}. + +pattern_parse_class_body([$] | Rest], Acc) -> + {lists:usort(lists:append(Acc)), Rest}; +pattern_parse_class_body([$\\, Escaped | Rest], Acc) -> + {class, Chars} = pattern_escape(Escaped), + pattern_parse_class_body(Rest, [Chars | Acc]); +pattern_parse_class_body([A, $-, B | Rest], Acc) when B =/= $] -> + pattern_parse_class_body(Rest, [lists:seq(A, B) | Acc]); +pattern_parse_class_body([C | Rest], Acc) -> + pattern_parse_class_body(Rest, [[C] | Acc]). + +pattern_escape($d) -> + {class, lists:seq($0, $9)}; +pattern_escape($w) -> + {class, lists:seq($a, $z) ++ lists:seq($A, $Z) ++ lists:seq($0, $9) ++ [$_]}; +pattern_escape($s) -> + {class, [$\s, $\t]}; +pattern_escape(C) -> + {lit, C}. + +pattern_printable() -> + lists:seq($a, $z) ++ lists:seq($A, $Z) ++ lists:seq($0, $9). + +pattern_gen({alt, Alts}) -> + triq_dom:oneof([pattern_gen(Alt) || Alt <- Alts]); +pattern_gen({seq, Terms}) -> + pattern_gen_seq(Terms); +pattern_gen({group, AST}) -> + pattern_gen(AST); +pattern_gen({lit, C}) -> + triq_dom:return([C]); +pattern_gen({class, Chars}) -> + triq_dom:bind( + triq_dom:elements(Chars), + fun(C) -> triq_dom:return([C]) end + ); +pattern_gen({repeat, Term, Min, Max}) -> + triq_dom:bind( + triq_dom:int(Min, Max), + fun(N) -> + triq_dom:bind( + triq_dom:vector(N, pattern_gen(Term)), + fun(Lists) -> triq_dom:return(lists:append(Lists)) end + ) + end + ). + +pattern_gen_seq([]) -> + triq_dom:return([]); +pattern_gen_seq([Term | Terms]) -> + triq_dom:bind( + pattern_gen(Term), + fun(Head) -> + triq_dom:bind( + pattern_gen_seq(Terms), + fun(Tail) -> triq_dom:return(Head ++ Tail) end + ) + end + ). + base64_chars() -> lists:append( [ diff --git a/test/property_test/restcheck_triq_properties.erl b/test/property_test/restcheck_triq_properties.erl index 87aeb24..1208c7b 100644 --- a/test/property_test/restcheck_triq_properties.erl +++ b/test/property_test/restcheck_triq_properties.erl @@ -40,7 +40,7 @@ prop_enum() -> [1, false, <<"bar">>, #{<<"foo">> => <<"baz">>}], #{<<"foo">> => <<"bar">>} ], - Schema = #{<<"enum">> => Enum}, + Schema = #{enum => Enum}, ?FORALL( Value, restcheck_triq:dto(Schema), @@ -48,7 +48,7 @@ prop_enum() -> ). prop_boolean() -> - Schema = #{<<"type">> => <<"boolean">>}, + Schema = #{type => boolean}, ?FORALL( Value, restcheck_triq:dto(Schema), @@ -57,9 +57,9 @@ prop_boolean() -> prop_integer_1() -> Schema = #{ - <<"type">> => <<"integer">>, - <<"minimum">> => 0, - <<"maximum">> => 0 + type => integer, + minimum => 0, + maximum => 0 }, ?FORALL( Value, @@ -69,12 +69,12 @@ prop_integer_1() -> prop_integer_2() -> Schema = #{ - <<"type">> => <<"integer">>, - <<"minimum">> => 2, - <<"exclusiveMinimum">> => false, - <<"maximum">> => 12, - <<"exclusiveMaximum">> => true, - <<"multipleOf">> => 3 + type => integer, + minimum => 2, + exclusive_minimum => false, + maximum => 12, + exclusive_maximum => true, + multiple_of => 3 }, ?FORALL( Value, @@ -84,12 +84,12 @@ prop_integer_2() -> prop_integer_3() -> Schema = #{ - <<"type">> => <<"integer">>, - <<"minimum">> => -12, - <<"exclusiveMinimum">> => true, - <<"maximum">> => 2, - <<"exclusiveMaximum">> => false, - <<"multipleOf">> => -3 + type => integer, + minimum => -12, + exclusive_minimum => true, + maximum => 2, + exclusive_maximum => false, + multiple_of => -3 }, ?FORALL( Value, @@ -99,11 +99,11 @@ prop_integer_3() -> prop_number_1() -> Schema = #{ - <<"type">> => <<"number">>, - <<"minimum">> => -2.5, - <<"exclusiveMinimum">> => true, - <<"maximum">> => 2.5, - <<"exclusiveMaximum">> => false + type => float, + minimum => -2.5, + exclusive_minimum => true, + maximum => 2.5, + exclusive_maximum => false }, ?FORALL( Value, @@ -113,11 +113,11 @@ prop_number_1() -> prop_number_2() -> Schema = #{ - <<"type">> => <<"number">>, - <<"minimum">> => -2.5, - <<"exclusiveMinimum">> => false, - <<"maximum">> => 2.5, - <<"exclusiveMaximum">> => true + type => float, + minimum => -2.5, + exclusive_minimum => false, + maximum => 2.5, + exclusive_maximum => true }, ?FORALL( Value, @@ -127,9 +127,9 @@ prop_number_2() -> prop_string() -> Schema = #{ - <<"type">> => <<"string">>, - <<"minLength">> => 2, - <<"maxLength">> => 4 + type => string, + min_length => 2, + max_length => 4 }, ?FORALL( Value, @@ -141,10 +141,10 @@ prop_string() -> prop_string_base64() -> Schema = #{ - <<"type">> => <<"string">>, - <<"format">> => <<"base64">>, - <<"minLength">> => 4, - <<"maxLength">> => 4 + type => string, + format => base64, + min_length => 4, + max_length => 4 }, ?FORALL( Value, @@ -152,10 +152,22 @@ prop_string_base64() -> string:length(Value) =:= 4 ). +prop_string_pattern() -> + Pattern = <<"^[0-9]{8}T[0-9]{6}([+-][0-9]{4}|Z)?$">>, + Schema = #{ + type => string, + pattern => Pattern + }, + ?FORALL( + Value, + restcheck_triq:dto(Schema), + is_binary(Value) andalso re:run(Value, Pattern) =/= nomatch + ). + 'prop_string_iso8601-datetime'() -> Schema = #{ - <<"type">> => <<"string">>, - <<"format">> => <<"iso8601-datetime">> + type => string, + format => iso8601 }, ?FORALL( Value, @@ -165,9 +177,9 @@ prop_string_base64() -> prop_array_1() -> Schema = #{ - <<"type">> => <<"array">>, - <<"items">> => #{ - <<"type">> => <<"number">> + type => array, + items => #{ + type => float } }, ?FORALL( @@ -178,13 +190,13 @@ prop_array_1() -> prop_array_2() -> Schema = #{ - <<"type">> => <<"array">>, - <<"items">> => #{ - <<"type">> => <<"boolean">> + type => array, + items => #{ + type => boolean }, - <<"minItems">> => 1, - <<"maxItems">> => 5, - <<"uniqueItems">> => true + min_items => 1, + max_items => 5, + unique_items => true }, ?FORALL( Value, @@ -195,10 +207,10 @@ prop_array_2() -> prop_object_1() -> Schema = #{ - <<"type">> => <<"object">>, - <<"minProperties">> => 10, - <<"maxProperties">> => 20, - <<"additionalProperties">> => #{<<"type">> => <<"boolean">>} + type => object, + min_properties => 10, + max_properties => 20, + additional_properties => #{type => boolean} }, ?FORALL( Value, @@ -213,14 +225,14 @@ prop_object_1() -> prop_object_2() -> Schema = #{ - <<"type">> => <<"object">>, - <<"properties">> => #{ - <<"foo">> => #{<<"type">> => <<"integer">>}, - <<"bar">> => #{<<"type">> => <<"boolean">>} + type => object, + properties => #{ + <<"foo">> => #{type => integer}, + <<"bar">> => #{type => boolean} }, - <<"required">> => [<<"foo">>, <<"bar">>], - <<"minProperties">> => 1, - <<"additionalProperties">> => false + required => [<<"foo">>, <<"bar">>], + min_properties => 1, + additional_properties => false }, ?FORALL( Value, @@ -230,9 +242,9 @@ prop_object_2() -> prop_all_of() -> Schema = #{ - <<"allOf">> => [ - #{<<"type">> => <<"integer">>, <<"minimum">> => 1}, - #{<<"type">> => <<"number">>, <<"maximum">> => 2.5, <<"exclusiveMaximum">> => true} + all_of => [ + #{type => integer, minimum => 1}, + #{type => float, maximum => 2.5, exclusive_maximum => true} ] }, ?FORALL( @@ -243,10 +255,10 @@ prop_all_of() -> prop_any_of() -> Schema = #{ - <<"anyOf">> => [ - #{<<"type">> => <<"boolean">>}, - #{<<"type">> => <<"number">>}, - #{<<"type">> => <<"string">>} + any_of => [ + #{type => boolean}, + #{type => float}, + #{type => string} ] }, ?FORALL( @@ -259,9 +271,9 @@ prop_any_of() -> prop_one_of() -> Schema = #{ - <<"oneOf">> => [ - #{<<"type">> => <<"boolean">>}, - #{<<"type">> => <<"string">>} + one_of => [ + #{type => boolean}, + #{type => string} ] }, ?FORALL( @@ -273,7 +285,7 @@ prop_one_of() -> prop_not() -> Schema = #{ - <<"not">> => #{<<"type">> => <<"boolean">>} + 'not' => #{type => boolean} }, ?FORALL( Value, diff --git a/test/restcheck_SUITE.erl b/test/restcheck_SUITE.erl index afbd644..0baae02 100644 --- a/test/restcheck_SUITE.erl +++ b/test/restcheck_SUITE.erl @@ -87,13 +87,13 @@ petstore(_Conf) -> #{<<"id">> => 2, <<"name">> => <<"Bar">>} ], {Code, Headers, Response} = - {200, [{<<"x-next">>, <<"/pets?nextPage=2">>}], njson:encode(Pets)}, + {200, [{<<"x-next">>, <<"/pets?nextPage=2">>}], element(2, njson:encode(Pets))}, {Code, [{<<"Content-Type">>, <<"application/json">>} | Headers], Response}; ([<<"pets">>], 'POST', _Headers, _QueryParameters, _Body) -> - {Code, Headers, Response} = {500, [], njson:encode(ServerError)}, + {Code, Headers, Response} = {500, [], element(2, njson:encode(ServerError))}, {Code, [{<<"Content-Type">>, <<"application/json">>} | Headers], Response}; ([<<"pets">>, _PetId], 'GET', _Headers, _QueryParameters, _Body) -> - {Code, Headers, Response} = {400, [], njson:encode(BadRequestError)}, + {Code, Headers, Response} = {400, [], element(2, njson:encode(BadRequestError))}, {Code, [{<<"Content-Type">>, <<"application/json">>} | Headers], Response} end ), @@ -102,7 +102,7 @@ petstore(_Conf) -> spec_path => unicode:characters_to_binary( code:priv_dir(restcheck) ++ "/oas/3.0/examples/petstore.json" ), - spec_format => erf_oas_3_0, + spec_format => erf_parser_oas_3_0, pbt_backend => restcheck_triq, host => <<"localhost">>, port => 8080, diff --git a/test/restcheck_client_SUITE.erl b/test/restcheck_client_SUITE.erl index a77c40b..82806f4 100644 --- a/test/restcheck_client_SUITE.erl +++ b/test/restcheck_client_SUITE.erl @@ -146,7 +146,7 @@ auth(_Conf) -> Value -> Value end, {200, [{<<"Content-Type">>, <<"application/json">>}], - njson:encode(#{<<"authorization">> => Authorization})} + element(2, njson:encode(#{<<"authorization">> => Authorization}))} end ), diff --git a/test/restcheck_schema_SUITE.erl b/test/restcheck_schema_SUITE.erl index 3b7b4f1..af30901 100644 --- a/test/restcheck_schema_SUITE.erl +++ b/test/restcheck_schema_SUITE.erl @@ -20,46 +20,59 @@ -compile([export_all, nowarn_export_all]). %%% MACROS --define(ENUM_SCHEMA, #{<<"enum">> => [1, <<"string">>, true]}). --define(BOOLEAN_SCHEMA, #{<<"type">> => <<"boolean">>}). +-define(ENUM_SCHEMA, #{enum => [1, <<"string">>, true]}). +-define(BOOLEAN_SCHEMA, #{type => boolean}). -define(INTEGER_SCHEMA, #{ - <<"type">> => <<"integer">>, - <<"minimum">> => 2, - <<"exclusiveMinimum">> => true, - <<"maximum">> => 6 + type => integer, + minimum => 2, + exclusive_minimum => true, + maximum => 6 }). -define(NUMBER_SCHEMA, #{ - <<"type">> => <<"number">>, - <<"minimum">> => 4, - <<"exclusiveMinimum">> => true, - <<"maximum">> => 8, - <<"exclusiveMaximum">> => true + type => float, + minimum => 4, + exclusive_minimum => true, + maximum => 8, + exclusive_maximum => true }). -define(STRING_SCHEMA, #{ - <<"type">> => <<"string">>, - <<"minLength">> => 3, - <<"maxLength">> => 6, - <<"pattern">> => <<"a{3}">> + type => string, + min_length => 3, + max_length => 6, + pattern => <<"a{3}">> }). -define(ARRAY_SCHEMA, #{ - <<"type">> => <<"array">>, - <<"items">> => ?NUMBER_SCHEMA, - <<"minItems">> => 1, - <<"maxItems">> => 3 + type => array, + items => ?NUMBER_SCHEMA, + min_items => 1, + max_items => 3 }). -define(OBJECT_SCHEMA, #{ - <<"type">> => <<"object">>, - <<"properties">> => #{ + type => object, + properties => #{ <<"foo">> => ?INTEGER_SCHEMA, <<"bar">> => ?STRING_SCHEMA }, - <<"minProperties">> => 3, - <<"additionalProperties">> => true + min_properties => 3, + additional_properties => true }). --define(INTERSECTION_SCHEMA, #{<<"allOf">> => [?INTEGER_SCHEMA, ?NUMBER_SCHEMA]}). --define(UNION_SCHEMA, #{<<"anyOf">> => [?BOOLEAN_SCHEMA, ?STRING_SCHEMA]}). --define(SYMMETRIC_DIFFERENCE_SCHEMA, #{<<"oneOf">> => [?INTEGER_SCHEMA, ?NUMBER_SCHEMA]}). --define(COMPLEMENT_SCHEMA, #{<<"not">> => ?BOOLEAN_SCHEMA}). +-define(INTEGER_SCHEMA_2, #{ + type => integer, + minimum => 4, + exclusive_minimum => true, + maximum => 8, + exclusive_maximum => true +}). +-define(FLOAT_SCHEMA_A, #{ + type => float, + minimum => 2, + exclusive_minimum => true, + maximum => 6 +}). +-define(INTERSECTION_SCHEMA, #{all_of => [?INTEGER_SCHEMA, ?INTEGER_SCHEMA_2]}). +-define(UNION_SCHEMA, #{any_of => [?BOOLEAN_SCHEMA, ?STRING_SCHEMA]}). +-define(SYMMETRIC_DIFFERENCE_SCHEMA, #{one_of => [?FLOAT_SCHEMA_A, ?NUMBER_SCHEMA]}). +-define(COMPLEMENT_SCHEMA, #{'not' => ?BOOLEAN_SCHEMA}). %%%----------------------------------------------------------------------------- %%% SUITE EXPORTS @@ -164,39 +177,38 @@ complement(_Conf) -> BooleanComplement = restcheck_schema:complement(?BOOLEAN_SCHEMA), ok = generate_and_load(boolean_complement, BooleanComplement), - ?assertEqual(false, boolean_complement:is_valid(false)), + assert_invalid(boolean_complement:is_valid(false)), ?assertEqual(true, boolean_complement:is_valid(1)), IntegerComplement = restcheck_schema:complement(?INTEGER_SCHEMA), ok = generate_and_load(integer_complement, IntegerComplement), - ?assertEqual(false, integer_complement:is_valid(3)), + assert_invalid(integer_complement:is_valid(3)), ?assertEqual(true, integer_complement:is_valid(1)), ?assertEqual(true, integer_complement:is_valid(true)), NumberComplement = restcheck_schema:complement(?NUMBER_SCHEMA), ok = generate_and_load(number_complement, NumberComplement), - ?assertEqual(false, number_complement:is_valid(5)), - ?assertEqual(false, number_complement:is_valid(5.5)), - ?assertEqual(true, number_complement:is_valid(9)), + assert_invalid(number_complement:is_valid(5.0)), + assert_invalid(number_complement:is_valid(5.5)), + ?assertEqual(true, number_complement:is_valid(9.0)), ?assertEqual(true, number_complement:is_valid(true)), StringComplement = restcheck_schema:complement(?STRING_SCHEMA), ok = generate_and_load(string_complement, StringComplement), - ?assertEqual(false, string_complement:is_valid(<<"123aaa">>)), + assert_invalid(string_complement:is_valid(<<"123aaa">>)), ?assertEqual(true, string_complement:is_valid(<<"123aa6">>)), ?assertEqual(true, string_complement:is_valid(<<"aaa4567">>)), ?assertEqual(true, string_complement:is_valid(true)), ArrayComplement = restcheck_schema:complement(?ARRAY_SCHEMA), ok = generate_and_load(array_complement, ArrayComplement), - ?assertEqual(false, array_complement:is_valid([5])), - ?assertEqual(true, array_complement:is_valid([9])), + assert_invalid(array_complement:is_valid([5.0])), + ?assertEqual(true, array_complement:is_valid([9.0])), ?assertEqual(true, array_complement:is_valid(true)), ObjectComplement = restcheck_schema:complement(?OBJECT_SCHEMA), ok = generate_and_load(object_complement, ObjectComplement), - ?assertEqual( - false, + assert_invalid( object_complement:is_valid(#{<<"foo">> => 4, <<"bar">> => <<"aaa">>, <<"baz">> => true}) ), ?assertEqual( @@ -211,83 +223,82 @@ complement(_Conf) -> IntersectionComplement = restcheck_schema:complement(?INTERSECTION_SCHEMA), ok = generate_and_load(intersection_complement, IntersectionComplement), - ?assertEqual(false, intersection_complement:is_valid(5)), + assert_invalid(intersection_complement:is_valid(5)), ?assertEqual(true, intersection_complement:is_valid(3)), ?assertEqual(true, intersection_complement:is_valid(true)), UnionComplement = restcheck_schema:complement(?UNION_SCHEMA), ok = generate_and_load(union_complement, UnionComplement), - ?assertEqual(false, union_complement:is_valid(true)), - ?assertEqual(false, union_complement:is_valid(<<"12aaa6">>)), + assert_invalid(union_complement:is_valid(true)), + assert_invalid(union_complement:is_valid(<<"12aaa6">>)), ?assertEqual(true, union_complement:is_valid(<<"123456">>)), ?assertEqual(true, union_complement:is_valid(<<"aaa4567">>)), ?assertEqual(true, union_complement:is_valid([1, 2, 3])), SymmetricDifferenceComplement = restcheck_schema:complement(?SYMMETRIC_DIFFERENCE_SCHEMA), ok = generate_and_load(symmetric_difference_complement, SymmetricDifferenceComplement), - ?assertEqual(false, symmetric_difference_complement:is_valid(3)), - ?assertEqual(false, symmetric_difference_complement:is_valid(7)), - ?assertEqual(true, symmetric_difference_complement:is_valid(5)), + assert_invalid(symmetric_difference_complement:is_valid(3.0)), + assert_invalid(symmetric_difference_complement:is_valid(7.0)), + ?assertEqual(true, symmetric_difference_complement:is_valid(5.0)), ?assertEqual(true, symmetric_difference_complement:is_valid(true)), ComplementComplement = restcheck_schema:complement(?COMPLEMENT_SCHEMA), ok = generate_and_load(complement_complement, ComplementComplement), - ?assertEqual(false, complement_complement:is_valid(5)), + assert_invalid(complement_complement:is_valid(5)), ?assertEqual(true, complement_complement:is_valid(true)), ok. intersection(_Conf) -> %% TODO: implement enum validation for non-strings - % EnumIntersection = restcheck_schema:intersection([?ENUM_SCHEMA, #{<<"enum">> => [true, #{<<"foo">> => <<"bar">>}]}]), + % EnumIntersection = restcheck_schema:intersection([?ENUM_SCHEMA, #{enum => [true, #{<<"foo">> => <<"bar">>}]}]), % ok = generate_and_load(enum_intersection, EnumIntersection), % false = enum_intersection:is_valid(#{<<"foo">> => <<"bar">>}), % false = enum_intersection:is_valid([1, 2, 3]), % true = enum_intersection:is_valid(true), BooleanIntersection = restcheck_schema:intersection([ - ?BOOLEAN_SCHEMA, #{<<"type">> => <<"boolean">>} + ?BOOLEAN_SCHEMA, #{type => boolean} ]), ok = generate_and_load(boolean_intersection, BooleanIntersection), - ?assertEqual(false, boolean_intersection:is_valid(<<"string">>)), + assert_invalid(boolean_intersection:is_valid(<<"string">>)), ?assertEqual(true, boolean_intersection:is_valid(true)), IntegerIntersection = restcheck_schema:intersection([ - ?INTEGER_SCHEMA, #{<<"type">> => <<"integer">>, <<"minimum">> => 4} + ?INTEGER_SCHEMA, #{type => integer, minimum => 4} ]), ok = generate_and_load(integer_intersection, IntegerIntersection), - ?assertEqual(false, integer_intersection:is_valid(3)), + assert_invalid(integer_intersection:is_valid(3)), ?assertEqual(true, integer_intersection:is_valid(4)), NumberIntersection = restcheck_schema:intersection([ - ?NUMBER_SCHEMA, #{<<"type">> => <<"number">>, <<"maximum">> => 10} + ?NUMBER_SCHEMA, #{type => float, maximum => 10} ]), ok = generate_and_load(number_intersection, NumberIntersection), - ?assertEqual(false, number_intersection:is_valid(1.0)), + assert_invalid(number_intersection:is_valid(1.0)), ?assertEqual(true, number_intersection:is_valid(7.0)), StringIntersection = restcheck_schema:intersection([ - ?STRING_SCHEMA, #{<<"type">> => <<"string">>, <<"pattern">> => <<"b{3}">>} + ?STRING_SCHEMA, #{type => string, pattern => <<"b{3}">>} ]), ok = generate_and_load(string_intersection, StringIntersection), - ?assertEqual(false, string_intersection:is_valid(<<"123aaa">>)), - ?assertEqual(false, string_intersection:is_valid(<<"bbb">>)), + assert_invalid(string_intersection:is_valid(<<"123aaa">>)), + assert_invalid(string_intersection:is_valid(<<"bbb">>)), ?assertEqual(true, string_intersection:is_valid(<<"aaabbb">>)), ArrayIntersection = restcheck_schema:intersection([ - ?ARRAY_SCHEMA, #{<<"type">> => <<"array">>, <<"items">> => ?INTEGER_SCHEMA} + ?ARRAY_SCHEMA, #{type => array, items => ?INTEGER_SCHEMA} ]), ok = generate_and_load(array_intersection, ArrayIntersection), - ?assertEqual(false, array_intersection:is_valid([2])), - ?assertEqual(false, array_intersection:is_valid([5.0, 5.1, 5.2])), + assert_invalid(array_intersection:is_valid([2])), + assert_invalid(array_intersection:is_valid([5.0, 5.1, 5.2])), ?assertEqual(true, array_intersection:is_valid([5, 5, 5])), ObjectIntersection = restcheck_schema:intersection([ - ?OBJECT_SCHEMA, #{<<"type">> => <<"object">>, <<"maxProperties">> => 4} + ?OBJECT_SCHEMA, #{type => object, max_properties => 4} ]), ok = generate_and_load(object_intersection, ObjectIntersection), - ?assertEqual( - false, + assert_invalid( object_intersection:is_valid(#{ <<"foo">> => 4, <<"bar">> => <<"aaa">>, @@ -296,8 +307,7 @@ intersection(_Conf) -> <<"qux">> => <<"quux">> }) ), - ?assertEqual( - false, + assert_invalid( object_intersection:is_valid(#{ <<"foo">> => 4, <<"bar">> => false, <<"baz">> => true, <<"foobar">> => 1 }) @@ -308,94 +318,94 @@ intersection(_Conf) -> ), IntersectionIntersection = restcheck_schema:intersection([ - ?INTERSECTION_SCHEMA, #{<<"allOf">> => [#{<<"type">> => <<"integer">>, <<"minimum">> => 5}]} + ?INTERSECTION_SCHEMA, #{all_of => [#{type => integer, minimum => 5}]} ]), ok = generate_and_load(intersection_intersection, IntersectionIntersection), - ?assertEqual(false, intersection_intersection:is_valid(4)), - ?assertEqual(false, intersection_intersection:is_valid(9)), + assert_invalid(intersection_intersection:is_valid(4)), + assert_invalid(intersection_intersection:is_valid(9)), ?assertEqual(true, intersection_intersection:is_valid(5)), UnionIntersection = restcheck_schema:intersection([ - ?UNION_SCHEMA, #{<<"anyOf">> => [?BOOLEAN_SCHEMA, ?NUMBER_SCHEMA]} + ?UNION_SCHEMA, #{any_of => [?BOOLEAN_SCHEMA, ?NUMBER_SCHEMA]} ]), ok = generate_and_load(union_intersection, UnionIntersection), - ?assertEqual(false, union_intersection:is_valid(5.0)), - ?assertEqual(false, union_intersection:is_valid(<<"foo">>)), + assert_invalid(union_intersection:is_valid(5.0)), + assert_invalid(union_intersection:is_valid(<<"foo">>)), ?assertEqual(true, union_intersection:is_valid(true)), SymmetricDifferenceIntersection = restcheck_schema:intersection([ - ?SYMMETRIC_DIFFERENCE_SCHEMA, #{<<"oneOf">> => [?INTEGER_SCHEMA, ?STRING_SCHEMA]} + ?SYMMETRIC_DIFFERENCE_SCHEMA, #{one_of => [?FLOAT_SCHEMA_A, ?STRING_SCHEMA]} ]), ok = generate_and_load(symmetric_difference_intersection, SymmetricDifferenceIntersection), - ?assertEqual(false, symmetric_difference_intersection:is_valid(<<"foo">>)), - ?assertEqual(false, symmetric_difference_intersection:is_valid(5)), - ?assertEqual(true, symmetric_difference_intersection:is_valid(3)), + assert_invalid(symmetric_difference_intersection:is_valid(<<"foo">>)), + assert_invalid(symmetric_difference_intersection:is_valid(5.0)), + ?assertEqual(true, symmetric_difference_intersection:is_valid(3.0)), ComplementIntersection = restcheck_schema:intersection([ - ?COMPLEMENT_SCHEMA, #{<<"not">> => ?BOOLEAN_SCHEMA} + ?COMPLEMENT_SCHEMA, #{'not' => ?BOOLEAN_SCHEMA} ]), ok = generate_and_load(complement_intersection, ComplementIntersection), - ?assertEqual(false, complement_intersection:is_valid(true)), + assert_invalid(complement_intersection:is_valid(true)), ?assertEqual(true, complement_intersection:is_valid(<<"foo">>)), ok. union(_Conf) -> %% TODO: implement enum validation for non-strings - % EnumUnion = restcheck_schema:union([?ENUM_SCHEMA, #{<<"enum">> => [#{<<"foo">> => <<"bar">>}]}]), + % EnumUnion = restcheck_schema:union([?ENUM_SCHEMA, #{enum => [#{<<"foo">> => <<"bar">>}]}]), % ok = generate_and_load(enum_union, EnumUnion), % false = enum_union:is_valid(false), % true = enum_union:is_valid(#{<<"foo">> => <<"bar">>}), - BooleanUnion = restcheck_schema:union([?BOOLEAN_SCHEMA, #{<<"type">> => <<"boolean">>}]), + BooleanUnion = restcheck_schema:union([?BOOLEAN_SCHEMA, #{type => boolean}]), ok = generate_and_load(boolean_union, BooleanUnion), ?assertEqual(true, boolean_union:is_valid(true)), - ?assertEqual(false, boolean_union:is_valid(1)), + assert_invalid(boolean_union:is_valid(1)), IntegerUnion = restcheck_schema:union([ - ?INTEGER_SCHEMA, #{<<"type">> => <<"integer">>, <<"minimum">> => 6} + ?INTEGER_SCHEMA, #{type => integer, minimum => 6} ]), ok = generate_and_load(integer_union, IntegerUnion), ?assertEqual(true, integer_union:is_valid(4)), ?assertEqual(true, integer_union:is_valid(7)), - ?assertEqual(false, integer_union:is_valid(1)), - ?assertEqual(false, integer_union:is_valid(true)), + assert_invalid(integer_union:is_valid(1)), + assert_invalid(integer_union:is_valid(true)), NumberUnion = restcheck_schema:union([ - ?NUMBER_SCHEMA, #{<<"type">> => <<"number">>, <<"maximum">> => 5} + ?NUMBER_SCHEMA, #{type => float, maximum => 5} ]), ok = generate_and_load(number_union, NumberUnion), - ?assertEqual(true, number_union:is_valid(1)), - ?assertEqual(true, number_union:is_valid(7)), - ?assertEqual(false, number_union:is_valid(9)), - ?assertEqual(false, number_union:is_valid(true)), + ?assertEqual(true, number_union:is_valid(1.0)), + ?assertEqual(true, number_union:is_valid(7.0)), + assert_invalid(number_union:is_valid(9)), + assert_invalid(number_union:is_valid(true)), StringUnion = restcheck_schema:union([ - ?STRING_SCHEMA, #{<<"type">> => <<"string">>, <<"pattern">> => <<"b{3}">>} + ?STRING_SCHEMA, #{type => string, pattern => <<"b{3}">>} ]), ok = generate_and_load(string_union, StringUnion), ?assertEqual(true, string_union:is_valid(<<"bbb">>)), ?assertEqual(true, string_union:is_valid(<<"aaa">>)), - ?assertEqual(false, string_union:is_valid(<<"foo">>)), - ?assertEqual(false, string_union:is_valid(true)), + assert_invalid(string_union:is_valid(<<"foo">>)), + assert_invalid(string_union:is_valid(true)), ArrayUnion = restcheck_schema:union([ - ?ARRAY_SCHEMA, #{<<"type">> => <<"array">>, <<"items">> => ?BOOLEAN_SCHEMA} + ?ARRAY_SCHEMA, #{type => array, items => ?BOOLEAN_SCHEMA} ]), ok = generate_and_load(array_union, ArrayUnion), ?assertEqual(true, array_union:is_valid([true])), - ?assertEqual(true, array_union:is_valid([5.0, 6, 7.9999])), - ?assertEqual(false, array_union:is_valid([<<"foo">>, <<"bar">>, <<"baz">>])), - ?assertEqual(false, array_union:is_valid(true)), + ?assertEqual(true, array_union:is_valid([5.0, 6.0, 7.9999])), + assert_invalid(array_union:is_valid([<<"foo">>, <<"bar">>, <<"baz">>])), + assert_invalid(array_union:is_valid(true)), ObjectUnion = restcheck_schema:union([ ?OBJECT_SCHEMA, #{ - <<"type">> => <<"object">>, - <<"properties">> => #{ - <<"qux">> => #{<<"type">> => <<"boolean">>} + type => object, + properties => #{ + <<"qux">> => #{type => boolean} }, - <<"required">> => [<<"qux">>] + required => [<<"qux">>] } ]), ok = generate_and_load(object_union, ObjectUnion), @@ -403,36 +413,36 @@ union(_Conf) -> true, object_union:is_valid(#{<<"foo">> => 4, <<"bar">> => <<"aaa">>, <<"baz">> => true}) ), ?assertEqual(true, object_union:is_valid(#{<<"qux">> => true})), - ?assertEqual(false, object_union:is_valid(#{<<"foo">> => <<"foobar">>})), - ?assertEqual(false, object_union:is_valid(false)), + assert_invalid(object_union:is_valid(#{<<"foo">> => <<"foobar">>})), + assert_invalid(object_union:is_valid(false)), IntersectionUnion = restcheck_schema:union([ - ?INTERSECTION_SCHEMA, #{<<"allOf">> => [?BOOLEAN_SCHEMA]} + ?INTERSECTION_SCHEMA, #{all_of => [?BOOLEAN_SCHEMA]} ]), ok = generate_and_load(intersection_union, IntersectionUnion), ?assertEqual(true, intersection_union:is_valid(true)), ?assertEqual(true, intersection_union:is_valid(5)), - ?assertEqual(false, intersection_union:is_valid(5.1)), - ?assertEqual(false, intersection_union:is_valid(<<"foo">>)), + assert_invalid(intersection_union:is_valid(5.1)), + assert_invalid(intersection_union:is_valid(<<"foo">>)), UnionUnion = restcheck_schema:union([ - ?UNION_SCHEMA, #{<<"anyOf">> => [?INTEGER_SCHEMA, ?NUMBER_SCHEMA]} + ?UNION_SCHEMA, #{any_of => [?INTEGER_SCHEMA, ?NUMBER_SCHEMA]} ]), ok = generate_and_load(union_union, UnionUnion), ?assertEqual(true, union_union:is_valid(7.9)), ?assertEqual(true, union_union:is_valid(true)), - ?assertEqual(false, union_union:is_valid(#{<<"foo">> => <<"bar">>})), + assert_invalid(union_union:is_valid(#{<<"foo">> => <<"bar">>})), SymmetricDifferenceUnion = restcheck_schema:union([ - ?SYMMETRIC_DIFFERENCE_SCHEMA, #{<<"oneOf">> => [?BOOLEAN_SCHEMA, ?STRING_SCHEMA]} + ?SYMMETRIC_DIFFERENCE_SCHEMA, #{one_of => [?BOOLEAN_SCHEMA, ?STRING_SCHEMA]} ]), ok = generate_and_load(symmetric_difference_union, SymmetricDifferenceUnion), - ?assertEqual(true, symmetric_difference_union:is_valid(4)), + ?assertEqual(true, symmetric_difference_union:is_valid(4.0)), ?assertEqual(true, symmetric_difference_union:is_valid(true)), - ?assertEqual(false, symmetric_difference_union:is_valid(5)), - ?assertEqual(false, symmetric_difference_union:is_valid(#{})), + assert_invalid(symmetric_difference_union:is_valid(5.0)), + assert_invalid(symmetric_difference_union:is_valid(#{})), - ComplementUnion = restcheck_schema:union([?COMPLEMENT_SCHEMA, #{<<"not">> => ?OBJECT_SCHEMA}]), + ComplementUnion = restcheck_schema:union([?COMPLEMENT_SCHEMA, #{'not' => ?OBJECT_SCHEMA}]), ok = generate_and_load(complement_union, ComplementUnion), ?assertEqual(true, complement_union:is_valid(true)), ?assertEqual( @@ -449,3 +459,10 @@ union(_Conf) -> generate_and_load(Name, Schema) -> DTO = ndto:generate(Name, Schema), ok = ndto:load(DTO). + +assert_invalid(false) -> + ok; +assert_invalid({false, _Reason}) -> + ok; +assert_invalid(Other) -> + ct:fail({expected_invalid, Other}). diff --git a/test/restcheck_schema_dom.erl b/test/restcheck_schema_dom.erl index 3ea8712..84dc09c 100644 --- a/test/restcheck_schema_dom.erl +++ b/test/restcheck_schema_dom.erl @@ -64,9 +64,9 @@ string() -> triq_dom:int(0, MaxLength), fun(MinLength) -> #{ - <<"type">> => <<"string">>, - <<"minLength">> => MinLength, - <<"maxLength">> => MaxLength + type => string, + min_length => MinLength, + max_length => MaxLength } end ) @@ -93,11 +93,11 @@ number() -> fun({Min, Offset, ExclusiveMinimum, ExclusiveMaximum}) -> Max = Min + Offset, #{ - <<"type">> => <<"number">>, - <<"minimum">> => Min, - <<"exclusiveMinimum">> => ExclusiveMinimum, - <<"maximum">> => Max, - <<"exclusiveMaximum">> => ExclusiveMaximum + type => float, + minimum => Min, + exclusive_minimum => ExclusiveMinimum, + maximum => Max, + exclusive_maximum => ExclusiveMaximum } end ). @@ -111,12 +111,12 @@ integer() -> triq_dom:int(Min, Max), fun(MultipleOf) -> #{ - <<"type">> => <<"integer">>, - <<"minimum">> => Min, - <<"exclusiveMinimum">> => ExclusiveMinimum, - <<"maximum">> => Max, - <<"exclusiveMaximum">> => ExclusiveMaximum, - <<"multipleOf">> => MultipleOf + type => integer, + minimum => Min, + exclusive_minimum => ExclusiveMinimum, + maximum => Max, + exclusive_maximum => ExclusiveMaximum, + multiple_of => MultipleOf } end ) @@ -124,7 +124,7 @@ integer() -> ). boolean() -> - triq_dom:return(#{<<"type">> => <<"boolean">>}). + triq_dom:return(#{type => boolean}). array() -> array(?DEPTH_BOUND). @@ -134,11 +134,11 @@ array(Depth) -> {schema(Depth - 1), triq_dom:int(), triq_dom:pos_integer(), triq_dom:bool()}, fun({Schema, MinItems, Offset, UniqueItems}) -> #{ - <<"type">> => <<"array">>, - <<"items">> => Schema, - <<"minItems">> => MinItems, - <<"maxItems">> => MinItems + Offset, - <<"uniqueItems">> => UniqueItems + type => array, + items => Schema, + min_items => MinItems, + max_items => MinItems + Offset, + unique_items => UniqueItems } end ). @@ -156,8 +156,8 @@ object(Depth) -> case PropertyList of [] -> #{ - <<"type">> => <<"object">>, - <<"additionalProperties">> => AdditionalProperties + type => object, + additional_properties => AdditionalProperties }; _PL -> PropertyNames = proplists:get_keys(PropertyList), @@ -173,12 +173,12 @@ object(Depth) -> triq_dom:int(0, MaxProperties), fun(MinProperties) -> #{ - <<"type">> => <<"object">>, - <<"properties">> => Properties, - <<"required">> => Required, - <<"minProperties">> => MinProperties, - <<"maxProperties">> => MaxProperties, - <<"additionalProperties">> => AdditionalProperties + type => object, + properties => Properties, + required => Required, + min_properties => MinProperties, + max_properties => MaxProperties, + additional_properties => AdditionalProperties } end ) diff --git a/test/restcheck_triq_SUITE.erl b/test/restcheck_triq_SUITE.erl index 1a68272..5e20990 100644 --- a/test/restcheck_triq_SUITE.erl +++ b/test/restcheck_triq_SUITE.erl @@ -49,6 +49,7 @@ groups() -> {string, [parallel], [ string, string_base64, + string_pattern, 'string_iso8601-datetime' ]}, {array, [parallel], [ @@ -153,6 +154,12 @@ string_base64(Conf) -> Conf ). +string_pattern(Conf) -> + ct_property_test:quickcheck( + restcheck_triq_properties:prop_string_pattern(), + Conf + ). + 'string_iso8601-datetime'(Conf) -> ct_property_test:quickcheck( restcheck_triq_properties:'prop_string_iso8601-datetime'(),