diff --git a/AUTHORS b/AUTHORS index ae3e4842..756cad00 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,4 @@ Emil Falk Daniel Lindberg Andrei Neculau Stefan Strigler +Emilio Del Tessandoro diff --git a/README.md b/README.md index abe4a8b3..3fba0f2a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ jesse implements [Draft 03] (http://tools.ietf.org/html/draft-zyp-json-schema-03 the specification. It supports almost all core schema definitions except: * format -* $ref ## Quick start - CLI diff --git a/src/jesse_schema_validator.erl b/src/jesse_schema_validator.erl index 93bf60c4..395dca85 100644 --- a/src/jesse_schema_validator.erl +++ b/src/jesse_schema_validator.erl @@ -58,8 +58,7 @@ validate_with_state(JsonSchema, Value, State) -> select_and_run_validator(SchemaVer, JsonSchema, Value, State). %%% Internal functions -%% @doc Returns "$schema" property from `JsonSchema' if it is present, -%% otherwise the default schema version from `State' is returned. + %% @private get_schema_ver(JsonSchema, State) -> case jesse_json_path:value(?_SCHEMA, JsonSchema, ?not_found) of diff --git a/src/jesse_schema_validator.hrl b/src/jesse_schema_validator.hrl index 38238d5a..6e2b9f6f 100644 --- a/src/jesse_schema_validator.hrl +++ b/src/jesse_schema_validator.hrl @@ -47,7 +47,7 @@ -define(DISALLOW, <<"disallow">>). -define(EXTENDS, <<"extends">>). -define(ID, <<"id">>). --define(_REF, <<"$ref">>). % NOT IMPLEMENTED YET +-define(REF, <<"$ref">>). %% Constant definitions for Json types -define(ANY, <<"any">>). @@ -58,6 +58,7 @@ -define(NUMBER, <<"number">>). -define(OBJECT, <<"object">>). -define(STRING, <<"string">>). +-define(SEPARATOR, <<"/">>). %% Supported $schema attributes -define(default_schema_ver, <<"http://json-schema.org/draft-03/schema#">>). @@ -70,6 +71,7 @@ -define(missing_id_field, 'missing_id_field'). -define(missing_required_property, 'missing_required_property'). -define(missing_dependency, 'missing_dependency'). +-define(missing_ref_path, 'missing_ref_path'). -define(no_match, 'no_match'). -define(no_extra_properties_allowed, 'no_extra_properties_allowed'). -define(no_extra_items_allowed, 'no_extra_items_allowed'). diff --git a/src/jesse_state.erl b/src/jesse_state.erl index 3cf87cab..6d5032b8 100644 --- a/src/jesse_state.erl +++ b/src/jesse_state.erl @@ -28,6 +28,7 @@ , get_allowed_errors/1 , get_current_path/1 , get_current_schema/1 + , get_original_schema/1 , get_default_schema_ver/1 , get_error_handler/1 , get_error_list/1 @@ -36,11 +37,17 @@ , set_allowed_errors/2 , set_current_schema/2 , set_error_list/2 + , find_schema/2 ]). -export_type([ state/0 ]). +%% Includes +-include("jesse_schema_validator.hrl"). + +-define(SCHEMA_LOADER_FUN, fun jesse_database:read/1). + %% Internal datastructures -record( state , { original_schema :: jesse:json_term() @@ -50,14 +57,14 @@ , error_list :: list() , error_handler :: fun((#state{}) -> list() | no_return()) , default_schema_ver :: atom() + , schema_loader_fun :: fun((binary()) -> {ok, jesse:json_term()} | + jesse:json_term() | + ?not_found) } ). -opaque state() :: #state{}. -%% Includes --include("jesse_schema_validator.hrl"). - %%% API %% @doc Adds `Property' to the `current_path' in `State'. -spec add_to_path(State :: state(), Property :: binary()) -> state(). @@ -80,6 +87,11 @@ get_current_path(#state{current_path = CurrentPath}) -> get_current_schema(#state{current_schema = CurrentSchema}) -> CurrentSchema. +%% @doc Getter for `original_schema'. +-spec get_original_schema(State :: state()) -> jesse:json_term(). +get_original_schema(#state{original_schema = OriginalSchema}) -> + OriginalSchema. + %% @doc Getter for `default_schema_ver'. -spec get_default_schema_ver(State :: state()) -> binary(). get_default_schema_ver(#state{default_schema_ver = SchemaVer}) -> @@ -114,6 +126,10 @@ new(JsonSchema, Options) -> , Options , ?default_schema_ver ), + LoaderFun = proplists:get_value( schema_loader_fun + , Options + , ?SCHEMA_LOADER_FUN + ), #state{ current_schema = JsonSchema , current_path = [] , original_schema = JsonSchema @@ -121,6 +137,7 @@ new(JsonSchema, Options) -> , error_list = [] , error_handler = ErrorHandler , default_schema_ver = DefaultSchemaVer + , schema_loader_fun = LoaderFun }. %% @doc Removes the last element from `current_path' in `State'. @@ -147,6 +164,21 @@ set_current_schema(State, NewSchema) -> set_error_list(State, ErrorList) -> State#state{error_list = ErrorList}. +%% @doc Find a schema based on URI +-spec find_schema(State :: state(), SchemaURI :: binary()) -> + jesse:json_term() | ?not_found. +find_schema(#state{schema_loader_fun=LoaderFun}, SchemaURI) -> + try LoaderFun(SchemaURI) of + {ok, Schema} -> Schema; + Schema -> + case jesse_lib:is_json_object(Schema) of + true -> Schema; + false -> ?not_found + end + catch + _:_ -> ?not_found + end. + %%% Local Variables: %%% erlang-indent-level: 2 %%% End: diff --git a/src/jesse_validator_draft3.erl b/src/jesse_validator_draft3.erl index 075d1df4..4ae0f9cd 100644 --- a/src/jesse_validator_draft3.erl +++ b/src/jesse_validator_draft3.erl @@ -186,6 +186,9 @@ check_value(Value, [{?DISALLOW, Disallow} | Attrs], State) -> check_value(Value, [{?EXTENDS, Extends} | Attrs], State) -> NewState = check_extends(Value, Extends, State), check_value(Value, Attrs, NewState); +check_value(Value, [{?REF, RefSchemaURI} | Attrs], State) -> + NewState = check_ref(Value, RefSchemaURI, State), + check_value(Value, Attrs, NewState); check_value(_Value, [], State) -> State; check_value(Value, [_Attr | Attrs], State) -> @@ -202,6 +205,65 @@ check_value(Property, Value, Attrs, State) -> %% Reset path again jesse_state:remove_last_from_path(State2). + +%% @private +check_ref(Value, <<"#", LocalPath/binary>> = RefSchemaURI, State) -> + Keys = binary:split(LocalPath, <<"/">>, ['global']), + DecodedKeys = lists:map(fun(T) -> decode_path_element(T) end, Keys), + OriginalSchema = jesse_state:get_original_schema(State), + + case local_schema(OriginalSchema, DecodedKeys) of + ?not_found -> handle_schema_invalid({no_such_ref, RefSchemaURI}, State); + LocalSchema -> check_ref_schema(Value, LocalSchema, State) + end; +check_ref(Value, RefSchemaURI, State) -> + case jesse_state:find_schema(State, RefSchemaURI) of + ?not_found -> handle_schema_invalid({no_such_schema, RefSchemaURI}, State); + RefSchema -> check_ref_schema(Value, RefSchema, State) + end. + +%% @private +check_ref_schema(Value, RefSchema, State) -> + TmpState = check_value(Value + , unwrap(RefSchema) + , set_current_schema(State, RefSchema)), + set_current_schema(TmpState, get_current_schema(State)). + +local_schema(Schema, []) -> Schema; +local_schema(Schema, [<<>> | Keys]) -> local_schema(Schema, Keys); +local_schema(Schema, [Key | Keys]) -> + SubSchema = get_value(Key, Schema), + case {Key, jesse_lib:is_json_object(SubSchema)} of + {?ITEMS, _} -> local_schema_array(SubSchema, Keys); + {_, true} -> local_schema(SubSchema, Keys); + {_, false} -> ?not_found + end. + +local_schema_array(Schema, [Key | Keys]) -> + try + Index = binary_to_integer(Key), + SubSchema = lists:nth(Index + 1, Schema), + local_schema(SubSchema, Keys) + catch + % maybe Item is not an integer + error:badarg -> ?not_found; + % maybe it happens to be an integer and there's not such index in the list + error:function_clause -> ?not_found + end; +local_schema_array(Schema, []) -> + Schema. + +%% @doc Decodes a $ref URI token. Replacen the ~0 and ~1 according to RFC 6901, +%% run URI decode, then go back to binary. +%% @private +decode_path_element(Token) -> + String = binary:bin_to_list(Token), + String1 = http_uri:decode(String), + String2 = re:replace(String1, "~0", "\~", [global,{return,list}]), + String3 = re:replace(String2, "~1", "/", [global,{return,list}]), + binary:list_to_bin(String3). + + %% @doc 5.1. type %% %% This attribute defines what the primitive type or the schema of the diff --git a/test/jesse_tests_draft3_SUITE.erl b/test/jesse_tests_draft3_SUITE.erl index 153293ed..f8cd89b7 100644 --- a/test/jesse_tests_draft3_SUITE.erl +++ b/test/jesse_tests_draft3_SUITE.erl @@ -44,7 +44,7 @@ , pattern/1 , patternProperties/1 , properties/1 - %% , ref/1 + , ref/1 , required/1 , type/1 , uniqueItems/1 @@ -79,7 +79,7 @@ all() -> , pattern , patternProperties , properties - %% , ref + , ref , required , type , uniqueItems @@ -180,11 +180,10 @@ properties(Config) -> Specs = ?config(Key, 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",