diff --git a/README.md b/README.md index 7520780f..efbf86c5 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,39 @@ ErrorType}` where * ErrorType is an atom such as `missing_id_field` or a tuple such as `{wrong_type_dependency, Dependency}`. +## Custom string format validators +Built-in format validators, like `ipv4`, `date-time` are often not enough for `string` type +and usage of pattern (regexps) is not convenient. + +Custom string format validator could be used with `{ext_format_validators, Validators}` option. +* `Validators` is a map of `CustomFormatName => ValidationFunction` pairs (`proplists` are also supported) +* `CustomFormatName` is a string that must be used in schema as a `format` value for `string` type +* `ValidationFunction` takes `string` value and must return `ok` or `error` atom indicating validation result + +Simple example: +```erlang +1> Schema = #{ +1> <<"type">> => <<"object">>, +1> <<"properties">> => #{ +1> <<"foo">> => #{ +1> <<"type">> => <<"string">>, +1> <<"format">> => <<"ipv4_and_port">> +1> } +1> } +1> }, +1> Validators = #{ +1> <<"ipv4_and_port">> => fun(<<"127.0.0.1:1234">>) -> ok; (_Else) -> error end +1> }, +1> Options = [{ext_format_validators, Validators}], +1> jesse:validate_with_schema(Schema, #{<<"foo">> => <<"127.0.0.1:1234">>}, Options). +{ok,#{<<"foo">> => <<"127.0.0.1:1234">>}} +2> jesse:validate_with_schema(Schema, #{<<"foo">> => <<"Hello, Joe!">>}, Options). +{error,[{data_invalid,#{<<"format">> => <<"ipv4_and_port">>, + <<"type">> => <<"string">>}, + wrong_format,<<"Hello, Joe!">>, + [<<"foo">>]}]} +``` + ## Caveats * pattern and patternProperty attributes: diff --git a/src/jesse.erl b/src/jesse.erl index 0bda4f5c..c9538183 100644 --- a/src/jesse.erl +++ b/src/jesse.erl @@ -41,6 +41,8 @@ , error_handler/0 , error_list/0 , external_validator/0 + , ext_format_validator/0 + , ext_format_validators/0 , json_term/0 , schema/0 , schema_id/0 @@ -71,7 +73,16 @@ -type external_validator() :: fun((json_term(), any()) -> any()) | undefined. -%% github.com/erlang/otp/blob/OTP-20.2.3/lib/inets/doc/src/http_uri.xml#L57 +-type ext_format_validator() :: fun((json_term()) -> ok | error). + +-ifndef(erlang_deprecated_types). +-type ext_format_validators() :: [{binary(), ext_format_validator()}] + | #{binary() => ext_format_validator()}. +-else. +-type ext_format_validators() :: [{binary(), ext_format_validator()}]. +-endif. + +%%github.com/erlang/otp/blob/OTP-20.2.3/lib/inets/doc/src/http_uri.xml#L57 -type http_uri_uri() :: string() | unicode:unicode_binary(). -type json_term() :: term(). @@ -95,6 +106,7 @@ | {default_schema_ver, schema_ver()} | {error_handler, error_handler()} | {external_validator, external_validator()} + | {ext_format_validators, ext_format_validators()} | {meta_schema_ver, schema_ver()} | {parser_fun, parser_fun()} | {schema_loader_fun, schema_loader_fun()}. diff --git a/src/jesse_state.erl b/src/jesse_state.erl index 95136b9b..59826deb 100644 --- a/src/jesse_state.erl +++ b/src/jesse_state.erl @@ -27,6 +27,7 @@ -export([ add_to_path/2 , get_allowed_errors/1 , get_external_validator/1 + , get_ext_format_validator/2 , get_current_path/1 , get_current_schema/1 , get_current_schema_id/1 @@ -59,6 +60,7 @@ , error_handler :: jesse:error_handler() , error_list :: jesse:error_list() , external_validator :: jesse:external_validator() + , ext_format_validators :: jesse:ext_format_validators() , id :: jesse:schema_id() , root_schema :: jesse:schema() , schema_loader_fun :: jesse:schema_loader_fun() @@ -142,18 +144,23 @@ new(JsonSchema, Options) -> ExternalValidator = proplists:get_value( external_validator , Options ), + ExtFormatValidators = proplists:get_value( ext_format_validators + , Options + , #{} + ), 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 - , default_schema_ver = DefaultSchemaVer - , schema_loader_fun = LoaderFun - , external_validator = ExternalValidator + NewState = #state{ root_schema = JsonSchema + , current_path = [] + , allowed_errors = AllowedErrors + , error_list = [] + , error_handler = ErrorHandler + , default_schema_ver = DefaultSchemaVer + , schema_loader_fun = LoaderFun + , external_validator = ExternalValidator + , ext_format_validators = ExtFormatValidators }, set_current_schema(NewState, JsonSchema). @@ -399,6 +406,21 @@ load_schema(#state{schema_loader_fun = LoaderFun}, SchemaURI) -> get_external_validator(#state{external_validator = Fun}) -> Fun. +-spec get_ext_format_validator(binary(), state()) -> + jesse:external_format_validator() | undefined. +-ifndef(erlang_deprecated_types). +get_ext_format_validator(Format, #state{ext_format_validators = Validators}) + when is_map(Validators) -> + maps:get(Format, Validators, undefined); +get_ext_format_validator(Format, #state{ext_format_validators = Validators}) + when is_list(Validators) -> + proplists:get_value(Format, Validators, undefined). +-else. +get_ext_format_validator(Format, #state{ext_format_validators = Validators}) + when is_list(Validators) -> + proplists:get_value(Format, Validators, undefined). +-endif. + %% @private -ifdef(OTP_RELEASE). %% OTP 21+ parse_ref(RefBin) -> diff --git a/src/jesse_validator_draft3.erl b/src/jesse_validator_draft3.erl index 2b32bbd9..63d08ab1 100644 --- a/src/jesse_validator_draft3.erl +++ b/src/jesse_validator_draft3.erl @@ -837,8 +837,8 @@ check_enum(Value, Enum, State) -> handle_data_invalid(?not_in_enum, Value, State) end. -check_format(_Value, _Format, State) -> - State. +check_format(Value, Format, State) -> + maybe_external_check_format(Value, Format, State). %% @doc 5.24. divisibleBy %% @@ -1052,3 +1052,14 @@ maybe_external_check_value(Value, State) -> Fun -> Fun(Value, State) end. + +maybe_external_check_format(Value, Format, State) -> + case jesse_state:get_ext_format_validator(Format, State) of + undefined -> State; + Fun when is_function(Fun, 1) -> + case Fun(Value) of + ok -> State; + error -> + handle_data_invalid(?wrong_format, Value, State) + end + end. \ No newline at end of file diff --git a/src/jesse_validator_draft4.erl b/src/jesse_validator_draft4.erl index f2085ea6..8355395c 100644 --- a/src/jesse_validator_draft4.erl +++ b/src/jesse_validator_draft4.erl @@ -980,8 +980,8 @@ check_format(Value, _Format = <<"ipv6">>, State) when is_binary(Value) -> check_format(Value, _Format = <<"uri">>, State) when is_binary(Value) -> %% not yet supported State; -check_format(_Value, _Format, State) -> - State. +check_format(Value, Format, State) -> + maybe_external_check_format(Value, Format, State). %% @doc 5.1.1. multipleOf %% @@ -1380,3 +1380,14 @@ maybe_external_check_value(Value, State) -> Fun -> Fun(Value, State) end. + +maybe_external_check_format(Value, Format, State) -> + case jesse_state:get_ext_format_validator(Format, State) of + undefined -> State; + Fun when is_function(Fun, 1) -> + case Fun(Value) of + ok -> State; + error -> + handle_data_invalid(?wrong_format, Value, State) + end + end. diff --git a/test/jesse_schema_validator_tests.erl b/test/jesse_schema_validator_tests.erl index 44c67cbd..5de7bde7 100644 --- a/test/jesse_schema_validator_tests.erl +++ b/test/jesse_schema_validator_tests.erl @@ -221,6 +221,57 @@ schema_unsupported_test() -> , jesse_schema_validator:validate(UnsupportedSchema, Json, []) ). +external_format_validator_test() -> + [ external_format_validator_test_draft(URI) + || URI <- [ <<"http://json-schema.org/draft-03/schema#">> + , <<"http://json-schema.org/draft-04/schema#">> + ] + ]. + +external_format_validator_test_draft(URI) -> + CustomFormatSchema = [ + {<<"type">>, <<"string">>}, + {<<"format">>, <<"ipv4_and_port">>} + ], + + Schema = {[ + {<<"$schema">>, URI}, + {<<"type">>, <<"object">>}, + {<<"properties">>, {[ + {<<"foo">>, CustomFormatSchema} + ]}} + ]}, + + Options = [{ + ext_format_validators, + [{ + <<"ipv4_and_port">>, + fun(<<"127.0.0.1:1234">>) -> ok; (_Else) -> error end + }] + }], + + ValidJson = {[ + {<<"foo">>, <<"127.0.0.1:1234">>} + ]}, + + ?assertEqual( + {ok, ValidJson}, + jesse_schema_validator:validate(Schema, ValidJson, Options) + ), + + InvalidJson = {[ + {<<"foo">>, <<"Hello, Joe!">>} + ]}, + + ?assertThrow([{ + data_invalid, + CustomFormatSchema, + wrong_format, + <<"Hello, Joe!">>, + [<<"foo">>] + }], + jesse_schema_validator:validate(Schema, InvalidJson, Options)). + -ifndef(erlang_deprecated_types). -ifndef(COMMON_TEST). % see Emakefile map_schema_test() -> @@ -270,6 +321,12 @@ map_data_test() -> ] ]. +map_external_format_validator_test() -> + [ map_external_format_validator_test_draft(URI) + || URI <- [ <<"http://json-schema.org/draft-03/schema#">> + , <<"http://json-schema.org/draft-04/schema#">> + ] + ]. map_data_test_draft(URI) -> Schema = {[ {<<"$schema">>, URI} @@ -325,5 +382,49 @@ map_data_test_draft(URI) -> , [{allowed_errors, infinity}] )). +map_external_format_validator_test_draft(URI) -> + CustomFormatSchema = #{ + <<"type">> => <<"string">>, + <<"format">> => <<"ipv4_and_port">> + }, + + Schema = #{ + <<"$schema">> => URI, + <<"type">> => <<"object">>, + <<"properties">> => #{ + <<"foo">> => CustomFormatSchema + } + }, + + Options = [{ + ext_format_validators, + #{ + <<"ipv4_and_port">> => + fun(<<"127.0.0.1:1234">>) -> ok; (_Else) -> error end + } + }], + + ValidJson = #{ + <<"foo">> => <<"127.0.0.1:1234">> + }, + ?assertEqual( + {ok, ValidJson}, + jesse_schema_validator:validate(Schema, ValidJson, Options) + ), + + InvalidJson = #{ + <<"foo">> => <<"Hello, Joe!">> + }, + + ?assertThrow([{ + data_invalid, + CustomFormatSchema, + wrong_format, + <<"Hello, Joe!">>, + [<<"foo">>] + }], + jesse_schema_validator:validate(Schema, InvalidJson, Options)). + -endif. -endif. +