From beda7b200c4cf17ae9ee3e20dc25ab3a07bc614e Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 21 Jan 2014 17:35:22 +0400 Subject: [PATCH] Implemented jesse $ref; not tested yet tests pass after I add 'jesse:' in front of all references Got rid of 'jesse:' prefix; State record replaces three different arguments Added jesse_uri file Referencing URIs in a proper way Added testing basic tests up references --- src/jesse_schema_validator.erl | 528 +++++++++++++++++---------- src/jesse_uri.erl | 256 +++++++++++++ test/jesse_ref_SUITE.erl | 114 ++++++ test/jesse_ref_SUITE_data/basic.json | 105 ++++++ test/jesse_tests_draft3_SUITE.erl | 12 +- 5 files changed, 810 insertions(+), 205 deletions(-) create mode 100644 src/jesse_uri.erl create mode 100644 test/jesse_ref_SUITE.erl create mode 100644 test/jesse_ref_SUITE_data/basic.json diff --git a/src/jesse_schema_validator.erl b/src/jesse_schema_validator.erl index 52cf05d..2397e84 100644 --- a/src/jesse_schema_validator.erl +++ b/src/jesse_schema_validator.erl @@ -104,6 +104,41 @@ -export_type([error/0, reason/0]). +-record(validator_state, + { + current_schema :: jesse:json_term(), + global_schema :: jesse:json_term(), + current_path :: list(), + base_uri :: no_base_uri | jesse_uri:uri() + } +). + +%% @private +start_state(Schema) -> + BaseURI = + try + URIString = get_schema_id(Schema), + case jesse_uri:parse(URIString) of + {ok, URI} -> URI; + {error, _} -> no_base_uri + end + catch + throw:_ -> no_base_uri + end, + #validator_state{ + current_schema = Schema, + global_schema = Schema, + current_path = jesse_json_path:new(), + base_uri = BaseURI + }. + +%% @private +push_path(State, Schema, Item) -> + State#validator_state{ + current_schema = Schema, + current_path = + jesse_json_path:push(Item, State#validator_state.current_path) + }. %%% API %% @doc Validates json `Data' against `Schema'. If the given json is valid, @@ -115,10 +150,10 @@ ) -> {ok, jesse:json_term()} | {error, Arg1 :: term()}. validate(JsonSchema, Value, AccTuple) -> - case check_value(Value, unwrap(JsonSchema), JsonSchema, - jesse_json_path:new(), {ok, AccTuple}) of - {ok, _} -> {ok, Value}; - {error, {_Fun, Errors}} -> {error, Errors} + case check_value(Value, unwrap(JsonSchema), + start_state(JsonSchema), {ok, AccTuple}) of + {ok, _} -> {ok, Value}; + {error, {_Fun, Errors}} -> {error, Errors} end. %% @doc Returns value of "id" field from json object `Schema', assuming that @@ -155,292 +190,263 @@ is_null(_) -> false. %% @doc Goes through attributes of the given schema `JsonSchema' and %% validates the value `Value' against them. %% @private -check_value(Value, [{?TYPE, Type} | Attrs], JsonSchema, Path, Accumulator) -> - case check_type(Value, Type) of - true -> check_value(Value, Attrs, JsonSchema, Path, Accumulator); +check_value(Value, [{?TYPE, Type} | Attrs], State, Accumulator) -> + case check_type(Value, Type, State) of + true -> + check_value(Value, Attrs, State, Accumulator); false -> %% In case of incorrect type, no other properties are checked against %% this value, since it may not be safe - Error = {'data_invalid', JsonSchema, 'wrong_type', Value}, - accumulate_error(Path, Error, Accumulator) + accumulate_data_error(State, wrong_type, Value, Accumulator) end; check_value( Value , [{?PROPERTIES, Properties} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case is_json_object(Value) of true -> check_properties(Value, unwrap(Properties), - JsonSchema, Path, Accumulator0); + State, Accumulator0); false -> Accumulator0 end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?PATTERNPROPERTIES, PatternProperties} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case is_json_object(Value) of true -> check_pattern_properties(Value, PatternProperties, - Path, Accumulator0); + State, Accumulator0); false -> Accumulator0 end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?ADDITIONALPROPERTIES, AdditionalProperties} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case is_json_object(Value) of true -> check_additional_properties(Value, AdditionalProperties, - JsonSchema, Path, Accumulator0); + State, Accumulator0); false -> Accumulator0 end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?ITEMS, Items} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case is_array(Value) of - true -> check_items(Value, Items, JsonSchema, Path, Accumulator0); + true -> check_items(Value, Items, State, Accumulator0); false -> Accumulator0 end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); %% 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] - , JsonSchema - , Path + , State , Accumulator ) -> - check_value(Value, Attrs, JsonSchema, Path, Accumulator); + check_value(Value, Attrs, State, Accumulator); %% 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] - , JsonSchema - , Path + , State , Accumulator ) -> - check_value(Value, Attrs, JsonSchema, Path, Accumulator); + check_value(Value, Attrs, State, Accumulator); check_value( Value , [{?DEPENDENCIES, Dependencies} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case is_json_object(Value) of - true -> check_dependencies(Value, Dependencies, JsonSchema, - Path, Accumulator0); + true -> + check_dependencies(Value, Dependencies, State, Accumulator0); false -> Accumulator0 end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?MINIMUM, Minimum} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> - Exclusive = get_path(?EXCLUSIVEMINIMUM, JsonSchema), + Exclusive = get_state_path(?EXCLUSIVEMINIMUM, State), Accumulator1 = case check_minimum(Value, Minimum, Exclusive) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'not_in_range', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, not_in_range, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?MAXIMUM, Maximum} | Attrs] - , JsonSchema - , Path + , State , Accumulator0) -> - Exclusive = get_path(?EXCLUSIVEMAXIMUM, JsonSchema), + Exclusive = get_state_path(?EXCLUSIVEMAXIMUM, State), Accumulator1 = case check_maximum(Value, Maximum, Exclusive) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'not_in_range', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, not_in_range, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); %% 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] - , JsonSchema - , Path + , State , Accumulator ) -> - check_value(Value, Attrs, JsonSchema, Path, Accumulator); + check_value(Value, Attrs, State, Accumulator); %% 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] - , JsonSchema - , Path + , State , Accumulator ) -> - check_value(Value, Attrs, JsonSchema, Path, Accumulator); + check_value(Value, Attrs, State, Accumulator); check_value( Value , [{?MINITEMS, MinItems} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case check_min_items(Value, MinItems) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'wrong_size', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, wrong_size, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?MAXITEMS, MaxItems} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case check_max_items(Value, MaxItems) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'wrong_size', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, wrong_size, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?UNIQUEITEMS, UniqueItems} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case is_array(Value) of false -> Accumulator0; true -> - Error = check_unique_items(Value, UniqueItems, JsonSchema), - accumulate_error(Path, Error, Accumulator0) + Error = check_unique_items(Value, UniqueItems, State), + accumulate_error(State, Error, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?PATTERN, Pattern} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case check_pattern(Value, Pattern) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, - {'no_match', Pattern}, Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, {no_match, Pattern}, Value, + Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?MINLENGTH, MinLength} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case check_min_length(Value, MinLength) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'wrong_length', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, wrong_length, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?MAXLENGTH, MaxLength} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case check_max_length(Value, MaxLength) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'wrong_length', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, wrong_length, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); -check_value(Value, [{?ENUM, Enum} | Attrs], JsonSchema, Path, Accumulator0) -> + check_value(Value, Attrs, State, Accumulator1); +check_value(Value, [{?ENUM, Enum} | Attrs], State, Accumulator0) -> Accumulator1 = case check_enum(Value, Enum) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'not_in_range', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, not_in_range, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?FORMAT, Format} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case check_format(Value, Format) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'wrong_format', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, wrong_format, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?DIVISIBLEBY, DivisibleBy} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = case check_divisible_by(Value, DivisibleBy) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'not_divisible', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, not_divisible, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?DISALLOW, Disallow} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> Accumulator1 = - case check_disallow(Value, Disallow) of + case check_disallow(Value, Disallow, State) of true -> Accumulator0; false -> - Error = { 'data_invalid', JsonSchema, 'not_allowed', Value }, - accumulate_error(Path, Error, Accumulator0) + accumulate_data_error(State, not_allowed, Value, Accumulator0) end, - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); + check_value(Value, Attrs, State, Accumulator1); check_value( Value , [{?EXTENDS, Extends} | Attrs] - , JsonSchema - , Path + , State , Accumulator0 ) -> - Accumulator1 = check_extends(Value, Extends, Path, Accumulator0), - check_value(Value, Attrs, JsonSchema, Path, Accumulator1); -check_value(_Value, [], _JsonSchema, _Path, Accumulator) -> + Accumulator1 = check_extends(Value, Extends, State, Accumulator0), + check_value(Value, Attrs, State, Accumulator1); +check_value( Value, [{?_REF, Ref} | Attrs], State, Accumulator0) -> + Accumulator1 = check_ref(Value, Ref, State, Accumulator0), + check_value(Value, Attrs, State, Accumulator1); +check_value(_Value, [], _State, Accumulator) -> Accumulator; -check_value(Value, [_Attr | Attrs], JsonSchema, Path, Accumulator) -> - check_value(Value, Attrs, JsonSchema, Path, Accumulator). +check_value(Value, [_Attr | Attrs], State, Accumulator) -> + check_value(Value, Attrs, State, Accumulator). %% @doc 5.1. type %% @@ -492,22 +498,22 @@ check_value(Value, [_Attr | Attrs], JsonSchema, Path, Accumulator) -> %% %% {"type":["string","number"]} %% @private -check_type(Value, ?STRING) -> is_binary(Value); -check_type(Value, ?NUMBER) -> is_number(Value); -check_type(Value, ?INTEGER) -> is_integer(Value); -check_type(Value, ?BOOLEAN) -> is_boolean(Value); -check_type(Value, ?OBJECT) -> is_json_object(Value); -check_type(Value, ?ARRAY) -> is_array(Value); -check_type(Value, ?NULL) -> is_null(Value); -check_type(_Value, ?ANY) -> true; -check_type(Value, UnionType) -> +check_type(Value, ?STRING, _State) -> is_binary(Value); +check_type(Value, ?NUMBER, _State) -> is_number(Value); +check_type(Value, ?INTEGER, _State) -> is_integer(Value); +check_type(Value, ?BOOLEAN, _State) -> is_boolean(Value); +check_type(Value, ?OBJECT, _State) -> is_json_object(Value); +check_type(Value, ?ARRAY, _State) -> is_array(Value); +check_type(Value, ?NULL, _State) -> is_null(Value); +check_type(_Value, ?ANY, _State) -> true; +check_type(Value, UnionType, State) -> case is_array(UnionType) of - true -> check_union_type(Value, UnionType); + true -> check_union_type(Value, UnionType, State); false -> true end. %% @private -check_union_type(Value, UnionType) -> +check_union_type(Value, UnionType, State) -> lists:any( fun(Type) -> case is_json_object(Type) of @@ -516,10 +522,14 @@ check_union_type(Value, UnionType) -> %% then we need to validate against %% that schema Acc = {ok, {fun dummy_accumulator/3, undefined}}, - Path = jesse_json_path:new(), - Acc =:= check_value(Value, unwrap(Type), Type, Path, Acc); + NewState = + State#validator_state{ + current_schema = Type, + current_path = jesse_json_path:new() + }, + Acc =:= check_value(Value, unwrap(Type), NewState, Acc); false -> - true =:= check_type(Value, Type) + true =:= check_type(Value, Type, State) end end, UnionType @@ -537,7 +547,7 @@ check_union_type(Value, UnionType) -> %% the property definition. Properties are considered unordered, the %% order of the instance properties MAY be in any order. %% @private -check_properties(Value, Properties, Schema, Path, Accumulator) -> +check_properties(Value, Properties, State, Accumulator) -> FoldFun = fun({PropertyName, PropertySchema}, Acc0) -> case get_path(PropertyName, Value) of @@ -550,18 +560,17 @@ check_properties(Value, Properties, Schema, Path, Accumulator) -> %% @end case get_path(?REQUIRED, PropertySchema) of true -> - Err = { 'data_invalid', Schema, - {'missing_required_property', PropertyName}, - Value }, - accumulate_error(Path, Err, Acc0); + accumulate_data_error(State, + {missing_required_property, PropertyName}, + Value, Acc0); _ -> Acc0 end; Property -> - NewPath = jesse_json_path:push(PropertyName, Path), + NewState = push_path(State, PropertySchema, PropertyName), check_value(Property, unwrap(PropertySchema), - PropertySchema, NewPath, Acc0) + NewState, Acc0) end end, lists:foldl(FoldFun, Accumulator, Properties). @@ -576,12 +585,12 @@ check_properties(Value, Properties, Schema, Path, Accumulator) -> %% the instance's property MUST be valid against the pattern name's %% schema value. %% @private -check_pattern_properties(Value, PatternProperties, Path, Accumulator) -> +check_pattern_properties(Value, PatternProperties, State, Accumulator) -> lists:foldl( fun (Pattern, Accumulator0) -> lists:foldl( fun(Property, Accumulator1) -> - check_match(Property, Pattern, Path, Accumulator1) + check_match(Property, Pattern, State, Accumulator1) end, Accumulator0, unwrap(Value) @@ -593,12 +602,11 @@ check_pattern_properties(Value, PatternProperties, Path, Accumulator) -> %% @private check_match({PropertyName, PropertyValue}, {Pattern, Schema}, - Path, Accumulator) -> + State, Accumulator) -> case re:run(PropertyName, Pattern, [{capture, none}]) of match -> - NewPath = jesse_json_path:push(PropertyName, Path), - check_value(PropertyValue, unwrap(Schema), Schema, - NewPath, Accumulator); + NewState = push_path(State, Schema, PropertyName), + check_value(PropertyValue, unwrap(Schema), NewState, Accumulator); nomatch -> Accumulator end. @@ -611,36 +619,33 @@ check_match({PropertyName, PropertyValue}, {Pattern, Schema}, %% the schema. The default value is an empty schema which allows any %% value for additional properties. %% @private -check_additional_properties(Value, false, JsonSchema, Path, Accumulator) -> - Properties = get_path(?PROPERTIES, JsonSchema), - PatternProperties = get_path(?PATTERNPROPERTIES, JsonSchema), +check_additional_properties(Value, false, State, Accumulator) -> + Properties = get_state_path(?PROPERTIES, State), + PatternProperties = get_state_path(?PATTERNPROPERTIES, State), case get_additional_properties(Value, Properties, PatternProperties) of [] -> Accumulator; _Extras -> - Error = { 'data_invalid', JsonSchema, - 'no_extra_properties_allowed', Value }, - accumulate_error(Path, Error, Accumulator) + accumulate_data_error(State, no_extra_properties_allowed, Value, + Accumulator) end; -check_additional_properties(_Value, true, _JsonSchema, _Path, Accumulator) -> +check_additional_properties(_Value, true, _State, Accumulator) -> Accumulator; check_additional_properties( Value , AdditionalProperties - , JsonSchema - , Path + , State , Accumulator ) -> - Properties = get_path(?PROPERTIES, JsonSchema), - PatternProperties = get_path(?PATTERNPROPERTIES, JsonSchema), + Properties = get_state_path(?PROPERTIES, State), + PatternProperties = get_state_path(?PATTERNPROPERTIES, State), case get_additional_properties(Value, Properties, PatternProperties) of [] -> Accumulator; Extras -> lists:foldl( fun({Name, Extra}, Accumulator0) -> - NewPath = jesse_json_path:push(Name, Path), + NewState = push_path(State, AdditionalProperties, Name), check_value( Extra , unwrap(AdditionalProperties) - , AdditionalProperties - , NewPath + , NewState , Accumulator0 ) end, Accumulator, @@ -689,35 +694,35 @@ filter_extra_names(Pattern, ExtraNames) -> %% (Section 5.6) attribute using the same rules as %% "additionalProperties" (Section 5.4) for objects. %% @private -check_items(Value, Items, JsonSchema, Path, Accumulator) -> +check_items(Value, Items, State, Accumulator) -> case is_json_object(Items) of true -> element(2, lists:foldl( fun(Item, {Num, Acc0}) -> - NewPath = jesse_json_path:push(Num, Path), + NewState = push_path(State, Items, Num), { Num + 1, - check_value(Item, unwrap(Items), Items, NewPath, Acc0) } + check_value(Item, unwrap(Items), NewState, Acc0) } end, {0, Accumulator}, Value)); false when is_list(Items) -> - check_items_array(Value, Items, JsonSchema, Path, Accumulator); + check_items_array(Value, Items, State, Accumulator); _ -> - Error = {'schema_invalid', JsonSchema, {'wrong_type_items', Items}}, - accumulate_error(Path, Error, Accumulator) + accumulate_schema_error(State, {wrong_type_items, Items}, + Accumulator) end. %% @private -check_items_array(Value, Items, JsonSchema, Path, Accumulator) -> +check_items_array(Value, Items, State, Accumulator) -> CheckItemsFun = fun (Start, Tuples) -> element(2, lists:foldl( fun({Item, Schema}, {Num, Acc0}) -> - NewPath = jesse_json_path:push(Num, Path), + NewState = push_path(State, Schema, Num), { Num + 1, - check_value(Item, unwrap(Schema), Schema, NewPath, Acc0) } + check_value(Item, unwrap(Schema), NewState, Acc0) } end, {Start, Accumulator}, Tuples) @@ -735,22 +740,19 @@ check_items_array(Value, Items, JsonSchema, Path, Accumulator) -> 0 -> CheckItemsFun(0, lists:zip(Value, Items)); NExtra when NExtra > 0 -> - case get_path(?ADDITIONALITEMS, JsonSchema) of + case get_state_path(?ADDITIONALITEMS, State) of [] -> Accumulator; true -> Accumulator; false -> - accumulate_error( Path, - { 'data_invalid', JsonSchema, - 'no_extra_items_allowed', Value }, - Accumulator ); + accumulate_data_error( State, 'no_extra_items_allowed', + Value, Accumulator ); AdditionalItems -> ExtraSchemas = lists:duplicate(NExtra, AdditionalItems), CheckItemsFun(length(Value), lists:zip(Value, lists:append(Items, ExtraSchemas))) end; NExtra when NExtra < 0 -> - accumulate_error(Path, { 'data_invalid', JsonSchema, - 'not_enough_items', Value }, Accumulator) + accumulate_data_error(State, 'not_enough_items', Value, Accumulator) end. %% @doc 5.8. dependencies @@ -773,13 +775,13 @@ check_items_array(Value, Items, JsonSchema, Path, Accumulator) -> %% instance object MUST be valid against the schema. %% %% @private -check_dependencies(Value, Dependencies, JsonSchema, Path, Accumulator) -> +check_dependencies(Value, Dependencies, State, Accumulator) -> lists:foldl( fun({DependencyName, DependencyValue}, Acc0) -> case get_path(DependencyName, Value) of [] -> Acc0; - _ -> check_dependency_value(Value, DependencyValue, JsonSchema, - Path, Acc0) + _ -> + check_dependency_value(Value, DependencyValue, State, Acc0) end end, Accumulator, @@ -787,36 +789,35 @@ check_dependencies(Value, Dependencies, JsonSchema, Path, Accumulator) -> ). %% @private -check_dependency_value(Value, Dependency, JsonSchema, Path, Accumulator) +check_dependency_value(Value, Dependency, State, Accumulator) when is_binary(Dependency) -> case get_path(Dependency, Value) of [] -> - Error = { 'data_invalid', JsonSchema, - {'missing_dependency', Dependency}, Value }, - accumulate_error(Path, Error, Accumulator); + accumulate_data_error(State, {missing_dependency, Dependency}, + Value, Accumulator); _ -> Accumulator end; -check_dependency_value(Value, Dependency, JsonSchema, Path, Accumulator) -> +check_dependency_value(Value, Dependency, State, Accumulator) -> case is_json_object(Dependency) of true -> - check_value(Value, unwrap(Dependency), Dependency, - Path, Accumulator); + check_value(Value, unwrap(Dependency), + State#validator_state{current_schema = Dependency}, + Accumulator); false -> case is_list(Dependency) of - true -> check_dependency_array(Value, Dependency, JsonSchema, - Path, Accumulator); + true -> check_dependency_array(Value, Dependency, State, + Accumulator); false -> - Error = { 'schema_invalid', JsonSchema, - {'wrong_type_dependency', Dependency} }, - accumulate_error(Path, Error, Accumulator) + accumulate_schema_error(State, + {wrong_type_dependency, Dependency}, Accumulator) end end. %% @private -check_dependency_array(Value, Dependency, JsonSchema, Path, Accumulator) -> +check_dependency_array(Value, Dependency, State, Accumulator) -> lists:foldl( fun(PropertyName, Acc0) -> - check_dependency_value(Value, PropertyName, JsonSchema, Path, Acc0) + check_dependency_value(Value, PropertyName, State, Acc0) end, Accumulator, Dependency @@ -905,7 +906,7 @@ check_max_items(Value, MaxItems) -> %% object. %% %% @private -check_unique_items(Value, true, JsonSchema) -> +check_unique_items(Value, true, State) -> try lists:foldl( fun (_Item, []) -> ok; @@ -914,7 +915,7 @@ check_unique_items(Value, true, JsonSchema) -> fun(ItemFromRest) -> case is_equal(Item, ItemFromRest) of true -> - throw({'data_invalid', JsonSchema, + throw({'data_invalid', State#validator_state.current_schema, {'not_unique', Item}, Value}); false -> ok end @@ -1000,8 +1001,8 @@ check_divisible_by(_Value, _DivisibleBy) -> true. %% instance matches any type or schema in the array, then this instance %% is not valid. %% @private -check_disallow(Value, Disallow) -> - case check_type(Value, Disallow) of +check_disallow(Value, Disallow, State) -> + case check_type(Value, Disallow, State) of true -> false; false -> true end. @@ -1017,25 +1018,85 @@ check_disallow(Value, Disallow) -> %% another schema MAY define additional attributes, constrain existing %% attributes, or add other constraints. %% @private -check_extends(Value, Extends, Path, Accumulator) -> +check_extends(Value, Extends, State, Accumulator) -> case is_json_object(Extends) of true -> - check_value(Value, unwrap(Extends), Extends, Path, Accumulator); + NewState = State#validator_state{current_schema = Extends}, + check_value(Value, unwrap(Extends), NewState, Accumulator); false -> case is_list(Extends) of - true -> check_extends_array(Value, Extends, Path, Accumulator); + true -> check_extends_array(Value, Extends, State, Accumulator); false -> ok %% TODO: implement handling of $ref end end. %% @private -check_extends_array(Value, Extends, Path, Accumulator) -> +check_extends_array(Value, Extends, State, Accumulator) -> lists:foldl( - fun(SchemaKey, Acc0) -> check_extends(Value, SchemaKey, Path, Acc0) end, + fun(SchemaKey, Acc0) -> + check_extends(Value, SchemaKey, State, Acc0) end, Accumulator, Extends ). +%% @private +get_by_pointer(Json, []) -> {ok, Json}; +get_by_pointer(Json, [K | P]) -> + case get_by_pointer_path(Json, K) of + {ok, Next} -> get_by_pointer(Next, P); + {error, Cause, Reason} -> {error, Cause, Reason} + end. + +%% @private +get_by_pointer_path(Json, K) when is_list(Json) -> + case string:to_integer(K) of + {error, Reason} -> {error, no_parse, Reason}; + {N, ""} -> + if + (N >= 0) andalso (N < length(Json)) -> + {ok, lists:nth(N+1, Json)}; + true -> {error, wrong_number, {N, Json}} + end + end; +get_by_pointer_path(Json, K) -> + case jesse_json_path:value(K, Json, error) of + error -> {error, no_key, {K, Json}}; + V -> {ok, V} + end. + +%% @private +check_ref(Value, URI, State, Accumulator) -> + case parse_jesse_uri(State, URI) of + {ok, {Id, SubPath}} -> + try + NewSchema = + case Id of + "" -> State#validator_state.global_schema; + _ -> jesse_database:read(list_to_binary(Id)) + end, + case get_by_pointer(NewSchema, SubPath) of + {error, Cause, Reason} -> + accumulate_schema_error(State, + {path_not_found, {Cause, Reason}, + {Id, SubPath, NewSchema}}, + Accumulator); + {ok, SubSchema} -> + NewState = + State#validator_state{ + current_schema = SubSchema, + global_schema = NewSchema + }, + check_value(Value, unwrap(SubSchema), NewState, + Accumulator) + end + catch + throw:{database_error, Key, schema_not_found} -> + accumulate_schema_error(State, + {subschema_not_found, Key}, Accumulator) + end; + {error, Err} -> accumulate_data_error(State, Err, URI, Accumulator) + end. + %%============================================================================= %% @doc Returns `true' if given values (instance) are equal, otherwise `false' %% is returned. @@ -1098,17 +1159,71 @@ compare_properties(Value1, Value2) -> , unwrap(Value1) ). +%%============================================================================= +%% @doc Parses uri, returning {ok, {schema_id, path}} +%% @private +-spec parse_jesse_uri(State :: #validator_state{}, URI :: iodata()) -> + {ok, {string(), [string()]}} | {error, any()}. +parse_jesse_uri(State, URI) when is_binary(URI) -> + parse_jesse_uri(State, binary_to_list(URI)); +parse_jesse_uri(State, URI) when is_list(URI) -> + case jesse_uri:parse(URI) of + {error, Reason} -> {error, Reason}; + {ok, ParsedURI} -> + parse_jesse_uri(State#validator_state.base_uri, + ParsedURI, jesse_uri:is_local(ParsedURI)) + end. + +-spec parse_jesse_uri( + BaseURI :: no_base_uri | jesse_uri:uri(), + URI :: jesse_uri:uri(), + IsLocal :: boolean()) -> + {ok, {string(), [string()]}} | {error, any()}. +parse_jesse_uri(BaseURI, URI, IsLocal) -> + case jesse_uri:get_fragment(URI) of + no_fragment -> + parse_jesse_uri(BaseURI, URI, IsLocal, []); + Fragment -> + parse_jesse_uri(BaseURI, URI, IsLocal, string:tokens(Fragment, "/")) + end. + +-spec parse_jesse_uri( + BaseURI :: no_base_uri | jesse_uri:uri(), + URI :: jesse_uri:uri(), + IsLocal :: boolean(), + FragmentPath :: [string()]) -> + {ok, {string(), [string()]}} | {error, any()}. +parse_jesse_uri(_BaseURI, _URI, true, FragmentPath) -> {ok, {"", FragmentPath}}; +parse_jesse_uri(BaseURI, URI, false, FragmentPath) -> + PureURI = jesse_uri:set_fragment(URI, no_fragment), + case resolve_fix(BaseURI, PureURI) of + {ok, FullURI} -> + {ok, {jesse_uri:render(FullURI), FragmentPath}}; + {error, Error} -> {error, Error} + end. + +-spec resolve_fix( + Base_URI :: no_base_uri | jesse_uri:uri(), + URI :: jesse_uri:uri()) -> + {ok, jesse_uri:uri()} | {error, any()}. +resolve_fix(no_base_uri, URI) -> {ok, URI}; +resolve_fix(BaseURI, URI) -> jesse_uri:resolve(BaseURI, URI). + %%============================================================================= %% @private get_path(Key, Schema) -> jesse_json_path:path(Key, Schema). +%% @private +get_state_path(Key, State) -> + get_path(Key, State#validator_state.current_schema). + %% @private unwrap(Value) -> jesse_json_path:unwrap_value(Value). %%% --------------------------------------------------------------------------- --spec accumulate_error/3:: ( Path :: jesse_json_path:path() +-spec accumulate_error/3:: ( State :: #validator_state{} , Error :: ok | error() , Acc0 :: {ok | error, {jesse:accumulator(), term()}} ) -> @@ -1118,11 +1233,26 @@ unwrap(Value) -> %% @doc Handle error using provided callback fun %% @end %%% --------------------------------------------------------------------------- -accumulate_error(_Path, ok, Accumulator) -> Accumulator; -accumulate_error(Path, Error, {_Result, {Fun, Acc0}}) -> - StringPath = jesse_json_path:to_string(Path), +accumulate_error(_State, ok, Accumulator) -> Accumulator; +accumulate_error(State, Error, {_Result, {Fun, Acc0}}) -> + StringPath = jesse_json_path:to_string(State#validator_state.current_path), {error, {Fun, Fun(StringPath, Error, Acc0)}}. +-spec accumulate_data_error/4 :: ( State :: #validator_state{}, + Reason :: reason(), Data :: jesse:json_term(), + Acc0 :: {ok | error, {jesse:accumulator(), term()}}) -> + Acc1 :: {error, {jesse:accumulator(), term()}}. +accumulate_data_error(State, Reason, Data, Acc0) -> + Error = {data_invalid, State#validator_state.current_schema, + Reason, Data}, + accumulate_error(State, Error, Acc0). +-spec accumulate_schema_error/3 :: ( State :: #validator_state{}, + Reason :: reason(), Acc0 :: {ok | error, {jesse:accumulator(), term()}}) -> + Acc1 :: {error, {jesse:accumulator(), term()}}. +accumulate_schema_error(State, Reason, Acc0) -> + Error = {schema_invalid, State#validator_state.current_schema, Reason}, + accumulate_error(State, Error, Acc0). + dummy_accumulator(_Path, _Error, undefined) -> undefined. diff --git a/src/jesse_uri.erl b/src/jesse_uri.erl new file mode 100644 index 0000000..50317ad --- /dev/null +++ b/src/jesse_uri.erl @@ -0,0 +1,256 @@ +%%%----------------------------------------------------------------------------- +%% @doc Parsing and combining URIs. +%% +%% This module follows the rules of RFC3986. It provides interface for +%% parsing and rendering URIs from/to their string representation. Also +%% this module resolves relative URIs against given base. This module is +%% completely side-effect free and, in particular, throws no exceptions. +%% @end +%% +%% @author Mikhail Mitrofanov +%% @copyright 2013 Alert Logic, Inc. +%%%----------------------------------------------------------------------------- +%%%============================================================================= + +-module(jesse_uri). + +-export([parse/1, render/1, is_absolute/1, is_local/1, resolve/2]). +-export([get_fragment/1, set_fragment/2]). + +-type(scheme() :: string()). +-type(userinfo() :: no_userinfo | string()). +-type(host() :: string()). +-type(uport() :: no_port | integer()). +-type(authority() :: {userinfo(), host(), uport()}). +-type(segment() :: string()). +-type(path() :: [segment()]). +-type(uquery() :: no_query | string()). +-type(fragment() :: no_fragment | string()). +-type(hier() :: + {with_host, authority(), path()} | + {absolute, path()} | + {relative, path()} | + empty +). +-type(absolute() :: {scheme(), hier(), uquery(), fragment()}). +-type(relative() :: {hier(), uquery(), fragment()}). +-type(uri() :: absolute() | relative()). + +-spec(get_fragment(URI :: uri()) -> fragment()). +get_fragment({_Scheme, _Hier, _Query, Fragment}) -> Fragment; +get_fragment({_Hier, _Query, Fragment}) -> Fragment. + +-spec(set_fragment(URI :: uri(), Fragment :: fragment()) -> uri()). +set_fragment({Scheme, Hier, Query, _Fragment}, Fragment) -> + {Scheme, Hier, Query, Fragment}; +set_fragment({Hier, Query, _Fragment}, Fragment) -> + {Hier, Query, Fragment}. + +-spec(index_of(Input :: list(), Elt :: term()) -> not_found | integer). +index_of(Input, Elt) -> index_of(Input, Elt, 1). + +-spec(index_of(Input :: list(), Elt :: term(), N :: integer) -> + not_found | integer). +index_of([], _Elt, _N) -> not_found; +index_of([C | Rest], Elt, N) -> + if + C =:= Elt -> N; + true -> index_of(Rest, Elt, N+1) + end. + +-spec(chop(Input :: list(), Sep :: term()) -> nothing | {list(), list()}). +chop(Input, Sep) -> + case index_of(Input, Sep) of + not_found -> nothing; + N -> + {Start, End} = lists:split(N-1, Input), + {Start, tl(End)} + end. + +-spec(split_by(Input :: list(), Sep :: term()) -> [list()]). +split_by(Input, Sep) -> + case chop(Input, Sep) of + nothing -> [Input]; + {Start, End} -> [Start | split_by(End, Sep)] + end. + +-spec(chop_fragment(Input :: string()) -> {string(), fragment()}). +chop_fragment(Input) -> + case chop(Input, $#) of + nothing -> {Input, no_fragment}; + {Start, End} -> {Start, End} + end. + +-spec(chop_query(Input :: string()) -> {string(), uquery()}). +chop_query(Input) -> + case chop(Input, $?) of + nothing -> {Input, no_query}; + {Start, End} -> {Start, End} + end. + +-spec(parse_authority(Input :: string()) -> + {error, term()} | {ok, authority()}). +parse_authority(Input) -> + {UserInfo, HostAndPort} = + case chop(Input, $@) of + nothing -> {no_userinfo, Input}; + {Start, End} -> {Start, End} + end, + case chop(HostAndPort, $:) of + nothing -> {ok, {UserInfo, HostAndPort, no_port}}; + {Host, PortString} -> + case string:to_integer(PortString) of + {Port, ""} -> {ok, UserInfo, Host, Port}; + _ -> {error, {invalid_port, PortString}} + end + end. + +-spec(parse_hier(Input :: string()) -> {error, term()} | {ok, hier()}). +parse_hier([]) -> {ok, empty}; +parse_hier([$/, $/ | Rest]) -> + {Authority, Path} = + case chop(Rest, $/) of + nothing -> {Rest, []}; + {Start, End} -> {Start, split_by(End, $/)} + end, + case parse_authority(Authority) of + {error, Reason} -> {error, Reason}; + {ok, Auth} -> {ok, {with_host, Auth, Path}} + end; +parse_hier([$/ | Rest]) -> {ok, {absolute, split_by(Rest, $/)}}; +parse_hier(Path) -> {ok, {relative, split_by(Path, $/)}}. + +-spec(parse_absolute(Input :: string()) -> {error, term()} | {ok, absolute()}). +parse_absolute(Input) -> + {BeforeFragment, Fragment} = chop_fragment(Input), + {BeforeQuery, Query} = chop_query(BeforeFragment), + case chop(BeforeQuery, $:) of + nothing -> {error, no_scheme}; + {Scheme, HierString} -> + case parse_hier(HierString) of + {error, Reason} -> {error, Reason}; + {ok, Hier} -> + {ok, {Scheme, Hier, Query, Fragment}} + end + end. + +-spec(parse_relative(Input :: string()) -> {error, term()} | {ok, relative()}). +parse_relative(Input) -> + {BeforeFragment, Fragment} = chop_fragment(Input), + {BeforeQuery, Query} = chop_query(BeforeFragment), + case parse_hier(BeforeQuery) of + {error, Reason} -> {error, Reason}; + {ok, Hier} -> {ok, {Hier, Query, Fragment}} + end. + +-spec(is_absolute(URI :: uri()) -> boolean()). +is_absolute({_Scheme, _Hier, _Query, _Fragment}) -> true; +is_absolute({_Hier, _Query, _Fragment}) -> false. + +-spec(is_local(URI :: uri()) -> boolean()). +is_local({empty, no_query, _Fragment}) -> true; +is_local({_Scheme, _Hier, _Query, _Fragment}) -> false; +is_local({_Hier, _Query, _Fragment}) -> false. + +-spec(parse(Input :: string()) -> {error, term()} | {ok, uri()}). +parse(Input) -> + case parse_absolute(Input) of + {ok, Absolute} -> {ok, Absolute}; + {error, _} -> parse_relative(Input) + end. + +-spec(render_authority(Authority :: authority()) -> string()). +render_authority({UserInfo, Host, Port}) -> + UserInfoString = + case UserInfo of + no_userinfo -> ""; + _ -> string:concat(UserInfo, "@") + end, + PortString = + case Port of + no_port -> ""; + _ -> [$: | integer_to_list(Port)] + end, + lists:concat([UserInfoString, Host, PortString]). + +-spec(render_path(Path :: path()) -> string()). +render_path([]) -> ""; +render_path([Segment]) -> Segment; +render_path([Segment | Rest]) -> lists:concat([Segment, $/, render_path(Rest)]). + +-spec(render_hier(Hier :: hier()) -> string()). +render_hier({with_host, Authority, []}) -> + lists:concat(["//", render_authority(Authority)]); +render_hier({with_host, Authority, Path}) -> + lists:concat(["//", render_authority(Authority), "/", render_path(Path)]); +render_hier({absolute, Path}) -> [$/ | render_path(Path)]; +render_hier({relative, Path}) -> render_path(Path); +render_hier(empty) -> "". + +-spec(render_relative(URI :: relative()) -> string()). +render_relative({Hier, Query, Fragment}) -> + QueryString = + case Query of + no_query -> ""; + _ -> [$? | Query] + end, + FragmentString = + case Fragment of + no_fragment -> ""; + _ -> [$# | Fragment] + end, + lists:concat([render_hier(Hier), QueryString, FragmentString]). + +-spec(render_absolute(URI :: absolute()) -> string()). +render_absolute({Scheme, Hier, Query, Fragment}) -> + lists:concat([Scheme, ":", render_relative({Hier, Query, Fragment})]). + +-spec(render(URI :: uri()) -> string()). +render(URI) -> + IsAbs = is_absolute(URI), + if + IsAbs -> render_absolute(URI); + true -> render_relative(URI) + end. + +-spec(merge_paths(BPath :: path(), Path :: path()) -> path()). +merge_paths([], Path) -> Path; +merge_paths([_Segment], Path) -> Path; +merge_paths([Segment | Rest], Path) -> [Segment | merge_paths(Rest, Path)]. + +-spec(remove_dots(Path :: path()) -> path()). +remove_dots(Path) -> + try + remove_dots(Path, []) + catch + throw:overflow -> throw({overflow, Path}) + end. + +-spec(remove_dots(Path :: path(), Buffer :: [segment()]) -> path()). +remove_dots([], Buffer) -> lists:reverse(Buffer); +remove_dots(["." | Path], Buffer) -> remove_dots(Path, Buffer); +remove_dots([".." | Path], [_ | Buffer]) -> remove_dots(Path, Buffer); +remove_dots([".." | _], []) -> throw(overflow); +remove_dots([Segment | Path], Buffer) -> remove_dots(Path, [Segment | Buffer]). + +-spec(resolve(Base :: absolute(), URI :: uri()) -> + {ok, uri()} | {error, term()}). +resolve(_BaseURI, {_Scheme, _Hier, _Query, _Fragment} = URI) -> {ok, URI}; +resolve({Scheme, Hier, _Query, _Fragment}, {RHier, Query, Fragment}) -> + try + {ok, {Scheme, join_hier(Hier, RHier), Query, Fragment}} + catch + throw:Err -> {error, Err} + end. + +-spec(join_hier(BaseHier :: hier(), Hier :: hier()) -> hier()). +join_hier(_BaseHier, {with_host, _Authority, _Path} = Hier) -> Hier; +join_hier({with_host, Authority, _Path}, {absolute, Path}) -> + {with_host, Authority, remove_dots(Path)}; +join_hier(_BaseHier, {absolute, Path}) -> {absolute, remove_dots(Path)}; +join_hier({with_host, Authority, BPath}, {relative, Path}) -> + {with_host, Authority, remove_dots(merge_paths(BPath, Path))}; +join_hier({AbsOrRel, BPath}, {relative, Path}) -> + {AbsOrRel, remove_dots(merge_paths(BPath, Path))}; +join_hier(empty, {relative, _Path} = URI) -> URI; +join_hier(BaseHier, empty) -> BaseHier. \ No newline at end of file diff --git a/test/jesse_ref_SUITE.erl b/test/jesse_ref_SUITE.erl new file mode 100644 index 0000000..ed77b50 --- /dev/null +++ b/test/jesse_ref_SUITE.erl @@ -0,0 +1,114 @@ +-module(jesse_ref_SUITE). + +-export([ all/0 + , init_per_suite/1 + , end_per_suite/1 + ]). + +-export([basic/1]). + +-include_lib("common_test/include/ct.hrl"). + +-define(DATA, <<"data">>). +-define(DESCRIPTION, <<"description">>). +-define(SCHEMAS, <<"schemas">>). +-define(_SCHEMA, <<"$schema">>). +-define(TESTS, <<"tests">>). +-define(VALID, <<"valid">>). + +all() -> + [ + basic + ]. + +%%% +init_per_suite(Config) -> + TestSpecs = load_test_specs(?config(data_dir, Config)), + TestSpecs ++ Config. + +end_per_suite(_Config) -> + ok. + +%%% Testcases +basic(Config) -> + Key = "basic", + Specs = ?config(Key, Config), + ok = run_tests(Specs). + +%%% Internal functions +run_tests(Specs) -> + lists:foreach( fun(Spec) -> + Description = get_path(?DESCRIPTION, Spec), + Schemas = get_path(?SCHEMAS, Spec), + TestSet = get_path(?TESTS, Spec), + io:format("** Test set: ~s~n", [Description]), + run_test_set(Schemas, TestSet) + end + , Specs + ). + +run_test_set(Schemas, TestSet) -> + lists:foreach( + fun (Schema) -> + Id = + try + jesse_schema_validator:get_schema_id(Schema) + catch + throw:_ -> "" + end, + case jesse:add_schema(list_to_binary(Id), Schema) of + ok -> ok; + {error, Err} -> throw(Err) + end + end, + Schemas + ), + lists:foreach( fun(Test) -> + Description = get_path(?DESCRIPTION, Test), + TestData = get_path(?DATA, Test), + io:format("* Test case: ~s~n", [Description]), + SchemaId = + case get_path(?_SCHEMA, TestData) of + [] -> <<>>; + Id -> Id + end, + Schema0 = jesse_database:read(SchemaId), + Result = jesse:validate_with_schema(Schema0, TestData), + io:format("Result: ~p~n", [Result]), + case get_path(?VALID, Test) of + true -> {ok, TestData} = Result; + false -> {error, Error} = Result, + match_error(Error) + end + end + , TestSet + ). + +load_test_specs(TestsDir) -> + FileList = filelib:wildcard(TestsDir ++ "/*.json"), + lists:map( fun(Filename) -> + {ok, Bin} = file:read_file(Filename), + JsonTest = jiffy:decode(Bin), + {filename_to_key(Filename), JsonTest} + end + , FileList + ). + +filename_to_key(Filename) -> + filename:rootname(filename:basename(Filename)). + +get_path(Key, Schema) -> + jesse_json_path:path(Key, Schema). + +match_error({'data_invalid', _, Type, _}) -> match_error_type(Type); +match_error({'schema_invalid', _, Type}) -> match_error_type(Type); +match_error(Error) -> throw({'wrong_error_format', Error}). + +match_error_type(Atom) when is_atom(Atom) -> ok; +match_error_type(Tuple) when is_tuple(Tuple) + andalso is_atom(element(1, Tuple)) -> ok; +match_error_type(Type) -> throw({'wrong_error_type', Type}). + +%%% Local Variables: +%%% erlang-indent-level: 2 +%%% End: diff --git a/test/jesse_ref_SUITE_data/basic.json b/test/jesse_ref_SUITE_data/basic.json new file mode 100644 index 0000000..7e9225f --- /dev/null +++ b/test/jesse_ref_SUITE_data/basic.json @@ -0,0 +1,105 @@ +[ + { + "description": "basic $ref tests", + "schemas": [ + { + "id": "http://a", + "type": "object", + "properties": { + "$schema": {"enum": ["http://a"]}, + "a": {"type": "string"} + } + }, + { + "id": "http://b", + "type": "object", + "properties": { + "$schema": {"enum": ["http://b"]}, + "b": {"type": "string"}, + "c": {"$ref": "http://a"} + } + }, + { + "id": "http://c/boo/", + "type": "object", + "properties": { + "$schema": {"enum": ["http://c/boo/"]}, + "x": {"$ref": "../oob"} + } + }, + { + "id": "http://c/oob", + "type": "object", + "properties": { + "$schema": {"enum": ["http://c/oob"]}, + "y": {"type": "string"} + } + }, + { + "id": "http://c/fif/ifi", + "type": "object", + "properties": { + "$schema": {"enum": ["http://c/fif/ifi"]}, + "z": {"$ref": "../oob"} + } + }, + { + "id": "http://c/ifi/fif", + "type": "object", + "properties": { + "$schema": {"enum": ["http://c/ifi/fif"]}, + "w": {"$ref": "oob"} + } + }, + { + "id": "http://c/ifi/oob", + "type": "string" + } + ], + "tests": [ + { + "description": "object with id", + "data": {"$schema": "http://a", "a": "test"}, + "valid": true + }, + { + "description": "referencing another scheme", + "data": { + "$schema": "http://b", + "b": "test", + "c": {"a": "subtest"} + }, + "valid": true + }, + { + "description": "up-reference", + "data": { + "$schema": "http://c/boo/", + "x": { + "y": "upref" + } + }, + "valid": true + }, + { + "description": "up-reference from file", + "data": { + "$schema": "http://c/fif/ifi", + "z": { + "y": "upref" + } + }, + "valid": true + }, + { + "description": "mismatch", + "data": { + "$schema": "http://c/ifi/fif", + "w": { + "y": "upref mismatch" + } + }, + "valid": false + } + ] + }] diff --git a/test/jesse_tests_draft3_SUITE.erl b/test/jesse_tests_draft3_SUITE.erl index aa221a7..1847cda 100644 --- a/test/jesse_tests_draft3_SUITE.erl +++ b/test/jesse_tests_draft3_SUITE.erl @@ -43,7 +43,7 @@ , pattern/1 , patternProperties/1 , properties/1 - %% , ref/1 + , ref/1 , required/1 , type/1 , uniqueItems/1 @@ -78,7 +78,7 @@ all() -> , pattern , patternProperties , properties - %% , ref + , ref , required , type , uniqueItems @@ -180,10 +180,10 @@ properties(Config) -> ok = run_tests(Specs). %% not implemented yet -%% ref(Config) -> -%% Key = "ref", -%% Specs = ?config(Key, Config), -%% ok = run_tests(Specs). +ref(Config) -> + Key = "ref", + Specs = ?config(Key, Config), + ok = run_tests(Specs). required(Config) -> Key = "required",