Packages

jesse (JSon Schema Erlang) is an implementation of a JSON Schema validator for Erlang.

Current section

Files

Jump to
shiguredo_jesse src jesse_state.erl
Raw

src/jesse_state.erl

%%%=============================================================================
%% Copyright 2012- Klarna AB
%% Copyright 2015- AUTHORS
%% Copyright 2023- Shiguredo Inc.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% @doc Json schema validation module.
%%
%% This module is the core of jesse, it implements the validation functionality
%% according to the standard.
%% @end
%%%=============================================================================
-module(jesse_state).
%% API
-export([add_to_path/2,
get_allowed_errors/1,
get_external_validator/1,
get_current_path/1,
get_current_schema/1,
get_current_schema_id/1,
get_default_schema_ver/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,
resolve_ref/2,
undo_resolve_ref/2,
canonical_path/2,
combine_id/2]).
-export_type([state/0]).
%% Includes
-include("jesse_schema_validator.hrl").
%% Internal datastructures
-record(state, {
allowed_errors :: jesse:allowed_errors(),
current_path :: current_path(),
current_schema :: jesse:schema(),
default_schema_ver :: jesse:schema_ver(),
error_handler :: jesse:error_handler(),
error_list :: jesse:error_list(),
external_validator :: jesse:external_validator(),
id :: jesse:schema_id(),
root_schema :: jesse:schema(),
schema_loader_fun :: jesse:schema_loader_fun()
}).
%% current path in reversed order
-type current_path() :: [current_path_item()].
-type current_path_item() :: binary() | non_neg_integer().
-opaque state() :: #state{}.
%%% API
%% @doc Adds `Property' to the `current_path' in `State'.
-spec add_to_path(State :: state(),
Item :: current_path_item()) -> state().
add_to_path(State, Item) ->
CurrentPath = State#state.current_path,
State#state{current_path = [Item | CurrentPath]}.
%% @doc Getter for `allowed_errors'.
-spec get_allowed_errors(State :: state()) -> jesse:allowed_errors().
get_allowed_errors(#state{allowed_errors = AllowedErrors}) ->
AllowedErrors.
%% @doc Getter for `current_path'.
-spec get_current_path(State :: state()) -> current_path().
get_current_path(#state{current_path = CurrentPath}) ->
CurrentPath.
%% @doc Getter for `current_schema'.
-spec get_current_schema(State :: state()) -> jesse:schema().
get_current_schema(#state{current_schema = CurrentSchema}) ->
CurrentSchema.
%% @doc Getter for `current_schema_id'.
-spec get_current_schema_id(State :: state()) -> jesse:schema_id().
get_current_schema_id(#state{
current_schema = CurrentSchema,
root_schema = RootSchema
}) ->
Default = jesse_lib:get_schema_id(RootSchema),
jesse_lib:get_schema_id(CurrentSchema, Default).
%% @doc Getter for `default_schema_ver'.
-spec get_default_schema_ver(State :: state()) -> jesse:schema_ver().
get_default_schema_ver(#state{default_schema_ver = SchemaVer}) ->
SchemaVer.
%% @doc Getter for `error_handler'.
-spec get_error_handler(State :: state()) -> jesse:error_handler().
get_error_handler(#state{error_handler = ErrorHandler}) ->
ErrorHandler.
%% @doc Getter for `error_list'.
-spec get_error_list(State :: state()) -> jesse:error_list().
get_error_list(#state{error_list = ErrorList}) ->
ErrorList.
%% @doc Returns newly created state.
-spec new(JsonSchema :: jesse:schema(),
Options :: jesse:options()) -> state().
new(JsonSchema, Options) ->
AllowedErrors = proplists:get_value(allowed_errors,
Options,
0),
MetaSchemaVer = jesse_json_path:value(?SCHEMA,
JsonSchema,
?DEFAULT_SCHEMA_VER),
DefaultSchemaVer = proplists:get_value(default_schema_ver,
Options,
MetaSchemaVer),
ErrorHandler = proplists:get_value(error_handler,
Options,
?default_error_handler_fun),
ExternalValidator = proplists:get_value(external_validator,
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
},
set_current_schema(NewState, JsonSchema).
%% @doc Removes the last element from `current_path' in `State'.
-spec remove_last_from_path(State :: state()) -> state().
remove_last_from_path(State = #state{current_path = [_Property | Path]}) ->
State#state{current_path = Path}.
%% @doc Setter for `allowed_errors'.
-spec set_allowed_errors(State :: state(),
AllowedErrors :: jesse:allowed_errors()) -> state().
set_allowed_errors(#state{} = State, AllowedErrors) ->
State#state{allowed_errors = AllowedErrors}.
%% @doc Setter for `current_schema'.
-spec set_current_schema(State :: state(),
NewSchema :: jesse:schema()) -> state().
set_current_schema(#state{id = Id} = State, NewSchema0) ->
NewSchema =
case jesse_json_path:value(?REF, NewSchema0, undefined) of
undefined ->
NewSchema0;
Ref ->
%% Instead of just removing all the other fields, we put schema as
%% 1st element, so, only `ref' will be validated, while other fields
%% (say, `definitions') may still be referenced.
ListSchema = jesse_json_path:unwrap_value(NewSchema0),
[{?REF, Ref} | lists:keydelete(?REF, 1, ListSchema)]
end,
IdTag = case schema_from_json(NewSchema) of
?JSON_SCHEMA_DRAFT6 -> ?ID;
_ -> ?ID_OLD
end,
NewSchemaId = jesse_json_path:value(IdTag, NewSchema, undefined),
NewId = combine_id(Id, NewSchemaId),
State#state{current_schema = NewSchema, id = NewId}.
schema_from_json(JsonSchema) ->
case jesse_json_path:value(?SCHEMA, JsonSchema, ?NOT_FOUND) of
?NOT_FOUND -> ?DEFAULT_SCHEMA_VER;
SchemaVer -> SchemaVer
end.
%% @doc Setter for `error_list'.
-spec set_error_list(State :: state(),
ErrorList :: jesse:error_list()) -> state().
set_error_list(State, ErrorList) ->
State#state{error_list = ErrorList}.
%% @doc Resolve a reference.
-spec resolve_ref(State :: state(), Reference :: jesse:schema_ref()) -> state().
resolve_ref(State, Reference) ->
Id = State#state.id,
CanonicalReference = combine_id(Id, Reference),
[BaseURI | Pointer] = re:split(CanonicalReference,
<<$#>>,
[{return, binary}, unicode]),
IsLocalReference =
case Id of
undefined ->
BaseURI =:= <<"">>;
_ ->
binary_to_list(BaseURI) =:= Id
end,
{State1, BaseSchema} =
case IsLocalReference of
true ->
{State, State#state.root_schema};
false ->
case load_schema(State, BaseURI) of
?NOT_FOUND ->
jesse_error:handle_schema_invalid({?schema_not_found,
CanonicalReference},
State);
RemoteSchema ->
SchemaVer =
jesse_json_path:value(
?SCHEMA, RemoteSchema, State#state.default_schema_ver),
RemoteState = State#state{
root_schema = RemoteSchema,
id = BaseURI,
default_schema_ver = SchemaVer
},
{RemoteState, RemoteSchema}
end
end,
Path = jesse_json_path:parse(Pointer),
try
load_local_schema(set_current_schema(State1, BaseSchema), Path)
catch
throw:?NOT_FOUND ->
jesse_error:handle_schema_invalid({?schema_not_found,
CanonicalReference},
State1)
end.
%% @doc Revert changes made by resolve_reference.
-spec undo_resolve_ref(state(), state()) -> state().
undo_resolve_ref(RefState, OriginalState) ->
RefState#state{
root_schema = OriginalState#state.root_schema,
current_schema = OriginalState#state.current_schema,
default_schema_ver = OriginalState#state.default_schema_ver,
id = OriginalState#state.id
}.
%% @doc Retrieve a specific part of a schema
%% @private
-spec load_local_schema(Schema :: state() | ?NOT_FOUND,
Path :: [binary()]) -> not_found | jesse:json_term().
load_local_schema(St, []) ->
St;
load_local_schema(St, [<<>> | Keys]) ->
load_local_schema(St, Keys);
load_local_schema(St, [Key | Keys]) ->
Schema = get_current_schema(St),
SubSchema = jesse_json_path:value(Key, Schema, ?NOT_FOUND),
(SubSchema =/= ?NOT_FOUND) orelse throw(?NOT_FOUND),
{ObjectSubSchema, Keys1} =
case jesse_lib:is_json_object(SubSchema) of
true ->
{SubSchema, Keys};
false ->
choose_array(SubSchema, Keys)
end,
load_local_schema(
set_current_schema(St, ObjectSubSchema),
Keys1).
choose_array(SubSchema, [Key | Keys] = AllKeys) ->
case jesse_lib:is_array(SubSchema) of
true ->
try binary_to_integer(Key) of
Index ->
choose_array(lists:nth(Index + 1, SubSchema), Keys)
catch
_:_ -> throw(?NOT_FOUND)
end;
false ->
case jesse_lib:is_json_object(SubSchema) of
true ->
{SubSchema, AllKeys};
false ->
throw(?NOT_FOUND)
end
end;
choose_array(Schema, []) ->
{Schema, []}.
%% 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().
%% @doc Resolve a new id
%% @private
-spec combine_id(undefined | http_uri_uri(),
undefined | string() | binary()) -> http_uri_uri().
combine_id(Id, undefined) ->
Id;
combine_id(Id, RefBin) ->
case parse_ref(RefBin) of
{absolute, Ref} ->
Ref;
%% Relative
{relative, Ref} ->
combine_relative_id(Id, Ref)
end.
%% @doc Combine a relative id
%% @private
combine_relative_id(IdBin, RelId) when is_binary(IdBin) ->
combine_relative_id(unicode:characters_to_list(IdBin), RelId);
combine_relative_id(undefined, RelId) ->
RelId;
combine_relative_id(Id, "#" ++ Fragment) ->
[WithoutFragment | _] = re:split(Id, "#", [{return, list}]),
WithoutFragment ++ "#" ++ Fragment;
combine_relative_id(Id, RelId) ->
Base = filename:dirname(Id),
combine_relative_id2(Base, RelId).
%% @doc Combine a relative id
%% @private
combine_relative_id2("file:", RelId) ->
canonical_path(RelId, "file:");
combine_relative_id2("file://" ++ Path, RelId) ->
canonical_path(filename:join([Path, RelId]), "file:");
combine_relative_id2("http:", RelId) ->
canonical_path(RelId, "http:");
combine_relative_id2("http://" ++ Path, RelId) ->
canonical_path(Path ++ [$/, RelId], "http:");
combine_relative_id2("https:", RelId) ->
canonical_path(RelId, "https:");
combine_relative_id2("https://" ++ Path, RelId) ->
canonical_path(Path ++ [$/, RelId], "https:");
combine_relative_id2(".", RelId) ->
canonical_path(RelId, "file:");
combine_relative_id2(Path, RelId) ->
canonical_path(filename:join([Path, RelId]), "file:").
%% @doc Return a canonical URI path.
%% @private
canonical_path("file://" ++ Path, _) ->
"file://" ++ filename:join(raw_canonical_path(Path));
canonical_path(Path, "file:" ++ _) ->
"file://" ++ filename:join(raw_canonical_path(Path));
canonical_path("http://" ++ Path, _) ->
"http://" ++ string:join(raw_canonical_path(Path), "/");
canonical_path(Path, "http:" ++ _) ->
"http://" ++ string:join(raw_canonical_path(Path), "/");
canonical_path("https://" ++ Path, _) ->
"https://" ++ string:join(raw_canonical_path(Path), "/");
canonical_path(Path, "https:" ++ _) ->
"https://" ++ string:join(raw_canonical_path(Path), "/");
canonical_path(Path, _) ->
"file://" ++ filename:join(raw_canonical_path(filename:absname(Path))).
%% @doc Return a raw canonical path.
%% @private
raw_canonical_path(Path) ->
PathItems = re:split(Path, "\\\\|/", [{return, list}]),
raw_canonical_path2(PathItems, []).
%% @doc Return a raw canonical path.
%% @private
raw_canonical_path2([], Acc) ->
lists:reverse(Acc);
raw_canonical_path2([H | T], Acc) ->
case H of
"." ->
raw_canonical_path2(T, Acc);
".." ->
raw_canonical_path2(T, tl(Acc));
_ ->
raw_canonical_path2(T, [H | Acc])
end.
%% @doc Load a schema based on URI
-spec load_schema(State :: state(),
SchemaURI :: string() | binary()) -> jesse:schema() |
?NOT_FOUND.
load_schema(State, SchemaURI) when is_binary(SchemaURI) ->
load_schema(State, unicode:characters_to_list(SchemaURI));
load_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
_C:_E ->
?NOT_FOUND
end.
%% @private
get_external_validator(#state{external_validator = Fun}) ->
Fun.
%% @private
parse_ref(RefBin) ->
Ref = unicode:characters_to_list(RefBin),
case uri_string:parse(Ref) of
#{scheme := _} ->
{absolute, Ref};
_ ->
{relative, Ref}
end.