diff --git a/.gitignore b/.gitignore index 3e30d8c8..1e247025 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /rebar3.crashdump /rebar.lock /test/*.beam +/deps/ \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index 1fe31636..defe5616 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ Andrei Neculau Stefan Strigler Sergey Prokhorov Yakov +Anton Belyaev Luis Azedo Carl-Johan Kjellander Alastair Hole diff --git a/README.md b/README.md index 92600072..74d951ed 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,38 @@ ok [<<"foo">>]}]} ``` +* Validate an instanse against a particular definition from schema definitions + +```erlang +1> Schema = <<"{\"definitions\": {\"Foo\": {\"properties\": {\"foo\": {\"type\": \"integer\"}}}, \"Bar\": {\"properties\": {\"bar\": {\"type\": \"boolean\"}}}}}">>. +<<"{\"definitions\": {\"Foo\": {\"properties\": {\"foo\": {\"type\": \"integer\"}}}, \"Bar\": {\"properties\": {\"bar\": {\"type\": \"boolea"...>> +2> jesse:validate_definition("Foo", +2> Schema, +2> <<"{\"foo\": 1}">>, +2> [{parser_fun, fun jiffy:decode/1}]). +{ok,[{<<"foo">>,1}]} +3> jesse:validate_definition("Bar", +3> Schema, +3> <<"{\"bar\": 2}">>, +3> [{parser_fun, fun jiffy:decode/1}]). +{error,[{data_invalid,[{<<"type">>,<<"boolean">>}], + wrong_type,2, + [<<"bar">>]}]} +4> jesse:validate_definition("FooBar", +4> Schema, +4> <<"{\"bar\": 2}">>, +4> [{parser_fun, fun jiffy:decode/1}]). +{error,[{schema_invalid,[{<<"definitions">>, + [{<<"Foo">>, + [{<<"properties">>, + [{<<"foo">>,[{<<"type">>,<<"integer">>}]}]}]}, + {<<"Bar">>, + [{<<"properties">>, + [{<<"bar">>,[{<<"type">>,<<"boolean">>}]}]}]}]}], + {schema_not_found,"#/definitions/FooBar"}}]} +``` + + * Since 0.4.0 it's possible to instruct jesse to collect errors, and not stop immediately when it finds an error in the given JSON instance: @@ -245,6 +277,11 @@ the given schema), one should use 'default_schema_ver' option when call a binary consisting a schema path, i.e. <<"http://json-schema.org/draft-03/schema#">>. +It is also possible to specify a validator module to use via `validator` option. +This option supersedes the mechanism with the $schema property described above. +Custom validator module can be specified as well. Such module should implement +`jesse_schema_validator` behaviour. + ## Validation errors The validation functions `jesse:validate/2` and `jesse:validate_with_schema/2,3` diff --git a/rebar.config b/rebar.config index 56465d0f..cf935fe7 100644 --- a/rebar.config +++ b/rebar.config @@ -11,6 +11,7 @@ , warnings_as_errors , warn_unused_record , warn_unused_vars + , nowarn_export_all ]}. {edoc_opts, [{preprocess, true}]}. {cover_enabled, true}. @@ -20,7 +21,6 @@ ]}. { deps , [ {jsx, "3.1.0"} - , {rfc3339, "0.2.2"} ]}. { project_plugins diff --git a/src/jesse.app.src b/src/jesse.app.src index d79e7ea7..3f02d5e1 100644 --- a/src/jesse.app.src +++ b/src/jesse.app.src @@ -12,7 +12,6 @@ , ssl , inets , jsx - , rfc3339 ]} , {licenses, [ "Apache 2.0" ]} diff --git a/src/jesse.erl b/src/jesse.erl index 0bda4f5c..80a32221 100644 --- a/src/jesse.erl +++ b/src/jesse.erl @@ -33,6 +33,10 @@ , load_schemas/3 , validate/2 , validate/3 + , validate_definition/3 + , validate_definition/4 + , validate_local_ref/3 + , validate_local_ref/4 , validate_with_schema/2 , validate_with_schema/3 ]). @@ -215,6 +219,82 @@ validate(Schema, Data, Options) -> {error, Error} end. +%% @doc Equivalent to {@link validate_definition/4} where `Options' is an empty list. +-spec validate_definition( Definition :: string() + , Schema :: json_term() | binary() + , Data :: json_term() | binary() + ) -> {ok, json_term()} + | jesse_error:error(). +validate_definition(Definition, Schema, Data) -> + validate_definition(Definition, Schema, Data, []). + +%% @doc Validates json `Data' agains the given `Definition' in the given +%% schema `Schema', using `Options'. +%% If the given json is valid, then it is returned to the caller, otherwise +%% an error with an appropriate error reason is returned. If the `parser_fun' +%% option is provided, then both `Schema' and `Data' are considered to be a +%% binary string, so `parser_fun' is used to convert both binary strings to a +%% supported internal representation of json. +%% If `parser_fun' is not provided, then both `Schema' and `Data' are considered +%% to already be a supported internal representation of json. +-spec validate_definition( Definition :: string() + , Schema :: json_term() | binary() + , Data :: json_term() | binary() + , Options :: [{Key :: atom(), Data :: any()}] + ) -> {ok, json_term()} + | jesse_error:error(). +validate_definition(Defintion, Schema, Data, Options) -> + try + ParserFun = proplists:get_value(parser_fun, Options, fun(X) -> X end), + ParsedSchema = try_parse(schema, ParserFun, Schema), + ParsedData = try_parse(data, ParserFun, Data), + jesse_schema_validator:validate_definition( Defintion + , ParsedSchema + , ParsedData + , Options + ) + catch + throw:Error -> {error, Error} + end. + +%% @doc Equivalent to {@link validate_local_ref/4} where `Options' is an empty list. +-spec validate_local_ref( RefPath :: string() + , Schema :: json_term() | binary() + , Data :: json_term() | binary() + ) -> {ok, json_term()} + | jesse_error:error(). +validate_local_ref(RefPath, Schema, Data) -> + validate_local_ref(RefPath, Schema, Data, []). + +%% @doc Validates json `Data' agains the given definition path 'RefPath' in the given +%% schema `Schema', using `Options'. +%% If the given json is valid, then it is returned to the caller, otherwise +%% an error with an appropriate error reason is returned. If the `parser_fun' +%% option is provided, then both `Schema' and `Data' are considered to be a +%% binary string, so `parser_fun' is used to convert both binary strings to a +%% supported internal representation of json. +%% If `parser_fun' is not provided, then both `Schema' and `Data' are considered +%% to already be a supported internal representation of json. +-spec validate_local_ref( RefPath :: string() + , Schema :: json_term() | binary() + , Data :: json_term() | binary() + , Options :: [{Key :: atom(), Data :: any()}] + ) -> {ok, json_term()} + | jesse_error:error(). +validate_local_ref(RefPath, Schema, Data, Options) -> + try + ParserFun = proplists:get_value(parser_fun, Options, fun(X) -> X end), + ParsedSchema = try_parse(schema, ParserFun, Schema), + ParsedData = try_parse(data, ParserFun, Data), + jesse_schema_validator:validate_local_ref( RefPath + , ParsedSchema + , ParsedData + , Options + ) + catch + throw:Error -> {error, Error} + end. + %% @doc Equivalent to {@link validate_with_schema/3} where `Options' %% is an empty list. -spec validate_with_schema( Schema :: schema() | binary() diff --git a/src/jesse_database.erl b/src/jesse_database.erl index b1f7f074..d5dafa50 100644 --- a/src/jesse_database.erl +++ b/src/jesse_database.erl @@ -303,13 +303,13 @@ get_schema_info(File, {Acc, ParseFun}) -> %% @doc Returns value of "id" field from json object `Schema', assuming that %% the given json object has such a field, otherwise returns undefined. %% @private --spec get_schema_id(Schema :: jesse:json_term()) -> string() | undefined. +-spec get_schema_id(Schema :: jesse:json_term()) -> binary() | undefined. get_schema_id(Schema) -> case jesse_json_path:value(?ID, Schema, undefined) of undefined -> undefined; Id -> - erlang:binary_to_list(Id) + Id end. %% @private diff --git a/src/jesse_schema_validator.erl b/src/jesse_schema_validator.erl index 69611ae3..311bdc48 100644 --- a/src/jesse_schema_validator.erl +++ b/src/jesse_schema_validator.erl @@ -25,12 +25,30 @@ %% API -export([ validate/3 + , validate_definition/4 + , validate_local_ref/4 , validate_with_state/3 ]). %% Includes -include("jesse_schema_validator.hrl"). +%% Behaviour definition +-callback check_value(Value, Attr, State) -> + State | no_return() + when + Value :: any(), + Attr :: {binary(), jesse:json_term()}, + State :: jesse_state:state(). + +-callback init_state(Opts :: jesse_state:validator_opts()) -> + validator_state(). + +-type validator_state() :: any(). + +-export_type([ validator_state/0 + ]). + %%% API %% @doc Validates json `Data' against `JsonSchema' with `Options'. %% If the given json is valid, then it is returned to the caller as is, @@ -45,6 +63,34 @@ validate(JsonSchema, Value, Options) -> NewState = validate_with_state(JsonSchema, Value, State), {result(NewState), Value}. +%% @doc Validates json `Data' against a given $ref path in `JsonSchema' with `Options'. +%% If the given json is valid, then it is returned to the caller as is, +%% otherwise an exception will be thrown. +-spec validate_definition( Definition :: string() + , JsonSchema :: jesse:json_term() + , Data :: jesse:json_term() + , Options :: [{Key :: atom(), Data :: any()}] + ) -> {ok, jesse:json_term()} + | no_return(). +validate_definition(Definition, JsonSchema, Value, Options) -> + RefPath = "#/definitions/" ++ Definition, + validate_local_ref(RefPath, JsonSchema, Value, Options). + +%% @doc Validates json `Data' against `Definition' in `JsonSchema' with `Options'. +%% If the given json is valid, then it is returned to the caller as is, +%% otherwise an exception will be thrown. +-spec validate_local_ref( RefPath :: string() + , JsonSchema :: jesse:json_term() + , Data :: jesse:json_term() + , Options :: [{Key :: atom(), Data :: any()}] + ) -> {ok, jesse:json_term()} + | no_return(). +validate_local_ref(RefPath, JsonSchema, Value, Options) -> + State = jesse_state:new(JsonSchema, Options), + Ref = make_ref(RefPath), + NewState = validate_with_state(Ref, Value, State), + {result(NewState), Value}. + %% @doc Validates json `Data' against `JsonSchema' with `State'. %% If the given json is valid, then the latest state is returned to the caller, %% otherwise an exception will be thrown. @@ -53,11 +99,31 @@ validate(JsonSchema, Value, Options) -> , State :: jesse_state:state() ) -> jesse_state:state() | no_return(). -validate_with_state(JsonSchema, Value, State) -> - SchemaVer = get_schema_ver(JsonSchema, State), - select_and_run_validator(SchemaVer, JsonSchema, Value, State). +validate_with_state(JsonSchema0, Value, State) -> + Validator = select_validator(JsonSchema0, State), + JsonSchema = jesse_json_path:unwrap_value(JsonSchema0), + run_validator(Validator, Value, JsonSchema, State). + %%% Internal functions +%% @doc Gets validator from the state or else +%% selects an appropriate one by schema version. +%% @private +select_validator(JsonSchema, State) -> + case jesse_state:get_validator(State) of + undefined -> + select_validator_by_schema(get_schema_ver(JsonSchema, State), State); + Validator -> + Validator + end. + +select_validator_by_schema(?json_schema_draft3, _) -> + jesse_validator_draft3; +select_validator_by_schema(?json_schema_draft4, _) -> + jesse_validator_draft4; +select_validator_by_schema(SchemaURI, State) -> + jesse_error:handle_schema_invalid({?schema_unsupported, SchemaURI}, State). + %% @doc Returns "$schema" property from `JsonSchema' if it is present, %% otherwise the default schema version from `State' is returned. %% @private @@ -76,18 +142,21 @@ result(State) -> _ -> throw(ErrorList) end. -%% @doc Runs appropriate validator depending on schema version -%% it is called with. +%% @doc Goes through attributes of the given `JsonSchema' and +%% validates the `Value' against them calling `Validator'. %% @private -select_and_run_validator(?json_schema_draft3, JsonSchema, Value, State) -> - jesse_validator_draft3:check_value( Value - , jesse_json_path:unwrap_value(JsonSchema) - , State - ); -select_and_run_validator(?json_schema_draft4, JsonSchema, Value, State) -> - jesse_validator_draft4:check_value( Value - , jesse_json_path:unwrap_value(JsonSchema) - , State - ); -select_and_run_validator(SchemaURI, _JsonSchema, _Value, State) -> - jesse_error:handle_schema_invalid({?schema_unsupported, SchemaURI}, State). +run_validator(_Validator, _Value, [], State) -> + State; +run_validator(Validator, Value, [Attr | Attrs], State0) -> + State = Validator:check_value( Value + , Attr + , State0 + ), + run_validator(Validator, Value, Attrs, State). + +%% @doc Makes a $ref schema object pointing to the given path +%% in schema defintions. +%% @private +make_ref(RefPath) -> + RefPath1 = list_to_binary(RefPath), + [{<<"$ref">>, RefPath1}]. diff --git a/src/jesse_state.erl b/src/jesse_state.erl index 95136b9b..81890227 100644 --- a/src/jesse_state.erl +++ b/src/jesse_state.erl @@ -33,11 +33,14 @@ , get_default_schema_ver/1 , get_error_handler/1 , get_error_list/1 + , get_validator/1 + , get_validator_state/1 , new/2 , remove_last_from_path/1 , set_allowed_errors/2 , set_current_schema/2 , set_error_list/2 + , set_validator_state/2 , resolve_ref/2 , undo_resolve_ref/2 , canonical_path/2 @@ -45,6 +48,7 @@ ]). -export_type([ state/0 + , validator_opts/0 ]). %% Includes @@ -62,6 +66,8 @@ , id :: jesse:schema_id() , root_schema :: jesse:schema() , schema_loader_fun :: jesse:schema_loader_fun() + , validator = undefined :: module() | 'undefined' + , validator_state = undefined :: any() | 'undefined' } ). @@ -71,6 +77,8 @@ -opaque state() :: #state{}. +-type validator_opts() :: any(). + %%% API %% @doc Adds `Property' to the `current_path' in `State'. -spec add_to_path( State :: state() @@ -118,6 +126,16 @@ get_error_handler(#state{error_handler = ErrorHandler}) -> get_error_list(#state{error_list = ErrorList}) -> ErrorList. +%% @doc Getter for `validator'. +-spec get_validator(State :: state()) -> module() | undefined. +get_validator(#state{validator = Validator}) -> + Validator. + +%% @doc Getter for `validator_state'. +-spec get_validator_state(State :: state()) -> any() | undefined. +get_validator_state(#state{validator_state = ValidatorState}) -> + ValidatorState. + %% @doc Returns newly created state. -spec new( JsonSchema :: jesse:schema() , Options :: jesse:options() @@ -146,6 +164,17 @@ new(JsonSchema, Options) -> , Options , ?default_schema_loader_fun ), + Validator = proplists:get_value( validator + , Options + , undefined + ), + ValidatorOpts = proplists:get_value( validator_opts + , Options + , undefined + ), + ValidatorState = init_validator_state( Validator + , ValidatorOpts + ), NewState = #state{ root_schema = JsonSchema , current_path = [] , allowed_errors = AllowedErrors @@ -154,6 +183,8 @@ new(JsonSchema, Options) -> , default_schema_ver = DefaultSchemaVer , schema_loader_fun = LoaderFun , external_validator = ExternalValidator + , validator = Validator + , validator_state = ValidatorState }, set_current_schema(NewState, JsonSchema). @@ -184,6 +215,11 @@ set_current_schema(#state{id = Id} = State, NewSchema) -> set_error_list(State, ErrorList) -> State#state{error_list = ErrorList}. +%% @doc Setter for `validator_state'. +-spec set_validator_state(State :: state(), ValidatorState :: any()) -> state(). +set_validator_state(State, ValidatorState) -> + State#state{validator_state = ValidatorState}. + %% @doc Resolve a reference. -spec resolve_ref(State :: state(), Reference :: jesse:schema_ref()) -> state(). resolve_ref(State, Reference) -> @@ -248,6 +284,16 @@ undo_resolve_ref(RefState, OriginalState) -> , id = OriginalState#state.id }. +%% @doc Init custom validator state. +%% @private +-spec init_validator_state( Validator :: module() | undefined + , Opts :: validator_opts() + ) -> jesse_schema_validator:validator_state(). +init_validator_state(undefined, _) -> + undefined; +init_validator_state(Validator, Opts) -> + Validator:init_state(Opts). + %% @doc Retrieve a specific part of a schema %% @private -spec load_local_schema( Schema :: ?not_found | jesse:schema() diff --git a/src/jesse_validator_draft3.erl b/src/jesse_validator_draft3.erl index 2b32bbd9..1ab698db 100644 --- a/src/jesse_validator_draft3.erl +++ b/src/jesse_validator_draft3.erl @@ -22,9 +22,11 @@ %%%============================================================================= -module(jesse_validator_draft3). +-behaviour(jesse_schema_validator). %% API --export([ check_value/3 +-export([ init_state/1 + , check_value/3 ]). %% Includes @@ -56,172 +58,153 @@ | {data_error(), binary()}. %%% API -%% @doc Goes through attributes of the given schema `JsonSchema' and -%% validates the value `Value' against them. --spec check_value( Value :: jesse:json_term() - , JsonSchema :: jesse:schema() - , State :: jesse_state:state() - ) -> jesse_state:state() | no_return(). -check_value(Value, [{?REF, RefSchemaURI} | Attrs], State) -> - case Attrs of - [] -> - validate_ref(Value, RefSchemaURI, State); - _ -> - handle_schema_invalid(?only_ref_allowed, State) +%% @doc Behaviour callback. Custom state is not used by this validator. +-spec init_state(_) -> undefined. +init_state(_) -> + undefined. + +%% @doc Validates the value `Value' against the attributes +%% of the given schema `JsonSchema'. +-spec check_value(Value, Attr, State) -> + State | no_return() + when + Value :: any(), + Attr :: {binary(), jesse:json_term()}, + State :: jesse_state:state(). +check_value(Value, {?TYPE, Type}, State) -> + check_type(Value, Type, State); +check_value(Value, {?PROPERTIES, Properties}, State) -> + case jesse_lib:is_json_object(Value) of + true -> check_properties( Value + , unwrap(Properties) + , State + ); + false -> State end; -check_value(Value, [{?TYPE, Type} | Attrs], State) -> - NewState = check_type(Value, Type, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?PROPERTIES, Properties} | Attrs], State) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_properties( Value - , unwrap(Properties) - , State - ); - false -> State - end, - check_value(Value, Attrs, NewState); check_value( Value - , [{?PATTERNPROPERTIES, PatternProperties} | Attrs] + , {?PATTERNPROPERTIES, PatternProperties} , State ) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_pattern_properties( Value - , PatternProperties - , State - ); - false -> State - end, - check_value(Value, Attrs, NewState); + case jesse_lib:is_json_object(Value) of + true -> check_pattern_properties( Value + , PatternProperties + , State + ); + false -> State + end; check_value( Value - , [{?ADDITIONALPROPERTIES, AdditionalProperties} | Attrs] + , {?ADDITIONALPROPERTIES, AdditionalProperties} , State ) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_additional_properties( Value - , AdditionalProperties - , State - ); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?ITEMS, Items} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_items(Value, Items, State); - false -> State - end, - check_value(Value, Attrs, NewState); + case jesse_lib:is_json_object(Value) of + true -> check_additional_properties( Value + , AdditionalProperties + , State + ); + false -> State + end; +check_value(Value, {?ITEMS, Items}, State) -> + case jesse_lib:is_array(Value) of + true -> check_items(Value, Items, State); + false -> State + end; %% doesn't really do anything, since this attribute will be handled %% by the previous function clause if it's presented in the schema -check_value( Value - , [{?ADDITIONALITEMS, _AdditionalItems} | Attrs] +check_value( _Value + , {?ADDITIONALITEMS, _AdditionalItems} , State ) -> - check_value(Value, Attrs, State); + State; %% doesn't really do anything, since this attribute will be handled %% by the previous function clause if it's presented in the schema -check_value(Value, [{?REQUIRED, _Required} | Attrs], State) -> - check_value(Value, Attrs, State); -check_value(Value, [{?DEPENDENCIES, Dependencies} | Attrs], State) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_dependencies(Value, Dependencies, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MINIMUM, Minimum} | Attrs], State) -> - NewState = case is_number(Value) of - true -> - ExclusiveMinimum = get_value( ?EXCLUSIVEMINIMUM - , get_current_schema(State) - ), - check_minimum(Value, Minimum, ExclusiveMinimum, State); - false -> - State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MAXIMUM, Maximum} | Attrs], State) -> - NewState = case is_number(Value) of - true -> - ExclusiveMaximum = get_value( ?EXCLUSIVEMAXIMUM - , get_current_schema(State) - ), - check_maximum(Value, Maximum, ExclusiveMaximum, State); - false -> - State - end, - check_value(Value, Attrs, NewState); +check_value(_Value, {?REQUIRED, _Required}, State) -> + State; +check_value(Value, {?DEPENDENCIES, Dependencies}, State) -> + case jesse_lib:is_json_object(Value) of + true -> check_dependencies(Value, Dependencies, State); + false -> State + end; +check_value(Value, {?MINIMUM, Minimum}, State) -> + case is_number(Value) of + true -> + ExclusiveMinimum = get_value( ?EXCLUSIVEMINIMUM + , get_current_schema(State) + ), + check_minimum(Value, Minimum, ExclusiveMinimum, State); + false -> + State + end; +check_value(Value, {?MAXIMUM, Maximum}, State) -> + case is_number(Value) of + true -> + ExclusiveMaximum = get_value( ?EXCLUSIVEMAXIMUM + , get_current_schema(State) + ), + check_maximum(Value, Maximum, ExclusiveMaximum, State); + false -> + State + end; %% doesn't really do anything, since this attribute will be handled %% by the previous function clause if it's presented in the schema -check_value( Value - , [{?EXCLUSIVEMINIMUM, _ExclusiveMinimum} | Attrs] +check_value( _Value + , {?EXCLUSIVEMINIMUM, _ExclusiveMinimum} , State ) -> - check_value(Value, Attrs, State); + State; %% doesn't really do anything, since this attribute will be handled %% by the previous function clause if it's presented in the schema -check_value( Value - , [{?EXCLUSIVEMAXIMUM, _ExclusiveMaximum} | Attrs] +check_value( _Value + , {?EXCLUSIVEMAXIMUM, _ExclusiveMaximum} , State ) -> - check_value(Value, Attrs, State); -check_value(Value, [{?MINITEMS, MinItems} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_min_items(Value, MinItems, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MAXITEMS, MaxItems} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_max_items(Value, MaxItems, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?UNIQUEITEMS, Uniqueitems} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_unique_items(Value, Uniqueitems, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?PATTERN, Pattern} | Attrs], State) -> - NewState = case is_binary(Value) of - true -> check_pattern(Value, Pattern, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MINLENGTH, MinLength} | Attrs], State) -> - NewState = case is_binary(Value) of - true -> check_min_length(Value, MinLength, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MAXLENGTH, MaxLength} | Attrs], State) -> - NewState = case is_binary(Value) of - true -> check_max_length(Value, MaxLength, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?ENUM, Enum} | Attrs], State) -> - NewState = check_enum(Value, Enum, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?FORMAT, Format} | Attrs], State) -> - NewState = check_format(Value, Format, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?DIVISIBLEBY, DivisibleBy} | Attrs], State) -> - NewState = case is_number(Value) of - true -> check_divisible_by(Value, DivisibleBy, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?DISALLOW, Disallow} | Attrs], State) -> - NewState = check_disallow(Value, Disallow, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?EXTENDS, Extends} | Attrs], State) -> - NewState = check_extends(Value, Extends, State), - check_value(Value, Attrs, NewState); -check_value(Value, [], State) -> - maybe_external_check_value(Value, State); -check_value(Value, [_Attr | Attrs], State) -> - check_value(Value, Attrs, State). + State; +check_value(Value, {?MINITEMS, MinItems}, State) -> + case jesse_lib:is_array(Value) of + true -> check_min_items(Value, MinItems, State); + false -> State + end; +check_value(Value, {?MAXITEMS, MaxItems}, State) -> + case jesse_lib:is_array(Value) of + true -> check_max_items(Value, MaxItems, State); + false -> State + end; +check_value(Value, {?UNIQUEITEMS, Uniqueitems}, State) -> + case jesse_lib:is_array(Value) of + true -> check_unique_items(Value, Uniqueitems, State); + false -> State + end; +check_value(Value, {?PATTERN, Pattern}, State) -> + case is_binary(Value) of + true -> check_pattern(Value, Pattern, State); + false -> State + end; +check_value(Value, {?MINLENGTH, MinLength}, State) -> + case is_binary(Value) of + true -> check_min_length(Value, MinLength, State); + false -> State + end; +check_value(Value, {?MAXLENGTH, MaxLength}, State) -> + case is_binary(Value) of + true -> check_max_length(Value, MaxLength, State); + false -> State + end; +check_value(Value, {?ENUM, Enum}, State) -> + check_enum(Value, Enum, State); +check_value(Value, {?FORMAT, Format}, State) -> + check_format(Value, Format, State); +check_value(Value, {?DIVISIBLEBY, DivisibleBy}, State) -> + case is_number(Value) of + true -> check_divisible_by(Value, DivisibleBy, State); + false -> State + end; +check_value(Value, {?DISALLOW, Disallow}, State) -> + check_disallow(Value, Disallow, State); +check_value(Value, {?EXTENDS, Extends}, State) -> + check_extends(Value, Extends, State); +check_value(Value, {?REF, RefSchemaURI}, State) -> + validate_ref(Value, RefSchemaURI, State); +check_value(Value, _Attr, State) -> + maybe_external_check_value(Value, State). %%% Internal functions %% @doc Adds Property to the current path and checks the value @@ -785,7 +768,7 @@ check_unique_items(Value, true, State) -> %% specification from ECMA 262/Perl 5 %% @private check_pattern(Value, Pattern, State) -> - case re:run(Value, Pattern, [{capture, none}, unicode]) of + case re:run(Value, Pattern, [{capture, none}, unicode, ucp]) of match -> State; nomatch -> handle_data_invalid(?no_match, Value, State) @@ -891,7 +874,11 @@ check_disallow(Value, Disallow, State) -> check_extends(Value, Extends, State) -> case jesse_lib:is_json_object(Extends) of true -> - check_value(Value, Extends, set_current_schema(State, Extends)); + NewState = set_current_schema(State, Extends), + jesse_schema_validator:validate_with_state( Extends + , Value + , NewState + ); false -> case is_list(Extends) of true -> check_extends_array(Value, Extends, State); diff --git a/src/jesse_validator_draft4.erl b/src/jesse_validator_draft4.erl index 072e55df..59c78019 100644 --- a/src/jesse_validator_draft4.erl +++ b/src/jesse_validator_draft4.erl @@ -22,9 +22,11 @@ %%%============================================================================= -module(jesse_validator_draft4). +-behaviour(jesse_schema_validator). %% API -export([ check_value/3 + , init_state/1 ]). %% Includes @@ -74,192 +76,168 @@ | {data_error(), [jesse_error:error_reason()]}. %%% API -%% @doc Goes through attributes of the given schema `JsonSchema' and -%% validates the value `Value' against them. --spec check_value( Value :: jesse:json_term() - , JsonSchema :: jesse:schema() - , State :: jesse_state:state() - ) -> jesse_state:state() | no_return(). -check_value(Value, [{?REF, RefSchemaURI} | Attrs], State) -> - case Attrs of - [] -> - validate_ref(Value, RefSchemaURI, State); - _ -> - handle_schema_invalid(?only_ref_allowed, State) +%% @doc Behaviour callback. Custom state is not used by this validator. +-spec init_state(_) -> undefined. +init_state(_) -> + undefined. + +%% @doc Validates the value `Value' against the attributes +%% of the given schema `JsonSchema'. +-spec check_value(Value, Attr, State) -> + State | no_return() + when + Value :: any(), + Attr :: {binary(), jesse:json_term()}, + State :: jesse_state:state(). +check_value(Value, {?TYPE, Type}, State) -> + check_type(Value, Type, State); +check_value(Value, {?PROPERTIES, Properties}, State) -> + case jesse_lib:is_json_object(Value) of + true -> check_properties( Value + , unwrap(Properties) + , State + ); + false -> State end; -check_value(Value, [{?TYPE, Type} | Attrs], State) -> - NewState = check_type(Value, Type, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?PROPERTIES, Properties} | Attrs], State) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_properties( Value - , unwrap(Properties) - , State - ); - false -> State - end, - check_value(Value, Attrs, NewState); check_value( Value - , [{?PATTERNPROPERTIES, PatternProperties} | Attrs] + , {?PATTERNPROPERTIES, PatternProperties} , State ) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_pattern_properties( Value - , PatternProperties - , State - ); - false -> State - end, - check_value(Value, Attrs, NewState); + case jesse_lib:is_json_object(Value) of + true -> check_pattern_properties( Value + , PatternProperties + , State + ); + false -> State + end; check_value( Value - , [{?ADDITIONALPROPERTIES, AdditionalProperties} | Attrs] + , {?ADDITIONALPROPERTIES, AdditionalProperties} , State ) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_additional_properties( Value - , AdditionalProperties - , State - ); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?ITEMS, Items} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_items(Value, Items, State); - false -> State - end, - check_value(Value, Attrs, NewState); + case jesse_lib:is_json_object(Value) of + true -> check_additional_properties( Value + , AdditionalProperties + , State + ); + false -> State + end; +check_value(Value, {?ITEMS, Items}, State) -> + case jesse_lib:is_array(Value) of + true -> check_items(Value, Items, State); + false -> State + end; %% doesn't really do anything, since this attribute will be handled %% by the previous function clause if it's presented in the schema -check_value( Value - , [{?ADDITIONALITEMS, _AdditionalItems} | Attrs] +check_value( _Value + , {?ADDITIONALITEMS, _AdditionalItems} , State ) -> - check_value(Value, Attrs, State); -check_value(Value, [{?REQUIRED, Required} | Attrs], State) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_required(Value, Required, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?DEPENDENCIES, Dependencies} | Attrs], State) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_dependencies(Value, Dependencies, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MINIMUM, Minimum} | Attrs], State) -> - NewState = case is_number(Value) of - true -> - ExclusiveMinimum = get_value( ?EXCLUSIVEMINIMUM - , get_current_schema(State) - ), - check_minimum(Value, Minimum, ExclusiveMinimum, State); - false -> - State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MAXIMUM, Maximum} | Attrs], State) -> - NewState = case is_number(Value) of - true -> - ExclusiveMaximum = get_value( ?EXCLUSIVEMAXIMUM - , get_current_schema(State) - ), - check_maximum(Value, Maximum, ExclusiveMaximum, State); - false -> - State - end, - check_value(Value, Attrs, NewState); + State; +check_value(Value, {?REQUIRED, Required}, State) -> + case jesse_lib:is_json_object(Value) of + true -> check_required(Value, Required, State); + false -> State + end; +check_value(Value, {?DEPENDENCIES, Dependencies}, State) -> + case jesse_lib:is_json_object(Value) of + true -> check_dependencies(Value, Dependencies, State); + false -> State + end; +check_value(Value, {?MINIMUM, Minimum}, State) -> + case is_number(Value) of + true -> + ExclusiveMinimum = get_value( ?EXCLUSIVEMINIMUM + , get_current_schema(State) + ), + check_minimum(Value, Minimum, ExclusiveMinimum, State); + false -> + State + end; +check_value(Value, {?MAXIMUM, Maximum}, State) -> + case is_number(Value) of + true -> + ExclusiveMaximum = get_value( ?EXCLUSIVEMAXIMUM + , get_current_schema(State) + ), + check_maximum(Value, Maximum, ExclusiveMaximum, State); + false -> + State + end; %% doesn't really do anything, since this attribute will be handled %% by the previous function clause if it's presented in the schema -check_value( Value - , [{?EXCLUSIVEMINIMUM, _ExclusiveMinimum} | Attrs] +check_value( _Value + , {?EXCLUSIVEMINIMUM, _ExclusiveMinimum} , State ) -> - check_value(Value, Attrs, State); + State; %% doesn't really do anything, since this attribute will be handled %% by the previous function clause if it's presented in the schema -check_value( Value - , [{?EXCLUSIVEMAXIMUM, _ExclusiveMaximum} | Attrs] +check_value( _Value + , {?EXCLUSIVEMAXIMUM, _ExclusiveMaximum} , State ) -> - check_value(Value, Attrs, State); -check_value(Value, [{?MINITEMS, MinItems} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_min_items(Value, MinItems, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MAXITEMS, MaxItems} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_max_items(Value, MaxItems, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?UNIQUEITEMS, Uniqueitems} | Attrs], State) -> - NewState = case jesse_lib:is_array(Value) of - true -> check_unique_items(Value, Uniqueitems, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?PATTERN, Pattern} | Attrs], State) -> - NewState = case is_binary(Value) of - true -> check_pattern(Value, Pattern, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MINLENGTH, MinLength} | Attrs], State) -> - NewState = case is_binary(Value) of - true -> check_min_length(Value, MinLength, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MAXLENGTH, MaxLength} | Attrs], State) -> - NewState = case is_binary(Value) of - true -> check_max_length(Value, MaxLength, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?ENUM, Enum} | Attrs], State) -> - NewState = check_enum(Value, Enum, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?FORMAT, Format} | Attrs], State) -> - NewState = check_format(Value, Format, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?MULTIPLEOF, Multiple} | Attrs], State) -> - NewState = case is_number(Value) of - true -> check_multiple_of(Value, Multiple, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MAXPROPERTIES, MaxProperties} | Attrs], State) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_max_properties(Value, MaxProperties, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?MINPROPERTIES, MinProperties} | Attrs], State) -> - NewState = case jesse_lib:is_json_object(Value) of - true -> check_min_properties(Value, MinProperties, State); - false -> State - end, - check_value(Value, Attrs, NewState); -check_value(Value, [{?ALLOF, Schemas} | Attrs], State) -> - NewState = check_all_of(Value, Schemas, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?ANYOF, Schemas} | Attrs], State) -> - NewState = check_any_of(Value, Schemas, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?ONEOF, Schemas} | Attrs], State) -> - NewState = check_one_of(Value, Schemas, State), - check_value(Value, Attrs, NewState); -check_value(Value, [{?NOT, Schema} | Attrs], State) -> - NewState = check_not(Value, Schema, State), - check_value(Value, Attrs, NewState); -check_value(Value, [], State) -> - maybe_external_check_value(Value, State); -check_value(Value, [_Attr | Attrs], State) -> - check_value(Value, Attrs, State). + State; +check_value(Value, {?MINITEMS, MinItems}, State) -> + case jesse_lib:is_array(Value) of + true -> check_min_items(Value, MinItems, State); + false -> State + end; +check_value(Value, {?MAXITEMS, MaxItems}, State) -> + case jesse_lib:is_array(Value) of + true -> check_max_items(Value, MaxItems, State); + false -> State + end; +check_value(Value, {?UNIQUEITEMS, Uniqueitems}, State) -> + case jesse_lib:is_array(Value) of + true -> check_unique_items(Value, Uniqueitems, State); + false -> State + end; +check_value(Value, {?PATTERN, Pattern}, State) -> + case is_binary(Value) of + true -> check_pattern(Value, Pattern, State); + false -> State + end; +check_value(Value, {?MINLENGTH, MinLength}, State) -> + case is_binary(Value) of + true -> check_min_length(Value, MinLength, State); + false -> State + end; +check_value(Value, {?MAXLENGTH, MaxLength}, State) -> + case is_binary(Value) of + true -> check_max_length(Value, MaxLength, State); + false -> State + end; +check_value(Value, {?ENUM, Enum}, State) -> + check_enum(Value, Enum, State); +check_value(Value, {?FORMAT, Format}, State) -> + check_format(Value, Format, State); +check_value(Value, {?MULTIPLEOF, Multiple}, State) -> + case is_number(Value) of + true -> check_multiple_of(Value, Multiple, State); + false -> State + end; +check_value(Value, {?MAXPROPERTIES, MaxProperties}, State) -> + case jesse_lib:is_json_object(Value) of + true -> check_max_properties(Value, MaxProperties, State); + false -> State + end; +check_value(Value, {?MINPROPERTIES, MinProperties}, State) -> + case jesse_lib:is_json_object(Value) of + true -> check_min_properties(Value, MinProperties, State); + false -> State + end; +check_value(Value, {?ALLOF, Schemas}, State) -> + check_all_of(Value, Schemas, State); +check_value(Value, {?ANYOF, Schemas}, State) -> + check_any_of(Value, Schemas, State); +check_value(Value, {?ONEOF, Schemas}, State) -> + check_one_of(Value, Schemas, State); +check_value(Value, {?NOT, Schema}, State) -> + check_not(Value, Schema, State); +check_value(Value, {?REF, RefSchemaURI}, State) -> + validate_ref(Value, RefSchemaURI, State); +check_value(Value, _Attr, State) -> + maybe_external_check_value(Value, State). %%% Internal functions %% @doc Adds Property to the current path and checks the value @@ -868,7 +846,7 @@ check_unique_items(Value, true, State) -> %% anchored. %% @private check_pattern(Value, Pattern, State) -> - case re:run(Value, Pattern, [{capture, none}, unicode]) of + case re:run(Value, Pattern, [{capture, none}, unicode, ucp]) of match -> State; nomatch -> handle_data_invalid(?no_match, Value, State) @@ -1381,10 +1359,12 @@ remove_last_from_path(State) -> %% @private valid_datetime(DateTimeBin) -> - case rfc3339:parse(DateTimeBin) of - {ok, _} -> - true; - _ -> + DateTimeStr = erlang:binary_to_list(DateTimeBin), + try calendar:rfc3339_to_system_time(DateTimeStr) of + Seconds when is_integer(Seconds) -> + true + catch + error:_ -> false end. diff --git a/test/jesse_tests_generic_SUITE.erl b/test/jesse_tests_generic_SUITE.erl new file mode 100644 index 00000000..ae8e390b --- /dev/null +++ b/test/jesse_tests_generic_SUITE.erl @@ -0,0 +1,83 @@ +%%%============================================================================= +%% Copyright 2012- Klarna AB +%% Copyright 2015- AUTHORS +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% +%% @doc jesse test suite which covers generic jessy functionality +%% (not schema specific). +%% @end +%%%============================================================================= + +-module(jesse_tests_generic_SUITE). + +-behaviour(jesse_schema_validator). + +-compile([ export_all + , nowarn_export_all + ]). + +-define(EXCLUDED_FUNS, [ module_info + , all + , init_per_suite + , end_per_suite + , init_state + , check_value + , update_custom_state + ]). + +-include_lib("common_test/include/ct.hrl"). + +-import(jesse_tests_util, [ get_tests/3 + , do_test/2 + ]). + +all() -> + Exports = ?MODULE:module_info(exports), + [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. + +init_per_suite(Config) -> + {ok, _} = application:ensure_all_started(jesse), + get_tests( "extra" + , <<"http://json-schema.org/draft-04/schema#">> + , Config) + ++ Config. + +end_per_suite(_Config) -> + ok. + +init_state(_) -> + 0. + +check_value(Value, {<<"customDef">>, Property}, State0) -> + State = update_custom_state(State0), + case jesse_json_path:path(Property, Value) of + true -> State; + false -> jesse_error:handle_data_invalid( 'custom_validator_reject' + , Value + , State); + %% Skip if custom property is missing + [] -> State + end; +check_value(Value, Attr, State) -> + jesse_validator_draft4:check_value(Value, Attr, State). + +update_custom_state(State) -> + ValidatorState = 0 = jesse_state:get_validator_state(State), + jesse_state:set_validator_state(State, ValidatorState + 1). + +%%% Testcases + +additionalItems(Config) -> + do_test("customValidator", Config). diff --git a/test/jesse_tests_generic_SUITE_data/extra/customValidator.json b/test/jesse_tests_generic_SUITE_data/extra/customValidator.json new file mode 100644 index 00000000..813216a8 --- /dev/null +++ b/test/jesse_tests_generic_SUITE_data/extra/customValidator.json @@ -0,0 +1,36 @@ +[ + { + "description": "use custom validator", + "schema": { + "customDef": "testSuccess", + "properties": { + "testSuccess": { + "type": "boolean" + } + }, + "required": [ + "testSuccess" + ] + }, + "tests": [ + { + "description": "custom validation success", + "data": {"testSuccess": true}, + "valid": true + }, + { + "description": "custom validation failure", + "data": {"testSuccess": false}, + "valid": false + }, + { + "description": "base validation failure", + "data": {"wrongProperty": "whatever"}, + "valid": false + } + ], + "options": { + "validator": "jesse_tests_generic_SUITE" + } + } +]