Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A bit different approach to external validators plus validation against definition in a schema #53

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e21065f
add suppport for configurable and extendable validators
arentrue Mar 17, 2017
c774499
clarify schema validator interface
arentrue Mar 24, 2017
7ef3873
introduce jesse:validate_ref/2,3 interface
arentrue Mar 22, 2017
e2ce63d
fix undefined type http_uri
arentrue Mar 24, 2017
9ba3702
change validate_ref to validate_definition api
arentrue Apr 10, 2017
3910592
Add init opts to custom validator
arentrue May 21, 2017
0dffb4a
Merge remote-tracking branch 'upstream/master' into ft/upstream_merging
Mar 7, 2019
723e835
Merge pull request #5 from 4tyTwo/ft/upstream_merging
4tyTwo Mar 7, 2019
7640f47
Ucp support in regexes (#6)
kehitt Mar 16, 2020
f583a3b
Use github actions (#8)
Yozhig Mar 19, 2020
a21da06
MSPF-532: get rid of rfc3339 library (#7)
Yozhig Mar 19, 2020
600cc83
Add jesse:validate_local_ref, implement validate_defition over it (#9)
kehitt Apr 29, 2020
5bb91aa
Avoid deprecated http_uri functions for OTP 21+
shino Feb 28, 2020
2a0fa89
Cosmetics
shino Mar 4, 2020
287efa0
Use OTP_RELEASE macro
shino Mar 4, 2020
ba495bc
erlang.yml: erlang:23.2
dinama Jan 12, 2021
bbb58fc
Update src/jesse_json_path.erl
dinama Jan 12, 2021
9b980b7
Merge pull request #11 from rbkmoney/fix/otp23
dinama Jan 12, 2021
0bef37c
+merge upstream/1.6.1
dinama Aug 19, 2021
0953543
+fix after merge
dinama Aug 19, 2021
edf44ce
Merge pull request #12 from rbkmoney/merge_1.6.1
dinama Aug 19, 2021
fd4eee8
MSPF-532: avoid rfc3339
dinama Aug 19, 2021
df03b5c
Merge pull request #13 from rbkmoney/fx/avopi_rfc3339
dinama Aug 19, 2021
6ee802a
+avoid some diffs +avoid ci/erlang.yaml
dinama Aug 20, 2021
f4ff58e
Merge pull request #14 from rbkmoney/fx/ci
dinama Aug 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Andrei Neculau <[email protected]>
Stefan Strigler <[email protected]>
Sergey Prokhorov <[email protected]>
Yakov <[email protected]>
Anton Belyaev <[email protected]>
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,38 @@ ok
[<<"foo">>]}]}
```

* Validate an instanse against a particular definition from schema definitions

```erlang
1> Schema = <<"{\"definitions\": {\"Foo\": {\"properties\": {\"foo\": {\"type\": \"integer\"}}}, \"Bar\": {\"properties\": {\"bar\": {\"type\": \"boolean\"}}}}}">>.
<<"{\"definitions\": {\"Foo\": {\"properties\": {\"foo\": {\"type\": \"integer\"}}}, \"Bar\": {\"properties\": {\"bar\": {\"type\": \"boolea"...>>
2> jesse:validate_definition("Foo",
2> Schema,
2> <<"{\"foo\": 1}">>,
2> [{parser_fun, fun jiffy:decode/1}]).
{ok,[{<<"foo">>,1}]}
3> jesse:validate_definition("Bar",
3> Schema,
3> <<"{\"bar\": 2}">>,
3> [{parser_fun, fun jiffy:decode/1}]).
{error,[{data_invalid,[{<<"type">>,<<"boolean">>}],
wrong_type,2,
[<<"bar">>]}]}
4> jesse:validate_definition("FooBar",
4> Schema,
4> <<"{\"bar\": 2}">>,
4> [{parser_fun, fun jiffy:decode/1}]).
{error,[{schema_invalid,[{<<"definitions">>,
[{<<"Foo">>,
[{<<"properties">>,
[{<<"foo">>,[{<<"type">>,<<"integer">>}]}]}]},
{<<"Bar">>,
[{<<"properties">>,
[{<<"bar">>,[{<<"type">>,<<"boolean">>}]}]}]}]}],
{schema_not_found,"#/definitions/FooBar"}}]}
```


* Since 0.4.0 it's possible to instruct jesse to collect errors, and not stop
immediately when it finds an error in the given JSON instance:

Expand Down Expand Up @@ -245,6 +277,11 @@ the given schema), one should use 'default_schema_ver' option when call
a binary consisting a schema path,
i.e. <<"http://json-schema.org/draft-03/schema#">>.

It is also possible to specify a validator module to use via `validator` option.
This option supersedes the mechanism with the $schema property described above.
Custom validator module can be specified as well. Such module should implement
`jesse_schema_validator` behaviour.

## Validation errors

The validation functions `jesse:validate/2` and `jesse:validate_with_schema/2,3`
Expand Down
40 changes: 40 additions & 0 deletions src/jesse.erl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
, load_schemas/3
, validate/2
, validate/3
, validate_definition/3
, validate_definition/4
, validate_with_schema/2
, validate_with_schema/3
]).
Expand Down Expand Up @@ -150,6 +152,44 @@ validate(Schema, Data, Options) ->
throw:Error -> {error, Error}
end.

%% @doc Equivalent to {@link validate_definition/4} where `Options' is an empty list.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Elvis:

Line 155 is too long: %% @doc Equivalent to {@link validate_definition/4} where `Options' is an empty list..

-spec validate_definition( Definition :: string()
, Schema :: json_term() | binary()
, Data :: json_term() | binary()
) -> {ok, json_term()}
| jesse_error:error().
validate_definition(Definition, Schema, Data) ->
validate_definition(Definition, Schema, Data, []).

%% @doc Validates json `Data' agains the given `Definition' in the given
%% schema `Schema', using `Options'.
%% If the given json is valid, then it is returned to the caller, otherwise
%% an error with an appropriate error reason is returned. If the `parser_fun'
%% option is provided, then both `Schema' and `Data' are considered to be a
%% binary string, so `parser_fun' is used to convert both binary strings to a
%% supported internal representation of json.
%% If `parser_fun' is not provided, then both `Schema' and `Data' are considered
%% to already be a supported internal representation of json.
-spec validate_definition( Definition :: string()
, Schema :: json_term() | binary()
, Data :: json_term() | binary()
, Options :: [{Key :: atom(), Data :: any()}]
) -> {ok, json_term()}
| jesse_error:error().
validate_definition(Defintion, Schema, Data, Options) ->
try
ParserFun = proplists:get_value(parser_fun, Options, fun(X) -> X end),
ParsedSchema = try_parse(schema, ParserFun, Schema),
ParsedData = try_parse(data, ParserFun, Data),
jesse_schema_validator:validate_definition( Defintion
, ParsedSchema
, ParsedData
, Options
)
catch
throw:Error -> {error, Error}
end.

%% @doc Equivalent to {@link validate_with_schema/3} where `Options'
%% is an empty list.
-spec validate_with_schema( Schema :: json_term() | binary()
Expand Down
89 changes: 72 additions & 17 deletions src/jesse_schema_validator.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,29 @@

%% API
-export([ validate/3
, validate_definition/4
, validate_with_state/3
]).

%% Includes
-include("jesse_schema_validator.hrl").

%% Behaviour definition
-callback check_value(Value, Attr, State) ->
State | no_return()
when
Value :: any(),
Attr :: {binary(), jesse:json_term()},
State :: jesse_state:state().

-callback init_state(Opts :: jesse_state:validator_opts()) ->
validator_state().

-type validator_state() :: any().

-export_type([ validator_state/0
]).

%%% API
%% @doc Validates json `Data' against `JsonSchema' with `Options'.
%% If the given json is valid, then it is returned to the caller as is,
Expand All @@ -45,6 +62,21 @@ validate(JsonSchema, Value, Options) ->
NewState = validate_with_state(JsonSchema, Value, State),
{result(NewState), Value}.

%% @doc Validates json `Data' against `Definition' in `JsonSchema' with `Options'.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Elvis:

Line 65 is too long: %% @doc Validates json Data' against Definition' in JsonSchema' with Options'..

%% If the given json is valid, then it is returned to the caller as is,
%% otherwise an exception will be thrown.
-spec validate_definition( Definition :: string()
, JsonSchema :: jesse:json_term()
, Data :: jesse:json_term()
, Options :: [{Key :: atom(), Data :: any()}]
) -> {ok, jesse:json_term()}
| no_return().
validate_definition(Defintion, JsonSchema, Value, Options) ->
State = jesse_state:new(JsonSchema, Options),
Schema = make_definition_ref(Defintion),
NewState = validate_with_state(Schema, Value, State),
{result(NewState), Value}.

%% @doc Validates json `Data' against `JsonSchema' with `State'.
%% If the given json is valid, then the latest state is returned to the caller,
%% otherwise an exception will be thrown.
Expand All @@ -53,11 +85,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
Expand All @@ -76,18 +128,21 @@ result(State) ->
_ -> throw(ErrorList)
end.

%% @doc Runs appropriate validator depending on schema version
%% it is called with.
%% @doc Goes through attributes of the given `JsonSchema' and
%% validates the `Value' against them calling `Validator'.
%% @private
select_and_run_validator(?json_schema_draft3, JsonSchema, Value, State) ->
jesse_validator_draft3:check_value( Value
, jesse_json_path:unwrap_value(JsonSchema)
, State
);
select_and_run_validator(?json_schema_draft4, JsonSchema, Value, State) ->
jesse_validator_draft4:check_value( Value
, jesse_json_path:unwrap_value(JsonSchema)
, State
);
select_and_run_validator(SchemaURI, _JsonSchema, _Value, State) ->
jesse_error:handle_schema_invalid({?schema_unsupported, SchemaURI}, State).
run_validator(_Validator, _Value, [], State) ->
State;
run_validator(Validator, Value, [Attr | Attrs], State0) ->
State = Validator:check_value( Value
, Attr
, State0
),
run_validator(Validator, Value, Attrs, State).

%% @doc Makes a $ref schema object pointing to the given `Definition'
%% in schema defintions.
%% @private
make_definition_ref(Definition) ->
Definition1 = list_to_binary(Definition),
[{<<"$ref">>, <<"#/definitions/", Definition1/binary>>}].
62 changes: 55 additions & 7 deletions src/jesse_state.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,32 @@
, 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
, remove_last_from_path/1
, set_allowed_errors/2
, set_current_schema/2
, set_error_list/2
, set_validator_state/2
, resolve_ref/2
, undo_resolve_ref/2
, canonical_path/2
, combine_id/2
]).

-export_type([ state/0
, validator_opts/0
]).

%% Includes
-include("jesse_schema_validator.hrl").

%% Internal datastructures
-type http_uri() :: string().

-record( state
, { root_schema :: jesse:json_term()
, current_schema :: jesse:json_term()
Expand All @@ -62,18 +68,22 @@
, non_neg_integer()
) -> list() | no_return()
)
, validator :: module() | 'undefined'
, validator_state :: any() | 'undefined'
, default_schema_ver :: binary()
, schema_loader_fun :: fun(( string()
) -> {ok, jesse:json_term()} |
jesse:json_term() |
?not_found
)
, id :: http_uri:uri() | 'undefined'
, id :: http_uri() | 'undefined'
}
).

-opaque state() :: #state{}.

-type validator_opts() :: any().

%%% API
%% @doc Adds `Property' to the `current_path' in `State'.
-spec add_to_path(State :: state(),
Expand Down Expand Up @@ -110,6 +120,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()) -> any() | undefined.
get_validator_state(#state{validator_state = ValidatorState}) ->
ValidatorState.

%% @doc Getter for `error_handler'.
-spec get_error_handler(State :: state()) -> fun(( jesse_error:error_reason()
, [jesse_error:error_reason()]
Expand Down Expand Up @@ -144,15 +164,28 @@ new(JsonSchema, Options) ->
, Options
, MetaSchemaVer
),
LoaderFun = proplists:get_value( schema_loader_fun
, 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
),
LoaderFun = proplists:get_value( schema_loader_fun
, Options
, ?default_schema_loader_fun
),
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
},
Expand Down Expand Up @@ -183,6 +216,11 @@ set_current_schema(#state{id = Id} = State, NewSchema) ->
set_error_list(State, ErrorList) ->
State#state{error_list = ErrorList}.

%% @doc Setter for `validator_state'.
-spec set_validator_state(State :: state(), ValidatorState :: any()) -> state().
set_validator_state(State, ValidatorState) ->
State#state{validator_state = ValidatorState}.

%% @doc Resolve a reference.
-spec resolve_ref(State :: state(), Reference :: binary()) -> state().
resolve_ref(State, Reference) ->
Expand Down Expand Up @@ -238,6 +276,16 @@ undo_resolve_ref(RefState, OriginalState) ->
, id = OriginalState#state.id
}.

%% @doc Init custom validator state.
%% @private
-spec init_validator_state( Validator :: module() | undefined
, Opts :: validator_opts()
) -> jesse_schema_validator:validator_state().
init_validator_state(undefined, _) ->
undefined;
init_validator_state(Validator, Opts) ->
Validator:init_state(Opts).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Elvis:

Remove the dynamic function call on line 287. Only modules that define callbacks should make dynamic calls.


%% @doc Retrieve a specific part of a schema
%% @private
-spec load_local_schema( Schema :: not_found | jesse:json_term()
Expand Down Expand Up @@ -277,8 +325,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) ->
Expand Down
Loading