diff --git a/src/jesse_error.erl b/src/jesse_error.erl index 4484e4d..adb5061 100644 --- a/src/jesse_error.erl +++ b/src/jesse_error.erl @@ -60,7 +60,7 @@ %% throws an exeption, otherwise adds a new element to the list and returs it. -spec default_error_handler( Error :: error_reason() , ErrorList :: [error_reason()] - , AllowedErrors :: non_neg_integer() + , AllowedErrors :: non_neg_integer() | 'infinity' ) -> [error_reason()] | no_return(). default_error_handler(Error, ErrorList, AllowedErrors) -> case AllowedErrors > length(ErrorList) orelse AllowedErrors =:= 'infinity' of diff --git a/src/jesse_lib.erl b/src/jesse_lib.erl index d43ceec..e556372 100644 --- a/src/jesse_lib.erl +++ b/src/jesse_lib.erl @@ -27,6 +27,7 @@ -export([ empty_if_not_found/1 , is_array/1 , is_json_object/1 + , is_json_object_empty/1 , is_null/1 ]). @@ -86,3 +87,22 @@ is_null(null) -> is_null(_Value) -> false. +%% @doc check if json object is_empty. +-spec is_json_object_empty(Value :: any()) -> boolean(). +is_json_object_empty({struct, Value}) + when is_list(Value) andalso Value =:= [] -> + true; +is_json_object_empty({Value}) + when is_list(Value) + andalso Value =:= [] -> + true; +%% handle `jsx' empty objects +is_json_object_empty([{}]) -> + true; +?IF_MAPS( +is_json_object_empty(Map) + when erlang:is_map(Map) -> + maps:size(Map) =:= 0; +) +is_json_object_empty(_) -> + false. diff --git a/src/jesse_schema_validator.hrl b/src/jesse_schema_validator.hrl index 1591994..106e72a 100644 --- a/src/jesse_schema_validator.hrl +++ b/src/jesse_schema_validator.hrl @@ -55,6 +55,7 @@ -define(MULTIPLEOF, <<"multipleOf">>). -define(MAXPROPERTIES, <<"maxProperties">>). -define(MINPROPERTIES, <<"minProperties">>). +-define(DEFAULT, <<"default">>). %% Constant definitions for Json types -define(ANY, <<"any">>). diff --git a/src/jesse_state.erl b/src/jesse_state.erl index 4549401..57e0812 100644 --- a/src/jesse_state.erl +++ b/src/jesse_state.erl @@ -44,9 +44,16 @@ , undo_resolve_ref/2 , canonical_path/2 , combine_id/2 + , validator_options/1 + , validator_option/2, validator_option/3 ]). +-type option() :: {Key :: atom(), Data :: any()}. +-type options() :: [option()]. + -export_type([ state/0 + , option/0 + , options/0 ]). %% Includes @@ -75,6 +82,7 @@ , extra_validator :: extra_validator() , setter_fun :: setter_fun() , id :: http_uri:uri() | 'undefined' + , validator_options :: options() } ). @@ -98,7 +106,7 @@ add_to_path(State, Property) -> State#state{current_path = [Property | CurrentPath]}. %% @doc Getter for `allowed_errors'. --spec get_allowed_errors(State :: state()) -> non_neg_integer(). +-spec get_allowed_errors(State :: state()) -> non_neg_integer() | 'infinity'. get_allowed_errors(#state{allowed_errors = AllowedErrors}) -> AllowedErrors. @@ -140,7 +148,7 @@ get_error_list(#state{error_list = ErrorList}) -> %% @doc Returns newly created state. -spec new( JsonSchema :: jesse:json_term() - , Options :: [{Key :: atom(), Data :: any()}] + , Options :: options() ) -> state(). new(JsonSchema, Options) -> ErrorHandler = proplists:get_value( error_handler @@ -172,6 +180,10 @@ new(JsonSchema, Options) -> Value = proplists:get_value( with_value , Options ), + ValidatorOptions = proplists:get_value( validator_options + , Options + , [] + ), NewState = #state{ root_schema = JsonSchema , current_path = [] , allowed_errors = AllowedErrors @@ -182,6 +194,7 @@ new(JsonSchema, Options) -> , extra_validator = ExtraValidator , setter_fun = SetterFun , current_value = Value + , validator_options = ValidatorOptions }, set_current_schema(NewState, JsonSchema). @@ -192,7 +205,7 @@ remove_last_from_path(State = #state{current_path = [_Property | Path]}) -> %% @doc Getter for `allowed_errors'. -spec set_allowed_errors( State :: state() - , AllowedErrors :: non_neg_integer() + , AllowedErrors :: non_neg_integer() | 'infinity' ) -> state(). set_allowed_errors(#state{} = State, AllowedErrors) -> State#state{allowed_errors = AllowedErrors}. @@ -231,14 +244,22 @@ resolve_ref(State, Reference) -> Path = jesse_json_path:parse(Pointer), case load_local_schema(State#state.root_schema, Path) of ?not_found -> - jesse_error:handle_schema_invalid({?schema_not_found, CanonicalReference}, State); + jesse_error:handle_schema_invalid( { ?schema_not_found + , CanonicalReference + } + , State + ); Schema -> set_current_schema(State, Schema) end; false -> case load_schema(State, BaseURI) of ?not_found -> - jesse_error:handle_schema_invalid({?schema_not_found, CanonicalReference}, State); + jesse_error:handle_schema_invalid( { ?schema_not_found + , CanonicalReference + } + , State + ); RemoteSchema -> SchemaVer = jesse_json_path:value(?SCHEMA, RemoteSchema, ?default_schema_ver), @@ -249,7 +270,11 @@ resolve_ref(State, Reference) -> Path = jesse_json_path:parse(Pointer), case load_local_schema(RemoteSchema, Path) of ?not_found -> - jesse_error:handle_schema_invalid({?schema_not_found, CanonicalReference}, State); + jesse_error:handle_schema_invalid( { ?schema_not_found + , CanonicalReference + } + , State + ); Schema -> set_current_schema(NewState, Schema) end @@ -418,12 +443,28 @@ load_schema(#state{schema_loader_fun = LoaderFun}, SchemaURI) -> get_current_value(#state{current_value = Value}) -> Value. -spec set_value(State :: state(), jesse:path(), jesse:json_term()) -> state(). -set_value(#state{setter_fun=undefined}=State, _Path, _Value) -> State; -set_value(#state{current_value=undefined}=State, _Path, _Value) -> State; -set_value(#state{setter_fun=Setter - ,current_value=Value - }=State, Path, NewValue) -> +set_value(#state{setter_fun = undefined} = State, _Path, _Value) -> State; +set_value(#state{current_value = undefined} = State, _Path, _Value) -> State; +set_value( #state{setter_fun = Setter + , current_value = Value + } = State + , Path + , NewValue + ) -> State#state{current_value = Setter(Path, NewValue, Value)}. get_extra_validator(#state{extra_validator = Fun}) -> Fun. + +-spec validator_options(State :: state()) -> options(). +validator_options(#state{validator_options = Options}) -> + Options. + +-spec validator_option(Option :: atom(), State :: state()) -> any(). +validator_option(Option, #state{validator_options = Options}) -> + proplists:get_value(Option, Options). + +-spec validator_option(Option :: atom(), State :: state(), Default :: any()) -> + any(). +validator_option(Option, #state{validator_options = Options}, Default) -> + proplists:get_value(Option, Options, Default). diff --git a/src/jesse_validator_draft3.erl b/src/jesse_validator_draft3.erl index 03d700e..9671771 100644 --- a/src/jesse_validator_draft3.erl +++ b/src/jesse_validator_draft3.erl @@ -31,6 +31,7 @@ -include("jesse_schema_validator.hrl"). -type schema_error() :: ?wrong_type_dependency + | ?schema_invalid | ?wrong_type_items. -type schema_error_type() :: schema_error() @@ -348,20 +349,19 @@ check_properties(Value, Properties, State) -> = lists:foldl( fun({PropertyName, PropertySchema}, CurrentState) -> case get_value(PropertyName, Value) of ?not_found -> -%% @doc 5.7. required -%% -%% This attribute indicates if the instance must have a value, and not -%% be undefined. This is false by default, making the instance -%% optional. -%% @end - case get_value(?REQUIRED, PropertySchema) of - true -> - handle_data_invalid( {?missing_required_property - , PropertyName} - , Value - , CurrentState); - _ -> - CurrentState + case get_value(?DEFAULT, PropertySchema) of + ?not_found -> + check_required( PropertySchema + , PropertyName + , Value + , CurrentState + ); + Default -> + check_default( PropertyName + , PropertySchema + , Default + , CurrentState + ) end; Property -> NewState = set_current_schema( CurrentState @@ -583,6 +583,24 @@ check_items_fun(Tuples, State) -> ), set_current_schema(TmpState, get_current_schema(State)). + +%% @doc 5.7. required +%% +%% This attribute indicates if the instance must have a value, and not +%% be undefined. This is false by default, making the instance +%% optional. +%% @private +check_required(PropertySchema, PropertyName, Value, CurrentState) -> + case get_value(?REQUIRED, PropertySchema) of + true -> + handle_data_invalid( {?missing_required_property + , PropertyName} + , Value + , CurrentState); + _ -> + CurrentState + end. + %% @doc 5.8. dependencies %% %% This attribute is an object that defines the requirements of a @@ -904,7 +922,8 @@ validate_ref(Value, Reference, State) -> {error, NewState} -> undo_resolve_ref(NewState, State); {ok, NewState, Schema} -> - ResultState = jesse_schema_validator:validate_with_state(Schema, Value, NewState), + ResultState = + jesse_schema_validator:validate_with_state(Schema, Value, NewState), undo_resolve_ref(ResultState, State) end. @@ -992,7 +1011,11 @@ compare_properties(Value1, Value2) -> %% Wrappers %% @private get_value(Key, Schema) -> - jesse_json_path:value(Key, Schema, ?not_found). + get_value(Key, Schema, ?not_found). + +%% @private +get_value(Key, Schema, Default) -> + jesse_json_path:value(Key, Schema, Default). %% @private unwrap(Value) -> @@ -1039,3 +1062,80 @@ check_external_validation(Value, State) -> undefined -> State; Fun -> Fun(Value, State) end. + +%% @private +validator_option(Option, State, Default) -> + jesse_state:validator_option(Option, State, Default). + +%% @private +set_value(PropertyName, Value, State) -> + Path = lists:reverse([PropertyName] ++ jesse_state:get_current_path(State)), + jesse_state:set_value(State, Path, Value). + +%% @private +check_default_for_type(Default, State) -> + validator_option('use_defaults', State, false) + andalso (not jesse_lib:is_json_object(Default) + orelse validator_option( 'apply_defaults_to_empty_objects' + , State + , false + ) + orelse not jesse_lib:is_json_object_empty(Default)). + +%% @private +check_default(PropertyName, PropertySchema, Default, State) -> + Type = get_value(?TYPE, PropertySchema, ?not_found), + case is_valid_default(Type, Default, State) of + true -> + set_default(PropertyName, PropertySchema, Default, State); + false -> + State + end. + +%% @private +is_valid_default(?not_found, _Default, _State) -> + false; +is_valid_default(Type, Default, State) + when is_binary(Type) -> + check_default_for_type(Default, State) + andalso is_type_valid(Default, Type, State); +is_valid_default(Types, Default, State) + when is_list(Types) -> + check_default_for_type(Default, State) + andalso lists:any( fun(Type) -> + is_type_valid(Default, Type, State) + end + , Types + ); +is_valid_default(_, _Default, _State) -> false. + +%% @private +set_default(PropertyName, PropertySchema, Default, State) -> + State1 = set_value(PropertyName, Default, State), + State2 = add_to_path(State1, PropertyName), + case validate_schema(Default, PropertySchema, State2) of + {true, State4} -> + jesse_state:remove_last_from_path(State4); + _ -> + State + end. + +%% @doc Validate a value against a schema in a given state. +%% Used by all combinators to run validation on a schema. +%% @private +validate_schema(Value, Schema, State0) -> + try + case jesse_lib:is_json_object(Schema) of + true -> + State1 = set_current_schema(State0, Schema), + State2 = jesse_schema_validator:validate_with_state( Schema + , Value + , State1 + ), + {true, State2}; + false -> + handle_schema_invalid(?schema_invalid, State0) + end + catch + throw:Errors -> {false, Errors} + end. diff --git a/src/jesse_validator_draft4.erl b/src/jesse_validator_draft4.erl index 119e51d..78b0582 100644 --- a/src/jesse_validator_draft4.erl +++ b/src/jesse_validator_draft4.erl @@ -377,17 +377,26 @@ check_properties(Value, Properties, State) -> TmpState = lists:foldl( fun({PropertyName, PropertySchema}, CurrentState) -> case get_value(PropertyName, Value) of - ?not_found -> - CurrentState; - Property -> - NewState = set_current_schema( CurrentState - , PropertySchema - ), - check_value( PropertyName - , Property - , PropertySchema - , NewState - ) + ?not_found -> + case get_value(?DEFAULT, PropertySchema) of + ?not_found -> + CurrentState; + Default -> + check_default( PropertyName + , PropertySchema + , Default + , CurrentState + ) + end; + Property -> + NewState = set_current_schema( CurrentState + , PropertySchema + ), + check_value( PropertyName + , Property + , PropertySchema + , NewState + ) end end , State @@ -1226,7 +1235,8 @@ validate_ref(Value, Reference, State) -> {error, NewState} -> undo_resolve_ref(NewState, State); {ok, NewState, Schema} -> - ResultState = jesse_schema_validator:validate_with_state(Schema, Value, NewState), + ResultState = + jesse_schema_validator:validate_with_state(Schema, Value, NewState), undo_resolve_ref(ResultState, State) end. @@ -1314,7 +1324,11 @@ compare_properties(Value1, Value2) -> %% Wrappers %% @private get_value(Key, Schema) -> - jesse_json_path:value(Key, Schema, ?not_found). + get_value(Key, Schema, ?not_found). + +%% @private +get_value(Key, Schema, Default) -> + jesse_json_path:value(Key, Schema, Default). %% @private unwrap(Value) -> @@ -1355,6 +1369,10 @@ add_to_path(State, Property) -> remove_last_from_path(State) -> jesse_state:remove_last_from_path(State). +%% @private +validator_option(Option, State, Default) -> + jesse_state:validator_option(Option, State, Default). + %% @private valid_datetime(DateTimeBin) -> case rfc3339:parse(DateTimeBin) of @@ -1369,3 +1387,56 @@ check_external_validation(Value, State) -> undefined -> State; Fun -> Fun(Value, State) end. + +%% @private +set_value(PropertyName, Value, State) -> + Path = lists:reverse([PropertyName] ++ jesse_state:get_current_path(State)), + jesse_state:set_value(State, Path, Value). + +%% @private +check_default_for_type(Default, State) -> + validator_option('use_defaults', State, false) + andalso (not jesse_lib:is_json_object(Default) + orelse validator_option( 'apply_defaults_to_empty_objects' + , State + , false + ) + orelse not jesse_lib:is_json_object_empty(Default)). + +%% @private +check_default(PropertyName, PropertySchema, Default, State) -> + Type = get_value(?TYPE, PropertySchema, ?not_found), + case is_valid_default(Type, Default, State) of + true -> + set_default(PropertyName, PropertySchema, Default, State); + false -> + State + end. + +%% @private +is_valid_default(?not_found, _Default, _State) -> + false; +is_valid_default(Type, Default, State) + when is_binary(Type) -> + check_default_for_type(Default, State) + andalso is_type_valid(Default, Type); +is_valid_default(Types, Default, State) + when is_list(Types) -> + check_default_for_type(Default, State) + andalso lists:any( fun(Type) -> + is_type_valid(Default, Type) + end + , Types + ); +is_valid_default(_, _Default, _State) -> false. + +%% @private +set_default(PropertyName, PropertySchema, Default, State) -> + State1 = set_value(PropertyName, Default, State), + State2 = add_to_path(State1, PropertyName), + case validate_schema(Default, PropertySchema, State2) of + {true, State4} -> + jesse_state:remove_last_from_path(State4); + _ -> + State + end. diff --git a/test/jesse_schema_validator_tests.erl b/test/jesse_schema_validator_tests.erl index 44c67cb..3a5ce9c 100644 --- a/test/jesse_schema_validator_tests.erl +++ b/test/jesse_schema_validator_tests.erl @@ -21,6 +21,76 @@ -module(jesse_schema_validator_tests). -include_lib("eunit/include/eunit.hrl"). +setter_test() -> + Schema = {[ + {<<"type">>, <<"object">>}, + {<<"properties">>, {[ + {<<"bar">>, {[ + {<<"type">>, <<"string">>}, + {<<"minLength">>, 4}, + {<<"default">>, <<"awesome">>} + ]}} + ]}} + ]}, + + Default = {[{<<"bar">>, <<"awesome">>}]}, + Value = {[]}, + Fun = fun([K], V, {L1}) -> + {[{K, V} | proplists:delete(K, L1)]} + end, + Options = [ {setter_fun, Fun} + , {validator_options, [use_defaults]} + ], + + [ ?assertEqual( {ok, Value} + , jesse_schema_validator:validate(Schema, Value, []) + ) + , ?assertEqual( {ok, Default} + , jesse_schema_validator:validate(Schema, Value, Options) + ) + ]. + +invalid_default_test() -> + BadSchema = {[ + {<<"type">>, <<"object">>}, + {<<"properties">>, {[ + {<<"bar">>, {[ + {<<"type">>, <<"string">>}, + {<<"minLength">>, 4}, + {<<"default">>, <<"bad">>} + ]}} + ]}} + ]}, + + GoodSchema = {[ + {<<"type">>, <<"object">>}, + {<<"properties">>, {[ + {<<"bar">>, {[ + {<<"type">>, <<"string">>}, + {<<"minLength">>, 4}, + {<<"default">>, <<"awesome">>} + ]}} + ]}} + ]}, + + WithDefault = {[{<<"bar">>, <<"good">>}]}, + WithoutDefault = {[]}, + + ?assertEqual( + {ok, WithoutDefault}, + jesse_schema_validator:validate(BadSchema, WithoutDefault, []) + ), + + ?assertEqual( + {ok, WithDefault}, + jesse_schema_validator:validate(BadSchema, WithDefault, []) + ), + + ?assertEqual( + {ok, WithoutDefault}, + jesse_schema_validator:validate(GoodSchema, WithoutDefault, []) + ). + data_invalid_test() -> IntegerSchema = {[{<<"type">>, <<"integer">>}]},