%- % Copyright (c) 2012-2014 Yakaz % Copyright (c) 2016 Jean-Sébastien Pédron % 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).