Current section
Files
Jump to
Current section
Files
src/yamerl_constr.erl
%-
% Copyright (c) 2012-2014 Yakaz
% Copyright (c) 2016 Jean-Sébastien Pédron <jean-sebastien.pedron@dumbbell.fr>
% All rights reserved.
%
% Redistribution and use in source and binary forms, with or without
% modification, are permitted provided that the following conditions
% are met:
% 1. Redistributions of source code must retain the above copyright
% notice, this list of conditions and the following disclaimer.
% 2. Redistributions in binary form must reproduce the above copyright
% notice, this list of conditions and the following disclaimer in the
% documentation and/or other materials provided with the distribution.
%
% THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
% ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
% ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
% FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
% DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
% OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
% LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
% OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
% SUCH DAMAGE.
-module(yamerl_constr).
-include("yamerl_errors.hrl").
-include("yamerl_tokens.hrl").
-include("yamerl_nodes.hrl").
-include("internal/yamerl_constr.hrl").
%% Public API.
-export([
new/1,
new/2,
string/1,
string/2,
file/1,
file/2,
next_chunk/3,
next_chunk/2,
last_chunk/2,
get_pres_details/1,
node_line/1,
node_column/1,
option_names/0
]).
%% -------------------------------------------------------------------
%% Exported types.
%% -------------------------------------------------------------------
%% FIXME:
%% This type should be "-opaque". However, up-to Erlang R15B03, an issue
%% with either this code or Dialyzer prevents us from declaring it
%% properly: Dialyzer reports warning regarding the stream_state_fun()
%% type and several guard expression which will never match.
-type yamerl_constr() :: #yamerl_constr{}.
-export_type([
yamerl_constr/0,
yamerl_constr_option/0,
yamerl_node/0,
yamerl_seq/0,
yamerl_map/0,
yamerl_str/0,
yamerl_null/0,
yamerl_bool/0,
yamerl_int/0,
yamerl_float/0,
yamerl_timestamp/0,
yamerl_erlang_atom/0,
yamerl_erlang_fun/0,
yamerl_user_node/0,
yamerl_doc/0,
yamerl_simple_node/0,
yamerl_simple_seq/0,
yamerl_simple_map/0,
yamerl_simple_str/0,
yamerl_simple_null/0,
yamerl_simple_bool/0,
yamerl_simple_int/0,
yamerl_simple_float/0,
yamerl_simple_timestamp/0,
yamerl_simple_erlang_atom/0,
yamerl_simple_erlang_fun/0,
yamerl_user_simple_node/0,
yamerl_simple_doc/0
]).
%% -------------------------------------------------------------------
%% Public API: chunked stream scanning.
%% -------------------------------------------------------------------
-spec new(Source) ->
Constr | no_return() when
Source :: term(),
Constr :: yamerl_parser:yamerl_parser().
new(Source) ->
new(Source, []).
-spec new(Source, Options) ->
Constr | no_return() when
Source :: term(),
Options :: [
yamerl_constr_option() |
yamerl_parser:yamerl_parser_option() |
proplists:property()
],
Constr :: yamerl_parser:yamerl_parser().
new(Source, Options) ->
Parser_Options = initialize(Options),
yamerl_parser:new(Source, Parser_Options).
-spec next_chunk(Constr, Chunk) ->
Ret | no_return() when
Constr :: yamerl_parser:yamerl_parser(),
Chunk :: unicode_binary(),
Ret :: {continue, New_Constr},
New_Constr :: yamerl_parser:yamerl_parser().
next_chunk(Constr, Chunk) ->
next_chunk(Constr, Chunk, false).
-spec next_chunk(Constr, Chunk, false) ->
Ret | no_return() when
Constr :: yamerl_parser:yamerl_parser(),
Chunk :: unicode_binary(),
Ret :: {continue, New_Constr},
New_Constr :: yamerl_parser:yamerl_parser();
(Constr, Chunk, true) ->
Result | no_return() when
Constr :: yamerl_parser:yamerl_parser(),
Chunk :: unicode_binary(),
Result :: [yamerl_doc()]
| [yamerl_simple_doc()].
next_chunk(Constr, Chunk, EOS) ->
Ret = yamerl_parser:next_chunk(Constr, Chunk, EOS),
if
EOS -> get_docs(Ret);
true -> Ret
end.
-spec last_chunk(Constr, Chunk) ->
Result | no_return() when
Constr :: yamerl_parser:yamerl_parser(),
Chunk :: unicode_binary(),
Result :: [yamerl_doc()]
| [yamerl_simple_doc()].
last_chunk(Constr, Chunk) ->
next_chunk(Constr, Chunk, true).
-spec get_docs(Constr) ->
Docs | no_return() when
Constr :: yamerl_parser:yamerl_parser(),
Docs :: [yamerl_doc()]
| [yamerl_simple_doc()].
get_docs(Constr) ->
case yamerl_parser:get_token_fun(Constr) of
Not_Fun when Not_Fun == acc orelse Not_Fun == drop ->
Error = #yamerl_parsing_error{
name = token_fun_cleared
},
yamerl_errors:throw(Error);
Token_Fun ->
Token_Fun(get_docs)
end.
%% -------------------------------------------------------------------
%% Public API: common stream sources.
%% -------------------------------------------------------------------
-spec string(String) ->
Result | no_return() when
String :: unicode_data(),
Result :: [yamerl_doc()]
| [yamerl_simple_doc()]
| term().
string(String) ->
string(String, []).
-spec string(String, Options) ->
Result | no_return() when
String :: unicode_data(),
Options :: [ yamerl_parser:yamerl_parser_option()
| yamerl_constr_option()
| proplists:property()],
Result :: [yamerl_doc()]
| [yamerl_simple_doc()]
| term().
string(String, Options) ->
Parser_Options = initialize(Options),
Constr = yamerl_parser:string(String, Parser_Options),
get_docs(Constr).
-spec file(Filename) ->
Result | no_return() when
Filename :: string(),
Result :: [yamerl_doc()]
| [yamerl_simple_doc()]
| term().
file(Filename) ->
file(Filename, []).
-spec file(Filename, Options) ->
Result | no_return() when
Filename :: string(),
Options :: [ yamerl_parser:yamerl_parser_option()
| yamerl_constr_option()
| proplists:property()],
Result :: [yamerl_doc()]
| [yamerl_simple_doc()]
| term().
file(Filename, Options) ->
Parser_Options = initialize(Options),
Constr = yamerl_parser:file(Filename, Parser_Options),
get_docs(Constr).
%% -------------------------------------------------------------------
%% Presentation details.
%% -------------------------------------------------------------------
get_pres_details(Token) ->
Line = ?TOKEN_LINE(Token),
Column = ?TOKEN_COLUMN(Token),
[{line, Line}, {column, Column}].
%% -------------------------------------------------------------------
%% Node informations.
%% -------------------------------------------------------------------
node_line(Node) ->
case node_pres(Node) of
undefined -> undefined;
Pres -> proplists:get_value(line, Pres)
end.
node_column(Node) ->
case node_pres(Node) of
undefined -> undefined;
Pres -> proplists:get_value(column, Pres)
end.
node_pres(Node) when
is_record(Node, yamerl_seq) orelse
is_record(Node, yamerl_map) orelse
is_record(Node, yamerl_str) orelse
is_record(Node, yamerl_null) orelse
is_record(Node, yamerl_bool) orelse
is_record(Node, yamerl_int) orelse
is_record(Node, yamerl_timestamp) orelse
is_record(Node, yamerl_erlang_atom) orelse
is_record(Node, yamerl_erlang_fun) ->
?NODE_PRES(Node);
node_pres(Node) when is_tuple(Node) ->
%% For user-defined nodes, we call the module responsible for it.
Mod = ?NODE_MOD(Node),
try
Mod:node_pres(Node)
catch
error:undef ->
undefined
end.
%% -------------------------------------------------------------------
%% Construction.
%% -------------------------------------------------------------------
construct(Constr, #yamerl_doc_start{}) ->
%% Prepare a document node.
Doc = #yamerl_doc{},
Constr1 = Constr#yamerl_constr{
current_doc = [Doc]
},
return_new_fun(Constr1);
construct(_, Token) when
is_record(Token, yamerl_stream_start) orelse
is_record(Token, yamerl_stream_end) orelse
is_record(Token, yamerl_yaml_directive) orelse
is_record(Token, yamerl_tag_directive) orelse
is_record(Token, yamerl_reserved_directive) orelse
is_record(Token, yamerl_doc_end) ->
%% This token doesn't start a node: ignore it.
ok;
construct(
#yamerl_constr{current_doc = Doc, current_node_is_leaf = false,
mods = Mods, tags = Tags} = Constr,
Token) when Doc /= undefined andalso
(is_record(Token, yamerl_collection_start) orelse
is_record(Token, yamerl_scalar)) ->
%% This token starts a node. We must determine the module to use to
%% construct this node.
Tag = case Token of
#yamerl_collection_start{tag = T} -> T;
#yamerl_scalar{tag = T} -> T
end,
Ret = case Tag of
#yamerl_tag{uri = {non_specific, _}} ->
%% The node has a non-specific tag. We let each module
%% decides if they want to construct the node.
try_construct(Constr, Mods, Token);
#yamerl_tag{uri = URI} ->
%% We look up this URI in the tag's index.
case proplists:get_value(URI, Tags) of
Mod when Mod /= undefined ->
Mod:construct_token(Constr, undefined, Token);
undefined ->
%% This tag isn't handled by anything!
Error = #yamerl_parsing_error{
name = unrecognized_node,
token = Tag,
line = ?TOKEN_LINE(Tag),
column = ?TOKEN_COLUMN(Tag)
},
Error1 = yamerl_errors:format(Error,
"Tag \"~s\" unrecognized by any module", [URI]),
yamerl_errors:throw(Error1)
end
end,
handle_construct_return(Constr, Doc, Ret);
construct(
#yamerl_constr{current_doc = Doc, current_node_is_leaf = false} = Constr,
#yamerl_anchor{name = Anchor}) when Doc /= undefined ->
handle_construct_return(Constr, Doc, #node_anchor{name = Anchor});
construct(
#yamerl_constr{current_doc = Doc, anchors = Anchors} = Constr,
#yamerl_alias{name = Alias} = Token) when Doc /= undefined ->
try
Node = dict:fetch(Alias, Anchors),
handle_construct_return(Constr, Doc, {finished, Node})
catch
_:_ ->
%% This alias references a non-existent anchor!
Error = #yamerl_parsing_error{
name = no_matching_anchor,
token = Token,
line = ?TOKEN_LINE(Token),
column = ?TOKEN_COLUMN(Token)
},
Error1 = yamerl_errors:format(Error,
"No anchor corresponds to alias \"~s\"", [Alias]),
yamerl_errors:throw(Error1)
end;
construct(
#yamerl_constr{current_doc =
[#unfinished_node{module = Mod} = Node | Doc]} = Constr,
Token) ->
%% This token continues a node. We call the current node's module to
%% handle it.
Ret = Mod:construct_token(Constr, Node, Token),
handle_construct_return(Constr, Doc, Ret).
try_construct(Constr, [Mod | Rest], Token) ->
case Mod:try_construct_token(Constr, undefined, Token) of
unrecognized -> try_construct(Constr, Rest, Token);
Ret -> Ret
end;
try_construct(_, [], Token) ->
Error = #yamerl_parsing_error{
name = unrecognized_node,
token = Token,
text = "No module found to handle node",
line = ?TOKEN_LINE(Token),
column = ?TOKEN_COLUMN(Token)
},
yamerl_errors:throw(Error).
construct_parent(#yamerl_constr{anchors = Anchors} = Constr,
[#node_anchor{name = Anchor} | Doc], Child) ->
Anchors1 = dict:store(Anchor, Child, Anchors),
Constr1 = Constr#yamerl_constr{
anchors = Anchors1
},
construct_parent(Constr1, Doc, Child);
construct_parent(#yamerl_constr{docs = Docs, docs_count = Count} = Constr,
[#yamerl_doc{} = Doc], Root) ->
%% This node is the root of the document.
Doc1 = Doc#yamerl_doc{
root = Root
},
Constr1 = Constr#yamerl_constr{
docs = Docs ++ [Doc1],
docs_count = Count + 1,
current_doc = undefined,
current_node_is_leaf = false,
anchors = dict:new()
},
return_new_fun(Constr1);
construct_parent(Constr, [#unfinished_node{module = Mod} = Node | Doc],
Child) ->
%% We call the parent node's module to handle this new child node.
Ret = Mod:construct_node(Constr, Node, Child),
handle_construct_return(Constr, Doc, Ret).
handle_construct_return(Constr, Doc, {finished, Node}) ->
%% Give this node to the parent node.
construct_parent(Constr, Doc, Node);
handle_construct_return(Constr, Doc, {unfinished, Node, Is_Leaf}) ->
%% Unfinished node, wait for the next tokens.
Constr1 = Constr#yamerl_constr{
current_doc = [Node | Doc],
current_node_is_leaf = Is_Leaf
},
return_new_fun(Constr1);
handle_construct_return(Constr, Doc, #node_anchor{} = Anchor) ->
%% Anchor before a (not-yet-started) node, wait this node.
Constr1 = Constr#yamerl_constr{
current_doc = [Anchor | Doc]
},
return_new_fun(Constr1).
return_new_fun(#yamerl_constr{detailed_constr = Detailed} = Constr) ->
Fun = fun
(get_docs) when not Detailed ->
[Doc#yamerl_doc.root || Doc <- Constr#yamerl_constr.docs];
(get_docs) ->
Constr#yamerl_constr.docs;
(get_constr) ->
Constr;
(T) ->
construct(Constr, T)
end,
{ok, Fun}.
%% -------------------------------------------------------------------
%% Node modules.
%% -------------------------------------------------------------------
setup_node_mods(Constr) ->
Mods1 = umerge_unsorted(
proplists:get_value(node_mods, Constr#yamerl_constr.options, []),
yamerl_app:get_param(node_mods)
),
Schema = proplists:get_value(schema, Constr#yamerl_constr.options, core),
Mods = case Schema of
failsafe -> umerge_unsorted(Mods1, ?FAILSAFE_SCHEMA_MODS);
json -> umerge_unsorted(Mods1, ?JSON_SCHEMA_MODS);
core -> umerge_unsorted(Mods1, ?CORE_SCHEMA_MODS);
yaml11 -> umerge_unsorted(Mods1, ?YAML11_SCHEMA_MODS)
end,
Auto = filter_autodetection_capable_mods(Mods, []),
Tags = index_tags(Mods, []),
Constr1 = Constr#yamerl_constr{
mods = Auto,
tags = Tags
},
return_new_fun(Constr1).
umerge_unsorted(List1, List2) ->
Fun = fun(Mod, List) ->
case lists:member(Mod, List) of
true -> List;
false -> List ++ [Mod]
end
end,
lists:foldl(Fun, List1, List2).
filter_autodetection_capable_mods([Mod | Rest], Auto) ->
catch Mod:module_info(),
Auto1 = case erlang:function_exported(Mod, try_construct_token, 3) of
true -> [Mod | Auto];
false -> Auto
end,
filter_autodetection_capable_mods(Rest, Auto1);
filter_autodetection_capable_mods([], Auto) ->
lists:reverse(Auto).
index_tags([Mod | Rest], Tags) ->
try
Tags1 = index_tags2(Tags, Mod:tags(), Mod),
index_tags(Rest, Tags1)
catch
_:_ ->
index_tags(Rest, Tags)
end;
index_tags([], Tags) ->
Tags.
index_tags2(Tags, [Tag | Rest], Mod) ->
Tags1 = case lists:keymember(Tag, 1, Tags) of
false -> [{Tag, Mod} | Tags];
true -> Tags
end,
index_tags2(Tags1, Rest, Mod);
index_tags2(Tags, [], _) ->
Tags.
%% -------------------------------------------------------------------
%% Internal functions.
%% -------------------------------------------------------------------
initialize(Options) ->
Options0 = proplists:unfold(Options),
{Constr_Options, Parser_Options, Ext_Options} = filter_options(Options0),
check_options(Constr_Options),
Detailed = proplists:get_value(detailed_constr, Constr_Options, false),
Constr = #yamerl_constr{
options = Constr_Options,
ext_options = Ext_Options,
detailed_constr = Detailed
},
{ok, Token_Fun} = setup_node_mods(Constr),
[{token_fun, Token_Fun} | Parser_Options].
filter_options(Options) ->
Constr_Option_Names = option_names(),
Parser_Option_Names = yamerl_parser:option_names(),
filter_options2(Options, Constr_Option_Names, Parser_Option_Names,
[], [], []).
filter_options2([{Name, _} = Option | Rest],
Constr_Option_Names, Parser_Option_Names,
Constr_Options, Parser_Options, Ext_Options) ->
case lists:member(Name, Constr_Option_Names) of
true ->
filter_options2(Rest,
Constr_Option_Names, Parser_Option_Names,
[Option | Constr_Options], Parser_Options, Ext_Options);
false ->
case lists:member(Name, Parser_Option_Names) of
true ->
filter_options2(Rest,
Constr_Option_Names, Parser_Option_Names,
Constr_Options, [Option | Parser_Options], Ext_Options);
false ->
filter_options2(Rest,
Constr_Option_Names, Parser_Option_Names,
Constr_Options, Parser_Options, [Option | Ext_Options])
end
end;
filter_options2([], _, _, Constr_Options, Parser_Options, Ext_Options) ->
{
lists:reverse(Constr_Options),
lists:reverse(Parser_Options),
lists:reverse(Ext_Options)
}.
option_names() ->
[
node_mods,
schema,
detailed_constr
].
check_options([Option | Rest]) ->
case is_option_valid(Option) of
true -> check_options(Rest);
false -> invalid_option(Option)
end;
check_options([]) ->
ok.
is_option_valid({detailed_constr, Flag}) when is_boolean(Flag) ->
true;
is_option_valid({node_mods, Mods}) when is_list(Mods) ->
Fun = fun(Mod) ->
not yamerl_app:is_node_mod(Mod)
end,
case lists:filter(Fun, Mods) of
[] -> true;
_ -> false
end;
is_option_valid({schema, Schema})
when Schema == failsafe
orelse Schema == json
orelse Schema == core
orelse Schema == yaml11 ->
true;
is_option_valid(_) ->
false.
invalid_option(Option) ->
Error = #yamerl_invalid_option{
option = Option
},
Error1 = case Option of
{detailed_constr, _} ->
Error#yamerl_invalid_option{
text = "Invalid value for option \"detailed_constr\": "
"it must be a boolean"
};
{node_mods, _} ->
Error#yamerl_invalid_option{
text = "Invalid value for option \"node_mods\": "
"it must be a list of modules"
};
_ ->
yamerl_errors:format(Error, "Unknown option \"~w\"", [Option])
end,
yamerl_errors:throw(Error1).