From 65c8605dcaac81d36ee0ff6b0614a7f3f3aba4de Mon Sep 17 00:00:00 2001 From: Anton Belyaev Date: Fri, 17 Mar 2017 13:30:30 +0300 Subject: [PATCH] add suppport for configurable and extendable validators --- Makefile | 2 +- README.md | 5 + src/jesse_schema_validator.erl | 51 ++- src/jesse_state.erl | 51 ++- src/jesse_validator.erl | 61 ++++ src/jesse_validator_draft3.erl | 277 ++++++++--------- src/jesse_validator_draft4.erl | 310 +++++++++---------- test/Generic-Test-Suite/customValidator.json | 36 +++ test/jesse_tests_generic_SUITE.erl | 82 +++++ 9 files changed, 548 insertions(+), 327 deletions(-) create mode 100644 src/jesse_validator.erl create mode 100644 test/Generic-Test-Suite/customValidator.json create mode 100644 test/jesse_tests_generic_SUITE.erl diff --git a/Makefile b/Makefile index cd63809..e5e825f 100644 --- a/Makefile +++ b/Makefile @@ -109,7 +109,7 @@ eunit: .PHONY: ct ct: - $(REBAR) ct skip_deps=true suites="jesse_tests_draft3,jesse_tests_draft4" + $(REBAR) ct skip_deps=true suites="jesse_tests_draft3,jesse_tests_draft4,jesse_tests_generic" .PHONY: xref xref: diff --git a/README.md b/README.md index 7520780..3be0a84 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,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/src/jesse_schema_validator.erl b/src/jesse_schema_validator.erl index 69611ae..f251565 100644 --- a/src/jesse_schema_validator.erl +++ b/src/jesse_schema_validator.erl @@ -53,11 +53,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 +96,15 @@ 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 = jesse_validator:check_value( Validator + , Value + , Attr + , State0 + ), + run_validator(Validator, Value, Attrs, State). diff --git a/src/jesse_state.erl b/src/jesse_state.erl index 5ed9d89..409b2d4 100644 --- a/src/jesse_state.erl +++ b/src/jesse_state.erl @@ -30,6 +30,8 @@ , get_current_schema/1 , get_current_schema_id/1 , get_default_schema_ver/1 + , get_validator/1 + , get_validator_state/1 , get_error_handler/1 , get_error_list/1 , new/2 @@ -37,6 +39,7 @@ , 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 @@ -50,6 +53,8 @@ -include("jesse_schema_validator.hrl"). %% Internal datastructures +-type http_uri() :: string(). + -record( state , { allowed_errors :: jesse:allowed_errors() , current_path :: current_path() @@ -61,6 +66,8 @@ , id :: jesse:schema_id() , root_schema :: jesse:schema() , schema_loader_fun :: jesse:schema_loader_fun() + , validator :: module() | 'undefined' + , validator_state :: jesse_validator:state() } ). @@ -107,6 +114,16 @@ get_current_schema_id(#state{ current_schema = CurrentSchema get_default_schema_ver(#state{default_schema_ver = SchemaVer}) -> SchemaVer. +%% @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()) -> jesse_validator:state(). +get_validator_state(#state{validator_state = ValidatorState}) -> + ValidatorState. + %% @doc Getter for `error_handler'. -spec get_error_handler(State :: state()) -> jesse:error_handler(). get_error_handler(#state{error_handler = ErrorHandler}) -> @@ -142,11 +159,24 @@ 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 , error_list = [] , error_handler = ErrorHandler + , validator = Validator + , validator_state = ValidatorState , default_schema_ver = DefaultSchemaVer , schema_loader_fun = LoaderFun }, @@ -179,6 +209,13 @@ 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 :: jesse_validator:state() + ) -> 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) -> @@ -234,6 +271,16 @@ undo_resolve_ref(RefState, OriginalState) -> , id = OriginalState#state.id }. +%% @doc Init custom validator state. +%% @private +-spec init_validator_state( Validator :: module() | undefined + , Opts :: jesse_validator:opts() + ) -> jesse_validator:state(). +init_validator_state(undefined, _) -> + undefined; +init_validator_state(Validator, Opts) -> + jesse_validator:init_state(Validator, Opts). + %% @doc Retrieve a specific part of a schema %% @private -spec load_local_schema( Schema :: ?not_found | jesse:schema() @@ -273,8 +320,8 @@ load_local_schema(Schema, [Key | Keys]) -> %% @doc Resolve a new id %% @private --spec combine_id(undefined | http_uri:uri(), - undefined | binary()) -> http_uri:uri(). +-spec combine_id(undefined | http_uri(), + undefined | binary()) -> http_uri(). combine_id(Id, undefined) -> Id; combine_id(Id, RefBin) -> diff --git a/src/jesse_validator.erl b/src/jesse_validator.erl new file mode 100644 index 0000000..20f74b9 --- /dev/null +++ b/src/jesse_validator.erl @@ -0,0 +1,61 @@ +%%%============================================================================= +%% Copyright 2014- 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 Json schema validation module. +%% +%% This module is the behaviour definition for jesse validator. +%% @end +%%%============================================================================= + +-module(jesse_validator). + +%% API +-export([ init_state/2 + , check_value/4 + ]). + +%% Behaviour definition +-callback init_state(Opts :: opts()) -> + state(). + +-callback check_value( Value :: any() + , Attr :: {binary(), jesse:json_term()} + , State :: jesse_state:state() + ) -> jesse_state:state() | no_return(). + + +-type opts() :: any(). + +-type state() :: any(). + +-export_type([ opts/0 + , state/0 + ]). + +%% API +-spec init_state( Validator :: module() + , Opts :: opts() + ) -> state(). +init_state(Validator, Opts) -> + Validator:init_state(Opts). + +-spec check_value( Validator :: module() + , Value :: any() + , Attr :: {binary(), jesse:json_term()} + , State :: jesse_state:state() + ) -> jesse_state:state() | no_return(). +check_value(Validator, Value, Attr, State) -> + Validator:check_value(Value, Attr, State). diff --git a/src/jesse_validator_draft3.erl b/src/jesse_validator_draft3.erl index 6aecb65..c380c55 100644 --- a/src/jesse_validator_draft3.erl +++ b/src/jesse_validator_draft3.erl @@ -22,9 +22,11 @@ %%%============================================================================= -module(jesse_validator_draft3). +-behaviour(jesse_validator). %% API --export([ check_value/3 +-export([ init_state/1 + , check_value/3 ]). %% Includes @@ -54,168 +56,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 :: any() - , JsonSchema :: jesse:json_term() - , State :: jesse_state:state() - ) -> jesse_state:state() | no_return(). -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); +%% @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 - , [{?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) -> State; -check_value(Value, [{?REF, RefSchemaURI} | Attrs], State) -> - NewState = validate_ref(Value, RefSchemaURI, State), - check_value(Value, Attrs, NewState); -check_value(Value, [_Attr | Attrs], State) -> - check_value(Value, Attrs, 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) -> + State. %%% Internal functions %% @doc Adds Property to the current path and checks the value @@ -880,7 +867,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 e8bbc12..ce25fc5 100644 --- a/src/jesse_validator_draft4.erl +++ b/src/jesse_validator_draft4.erl @@ -22,9 +22,11 @@ %%%============================================================================= -module(jesse_validator_draft4). +-behaviour(jesse_validator). %% API --export([ check_value/3 +-export([ init_state/1 + , check_value/3 ]). %% Includes @@ -71,188 +73,168 @@ | {data_error(), binary()}. %%% API -%% @doc Goes through attributes of the given schema `JsonSchema' and -%% validates the value `Value' against them. --spec check_value( Value :: any() - , JsonSchema :: jesse:json_term() - , State :: jesse_state:state() - ) -> jesse_state:state() | no_return(). -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); +%% @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 - , [{?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) -> State; -check_value(Value, [{?REF, RefSchemaURI} | Attrs], State) -> - NewState = validate_ref(Value, RefSchemaURI, State), - check_value(Value, Attrs, NewState); -check_value(Value, [_Attr | Attrs], State) -> - check_value(Value, Attrs, 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) -> + State. %%% Internal functions %% @doc Adds Property to the current path and checks the value diff --git a/test/Generic-Test-Suite/customValidator.json b/test/Generic-Test-Suite/customValidator.json new file mode 100644 index 0000000..813216a --- /dev/null +++ b/test/Generic-Test-Suite/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" + } + } +] diff --git a/test/jesse_tests_generic_SUITE.erl b/test/jesse_tests_generic_SUITE.erl new file mode 100644 index 0000000..c16afb9 --- /dev/null +++ b/test/jesse_tests_generic_SUITE.erl @@ -0,0 +1,82 @@ +%%%============================================================================= +%% 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_validator). + +-compile([ 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/2 + , do_test/2 + ]). + +all() -> + Exports = ?MODULE:module_info(exports), + [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. + +init_per_suite(Config) -> + inets:start(), + get_tests( "Generic-Test-Suite" + , <<"http://json-schema.org/draft-04/schema#">> + ) + ++ Config. + +end_per_suite(_Config) -> + inets:stop(). + +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).