-module(xml_builderl).
-export([document/1, document/2, document/3, doctype/2, elem/1, elem/2, elem/3]).
-export([generate/1, generate/2, generate_iodata/1, generate_iodata/2, from_map/1]).
-define(IS_BLANK_MAP(M), ((M == undefined) or (M == #{}))).
-define(IS_BLANK_LIST(L), ((L == undefined) or (L == []))).
-define(IS_BLANK_ATTRS(A), (?IS_BLANK_MAP(A) or ?IS_BLANK_LIST(A))).
document(Elems) ->
[xml_decl | wrap(elems_with_prolog(Elems))].
document(Name, AttrsOrContent) ->
[xml_decl | [elem(Name, AttrsOrContent)]].
document(Name, Attrs, Content) ->
[xml_decl | [elem(Name, Attrs, Content)]].
%% Chose elem instead of element since element is a BIF
elem(Name) when is_binary(Name) ->
elem({undefined, undefined, Name});
elem({iodata, _IoData} = IoData) ->
elem({undefined, undefined, IoData});
elem(Name) when is_binary(Name) or is_atom(Name) ->
elem({Name});
elem(List) when is_list(List) ->
List1 = lists:filter(fun (E) ->
E =/= undefined
end, List),
lists:map(fun elem/1, List1);
elem({Name}) ->
elem(Name, undefined, undefined);
elem({Name, Attrs}) when is_map(Attrs) ->
elem({Name, Attrs, undefined});
elem({Name, Content}) ->
elem({Name, undefined, Content});
elem({Name, Attrs, Content}) when is_list(Content) ->
{Name, Attrs, elem(Content)};
elem({Name, Attrs, Content}) ->
{Name, Attrs, Content}.
elem(Name, Attrs) when is_map(Attrs) ->
elem({Name, Attrs, undefined});
elem(Name, Content) ->
elem({Name, undefined, Content}).
elem(Name, Attrs, Content) ->
elem({Name, Attrs, Content}).
doctype(Name, [{system, SystemIdentifier}]) ->
{doctype, {system, Name, SystemIdentifier}};
doctype(Name, [{public, [PublicIdentifier, SystemIdentifier]}]) ->
{doctype, {public, Name, PublicIdentifier, SystemIdentifier}}.
elems_with_prolog([First | Rest]) when length(Rest) > 0 ->
[first_elem(First) | elem(Rest)];
elems_with_prolog(ElemSpec) ->
elem(ElemSpec).
first_elem({doctype, Args} = DoctypeDecl) when is_tuple(Args) ->
DoctypeDecl;
first_elem(ElemSpec) ->
elem(ElemSpec).
generate(Any) ->
generate(Any, #{}).
generate(Any, Options) ->
list_to_binary(format(Any, 0, Options)).
generate_iodata(Any) ->
generate_iodata(Any, #{}).
generate_iodata(Any, Options) ->
format(Any, 0, Options).
format(xml_decl, 0, Options) ->
Encoding = maps:get(encoding, Options, <<"UTF-8">>),
Standalone =
case maps:get(standalone, Options, undefined) of
true -> " standalone=\"yes\"";
false -> " standalone=\"no\"";
undefined -> ""
end,
["<?xml version=\"1.0\" encoding=\"", to_string(Encoding), $", Standalone, "?>"];
format({doctype, {system, Name, System}}, 0, _Options) ->
["<!DOCTYPE ",
to_string(Name),
" SYSTEM \"",
to_string(System),
"\">"];
format({doctype, {public, Name, Public, System}}, 0, _Options) ->
["<!DOCTYPE ",
to_string(Name),
" PUBLIC \"",
to_string(Public),
"\" \"",
to_string(System),
"\">"];
format(String, Level, Options) when is_bitstring(String) ->
format({undefined, undefined, String}, Level, Options);
format(List, Level, Options) when is_list(List) ->
Formatter = formatter(Options),
map_intersperse(List, Formatter:line_break(), fun (Arg) ->
format(Arg, Level, Options)
end);
format({undefined, undefined, Name}, Level, Options) when is_bitstring(Name) ->
[indent(Level, Options), to_string(Name)];
format({undefined, undefined, {iodata, IoData}}, _Level, _Options) ->
IoData;
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_ATTRS(Attrs) and ?IS_BLANK_LIST(Content) ->
[indent(Level, Options),
"<",
to_string(Name),
"/>"];
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_LIST(Content) ->
[indent(Level, Options),
"<",
to_string(Name),
" ",
format_attributes(Attrs),
"/>"];
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_ATTRS(Attrs) and not is_list(Content) ->
[indent(Level, Options),
"<",
to_string(Name),
">",
format_content(Content, Level + 1, Options),
"</",
to_string(Name),
">"];
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_ATTRS(Attrs) and is_list(Content) ->
Formatter = formatter(Options),
FormatChar = Formatter:line_break(),
[indent(Level, Options),
"<",
to_string(Name),
">",
format_content(Content, Level + 1, Options),
FormatChar,
indent(Level, Options),
"</",
to_string(Name),
">"];
format({Name, Attrs, Content}, Level, Options) when not ?IS_BLANK_ATTRS(Attrs) and not is_list(Content) ->
[indent(Level, Options),
"<",
to_string(Name),
" ",
format_attributes(Attrs),
">",
format_content(Content, Level + 1, Options),
"</",
to_string(Name),
">"];
format({Name, Attrs, Content}, Level, Options) when not ?IS_BLANK_ATTRS(Attrs) and is_list(Content) ->
Formatter = formatter(Options),
FormatChar = Formatter:line_break(),
[indent(Level, Options),
"<",
to_string(Name),
" ",
format_attributes(Attrs),
">",
format_content(Content, Level + 1, Options),
FormatChar,
indent(Level, Options),
"</",
to_string(Name),
">"].
format_content(Children, Level, Options) when is_list(Children) ->
Formatter = formatter(Options),
FormatChar = Formatter:line_break(),
[FormatChar, map_intersperse(Children, FormatChar, fun (Arg) ->
format(Arg, Level, Options)
end)];
format_content(Content, _Level, _Options) ->
escape(Content).
format_attributes(Attrs) ->
map_intersperse(Attrs, <<" ">>, fun ({Name, Value}) ->
[to_string(Name), "=", quote_attribute_value(Value)]
end).
indent(Level, Options) ->
Formatter = formatter(Options),
Formatter:indentation(Level, Options).
formatter(Options) ->
case maps:get(format, Options, undefined) of
none -> xml_builderl_format_none;
_ -> xml_builderl_format_indented
end.
quote_attribute_value(Val) when not is_bitstring(Val) ->
quote_attribute_value(to_string(Val));
quote_attribute_value(Val) ->
Escape = string_contains(Val, [<<"\"">>, <<"&">>, <<"<">>]),
case Escape of
true -> [$", escape(Val), $"];
false -> [$", Val, $"]
end.
escape({iodata, IoData}) ->
IoData;
escape({safe, Data}) when is_bitstring(Data) ->
Data;
escape({safe, Data}) ->
to_string(Data);
escape({cdata, Data}) ->
["<![CDATA[", Data, "]]>"];
escape(Data) when is_binary(Data) ->
to_string(escape_string(Data));
escape(Data) when not is_binary(Data) ->
to_string(escape_string(to_string(Data))).
escape_string(<<"">>) ->
<<"">>;
escape_string(<<"&"/utf8, Rest/binary>>) ->
escape_entity(Rest);
escape_string(<<"<"/utf8, Rest/binary>>) ->
["<" | escape_string(Rest)];
escape_string(<<">"/utf8, Rest/binary>>) ->
[">" | escape_string(Rest)];
escape_string(<<"\""/utf8, Rest/binary>>) ->
[""" | escape_string(Rest)];
escape_string(<<"'"/utf8, Rest/binary>>) ->
["'" | escape_string(Rest)];
escape_string(<<C/utf8, Rest/binary>>) ->
[C | escape_string(Rest)].
escape_entity(<<"amp;"/utf8, Rest/binary>>) ->
["&" | escape_string(Rest)];
escape_entity(<<"lt;"/utf8, Rest/binary>>) ->
["<" | escape_string(Rest)];
escape_entity(<<"gt;"/utf8, Rest/binary>>) ->
[">" | escape_string(Rest)];
escape_entity(<<"quot;"/utf8, Rest/binary>>) ->
[""" | escape_string(Rest)];
escape_entity(<<"apos;"/utf8, Rest/binary>>) ->
["'" | escape_string(Rest)];
escape_entity(Rest) ->
["&" | escape_string(Rest)].
to_string(Term) when is_binary(Term) ->
Term;
to_string(Term) when is_bitstring(Term) ->
Term;
to_string(Term) when is_list(Term) ->
list_to_binary(Term);
to_string(Term) ->
list_to_binary(lists:flatten(io_lib:format("~p", [Term]))).
wrap(Data) when is_list(Data) ->
Data;
wrap(Data) ->
[Data].
string_contains(S, Search) when is_binary(Search) ->
nomatch =/= string:find(S, Search);
string_contains(_S, []) ->
false;
string_contains(S, [H | T]) ->
case string_contains(S, H) of
true ->
true;
false ->
string_contains(S, T)
end.
%% Ported from Elixir standard library Enum.map_intersperse
map_intersperse(Enumerable, Separator, Mapper) when is_map(Enumerable) ->
Reduced = lists:foldl(fun (Entry, Acc) ->
case Acc == first of
true ->
[Mapper(Entry)];
false ->
[Mapper(Entry), Separator | Acc]
end
end, first, maps:to_list(Enumerable)),
case Reduced == first of
true ->
[];
false ->
lists:reverse(Reduced)
end;
map_intersperse(Enumerable, Separator, Mapper) when is_list(Enumerable) ->
map_intersperse_list(Enumerable, Separator, Mapper).
map_intersperse_list([], _, _) ->
[];
map_intersperse_list([Last], _, Mapper) ->
[Mapper(Last)];
map_intersperse_list([Head | Rest], Separator, Mapper) ->
[Mapper(Head), Separator | map_intersperse_list(Rest, Separator, Mapper)].
from_map(Map) when is_map(Map) ->
Tags = lists:map(fun ({Key, Value}) ->
build_tag(Key, Value)
end, maps:to_list(Map)),
generate(document(Tags)).
build_tag(Key, Value) ->
build_tag(Key, Value, #{}).
build_tag(Key, Map, Attributes) when is_map(Map) ->
case maps:get(<<"#content">>, Map, undefined) of
undefined ->
Tags = lists:map(fun ({K, V}) ->
build_tag(K, V)
end, maps:to_list(Map)),
elem(Key, Attributes, Tags);
Value ->
FAttributes = lists:filter(fun ({K, _V}) ->
string:slice(K, 0, 1) == <<"-">>
end, maps:to_list(Map)),
Attributes2 = maps:from_list(lists:map(fun ({K, V}) ->
{string:slice(K, 1), V}
end, FAttributes)),
build_tag(Key, Value, Attributes2)
end;
build_tag(Key, Values, Attributes) when is_list(Values) ->
lists:map(fun (Value) ->
build_tag(Key, Value, Attributes)
end, Values);
build_tag(Key, Value, Attributes) ->
elem(Key, Attributes, to_string(Value)).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
string_contains_test() ->
?assert(string_contains(<<"foo\"">>, <<"\"">>)),
?assert(string_contains(<<"foo&">>, <<"&">>)),
?assert(string_contains(<<"foo<">>, <<"<">>)),
?assert(string_contains(<<"foo\"">>, [<<"\"">>, <<"&">>, <<"<">>])).
element_test() ->
?assertEqual({person, undefined, undefined}, elem(person)),
?assertEqual({person, undefined, <<"data">>}, elem(person, <<"data">>)),
?assertEqual({person, #{id => 1}, undefined}, elem(person, #{id => 1})),
?assertEqual({person, #{id => 1}, <<"data">>}, elem(person, #{id => 1}, <<"data">>)).
element_nested_test() ->
?assertEqual({person, #{id => 1}, [{first, undefined, <<"Steve">>}, {last, undefined, <<"Jobs">>}]},
elem(person, #{id => 1}, [elem(first, <<"Steve">>), elem(last, <<"Jobs">>)])).
document_test() ->
?assertEqual([xml_decl, {person, undefined, undefined}], document(person)),
?assertEqual([xml_decl, {doctype, {system, <<"greeting">>, <<"hello.dtd">>}}, {greeting, undefined, <<"Hello, World!">>}], document([doctype(<<"greeting">>, [{system, <<"hello.dtd">>}]), {greeting, <<"Hello, World!">>}])),
?assertEqual([xml_decl, {doctype, {public, <<"html">>, <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
<<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>}},
{html, undefined, <<"Hello, world!">>}],
document([
doctype(
<<"html">>,
[{public, [
<<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
<<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>
]}]
),
{html, <<"Hello, world!">>}
])),
?assertEqual([xml_decl, {person, undefined, <<"Josh">>}],
document(person, <<"Josh">>)),
?assertEqual([xml_decl, {person, #{id => 1}, <<"Josh">>}],
document(person, #{id => 1}, <<"Josh">>)).
generate_iodata_test() ->
?assertEqual([<<"">>, "<", <<"person">>, "/>"],
generate_iodata(elem(person))),
?assertEqual([<<"">>, "<", <<"person">>, ">", [<<"test">>, $i, <<"ng 123">>], "</", <<"person">>, ">"],
generate_iodata(elem(person, {iodata, [<<"test">>, $i, <<"ng 123">>]}))).
generate_test() ->
?assertEqual(<<"<person/>">>, generate(elem(person))),
?assertEqual(<<"<person id=\"1\">Steve Jobs</person>">>, generate({person, #{id => 1}, <<"Steve Jobs">>})),
?assertEqual(<<"<name><first>Steve</first></name>">>, generate({name, undefined, [{first, undefined, <<"Steve">>}]}, #{format => none})),
?assertEqual(<<"<name>\n<first>Steve</first>\n</name>">>, generate({name, undefined, [{first, undefined, <<"Steve">>}]}, #{whitespace => <<"">>})),
?assertEqual(<<"<name>\n <first>Steve</first>\n</name>">>, generate({name, undefined, [{first, undefined, <<"Steve">>}]})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>">>, generate(xml_decl, #{encoding => <<"ISO-8859-1">>})),
Input = {level1, undefined, [{level2, undefined, <<"test_value">>}]},
?assertEqual(<<"<level1><level2>test_value</level2></level1>">>, generate(Input, #{format => none})),
?assertEqual(<<"<level1>\n\t<level2>test_value</level2>\n</level1>">>, generate(Input, #{whitespace => <<"\t">>})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>">>, generate(xml_decl)),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>">>,
generate(xml_decl, #{standalone => true})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>">>,
generate(xml_decl, #{standalone => false})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>">>,
generate(xml_decl)).
document_generate_test() ->
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person/>">>,
generate(document(person))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
generate(document(person, <<"Josh">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?><person/>">>,
generate(document(person), #{format => none})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"1\"/>">>,
generate(document(person, #{id => 1}))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"1\">some data</person>">>,
generate(document(person, #{id => 1}, <<"some data">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE greeting SYSTEM \"hello.dtd\">\n<greeting>Hello, World!</greeting>">>,
generate(document([doctype(<<"greeting">>, [{system, <<"hello.dtd">>}]),
{greeting, <<"Hello, World!">>}]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html>Hello, world!</html>">>,
generate(document([doctype(<<"html">>, [{public, [<<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
<<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>]}]), {html, <<"Hello, world!">>}]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<oldschool/>">>,
generate(document([elem(oldschool, [])]), #{format => indent, encoding => <<"ISO-8859-1">>})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"ISO-8859-1\" standalone=\"yes\"?>\n<standaloneOldschool/>">>,
generate(document([elem(standaloneOldschool, [])]), #{format => indent, encoding => <<"ISO-8859-1">>, standalone => true})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
generate(document(person, <<"Josh">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person city=\"Montreal\" occupation=\"Developer\"/>">>,
generate(document(person, #{occupation => <<"Developer">>, city => <<"Montreal">>}))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person city=\"Montreal\" occupation=\"Developer\">Josh</person>">>,
generate(document(person, #{occupation => <<"Developer">>, city => <<"Montreal">>}, <<"Josh">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person city=\"Montreal\" occupation=\"Developer\"/>">>,
generate(document(person, #{occupation => <<"Developer">>, city => <<"Montreal">>}, undefined))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
generate(document(person, #{}, <<"Josh">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person/>">>,
generate(document(person, #{}, undefined))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person occupation=\"Developer\" city=\"Montreal\">Josh</person>">>,
generate(document(person, [{occupation, <<"Developer">>}, {city, <<"Montreal">>}], <<"Josh">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person occupation=\"Developer\" city=\"Montreal\"/>">>,
generate(document(person, [{occupation, <<"Developer">>}, {city, <<"Montreal">>}], undefined))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
generate(document(person, [], <<"Josh">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person/>">>,
generate(document(person, [], undefined))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name id=\"123\">Josh</name>\n</person>">>,
generate(document(person, [{name, #{id => 123}, <<"Josh">>}]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <first_name>Josh</first_name>\n <last_name>Nussbaum</last_name>\n</person>">>,
generate(document(person, [{first_name, <<"Josh">>}, {last_name, <<"Nussbaum">>}]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"123\">\n <name>Josh</name>\n</person>">>,
generate(document(person, #{id => 123}, [{name, <<"Josh">>}]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"123\">\n <first_name>Josh</first_name>\n <last_name>Nussbaum</last_name>\n</person>">>,
generate(document(person, #{id => 123}, [{first_name, <<"Josh">>}, {last_name, <<"Nussbaum">>}]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n TextNode\n <name id=\"123\">Josh</name>\n TextNode\n</person>">>,
generate(document(person, [<<"TextNode">>, {name, #{id => 123}, <<"Josh">>}, <<"TextNode">>]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<name id=\"123\">Josh</name>">>,
generate(document(name, #{id => 123}, <<"Josh">>))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<first_name>Josh</first_name>\n<last_name>Nussbaum</last_name>">>,
generate(document([{first_name, <<"Josh">>}, {last_name, <<"Nussbaum">>}]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<first_name/>\n<middle_name/>\n<last_name/>">>,
generate(document([first_name, middle_name, last_name]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<first_name/>\n<last_name/>">>,
generate(document([first_name, undefined, last_name]))),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <first>Josh</first>\n <last>Nussbaum</last>\n</person>">>,
generate(document(person, [{first, <<"Josh">>}, {last, <<"Nussbaum">>}]))).
element_generate_test() ->
?assertEqual(<<"<person>Josh</person>">>, generate(elem(person, <<"Josh">>))),
?assertEqual(<<"<person occupation=\"Developer\">Josh</person>">>, generate(elem(person, #{occupation => <<"Developer">>}, <<"Josh">>))),
?assertEqual(<<"<person height=\"12\"/>">>, generate(elem(person, #{height => 12}))),
?assertEqual(<<"<person height=\"10'\"/>">>, generate(elem(person, #{height => <<"10'">>}))),
?assertEqual(<<"<person height=\"10"\"/>">>, generate(elem(person, #{height => <<"10\"">>}))),
?assertEqual(<<"<person height=\"<10'5"\"/>">>, generate(elem(person, #{height => <<"<10'5\"">>}))),
?assertEqual(<<"<person height=\"<10\"/>">>, generate(elem(person, #{height => <<"<10">>}))),
?assertEqual(<<"<person>Josh</person>">>, generate(elem(person, <<"Josh">>))),
?assertEqual(<<"<person><Josh></person>">>, generate(elem(person, <<"<Josh>">>))),
?assertEqual(<<"<data>1 <> 2 & 2 <> 3 "'"'</data>">>,
generate(elem(data, <<"1 <> 2 & 2 <> 3 \"'\"'">>))),
?assertEqual(<<"<data>><"'&</data>">>,
generate(elem(data, <<"><"'&">>))),
?assertEqual(<<"<person><![CDATA[john & <is ok>]]></person>">>,
generate(elem(person, {cdata, "john & <is ok>"}))),
?assertEqual(<<"<person>john & <is ok></person>">>,
generate(elem(person, {safe, <<"john & <is ok>">>}))),
?assertEqual(<<"<person><name>john</name><age>12</age></person>">>,
generate(elem(person, {iodata, [generate(elem(name, <<"john">>)), generate(elem(age, <<"12">>))]}))),
?assertEqual(<<"<person>testing 123</person>">>,
generate(elem(person, {iodata, [<<"test">>, $i, <<"ng 123">>]}))).
map_to_xml_test() ->
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>Value1</Tag1>">>,
from_map(#{<<"Tag1">> => <<"Value1">>})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>Value1</Tag1>\n<Tag2>Value2</Tag2>\n<Tag3>Value3</Tag3>">>,
from_map(#{<<"Tag1">> => <<"Value1">>,
<<"Tag2">> => <<"Value2">>,
<<"Tag3">> => <<"Value3">>})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tags>\n <Tag1>\n <Sub1>Val1</Sub1>\n </Tag1>\n <Tag1>\n <Sub1>Val2</Sub1>\n </Tag1>\n <Tag1>\n <Sub1>Val3</Sub1>\n </Tag1>\n</Tags>">>,
from_map(#{
<<"Tags">> => #{
<<"Tag1">> => [
#{<<"Sub1">> => <<"Val1">>},
#{<<"Sub1">> => <<"Val2">>},
#{<<"Sub1">> => <<"Val3">>}
]
}
})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>1234</Tag1>\n<Tag2>12.15</Tag2>">>,
from_map(#{<<"Tag1">> => 1234, <<"Tag2">> => 12.15})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>true</Tag1>\n<Tag2>false</Tag2>">>,
from_map(#{<<"Tag1">> => true, <<"Tag2">> => false})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"123\" something=\"111\">some value</Tag1>">>,
from_map(#{<<"Tag1">> => #{<<"#content">> => <<"some value">>,
<<"-id">> => 123,
<<"-something">> => <<"111">>}})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"123\" something=\"111\">\n <Tag2>\n <Tag3>value</Tag3>\n </Tag2>\n</Tag1>">>,
from_map(#{
<<"Tag1">> => #{
<<"#content">> => #{
<<"Tag2">> => #{
<<"Tag3">> => <<"value">>
}
},
<<"-id">> => 123,
<<"-something">> => <<"111">>
}
})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"1234\" something=\"value\">\n <Tag2 id=\"1234\" something=\"value\">\n <Tag3 id=\"1234\" something=\"value\">final value</Tag3>\n </Tag2>\n</Tag1>">>,
from_map(#{
<<"Tag1">> => #{
<<"#content">> => #{
<<"Tag2">> => #{
<<"#content">> => #{
<<"Tag3">> => #{
<<"#content">> => <<"final value">>,
<<"-id">> => <<"1234">>,
<<"-something">> => <<"value">>
}
},
<<"-id">> => <<"1234">>,
<<"-something">> => <<"value">>
}
},
<<"-id">> => <<"1234">>,
<<"-something">> => <<"value">>
}
})),
?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"1234\" something=\"value\">value1</Tag1>\n<Tag1 id=\"1234\" something=\"value\">value2</Tag1>">>,
from_map(#{
<<"Tag1">> => #{
<<"#content">> => [
<<"value1">>,
<<"value2">>
],
<<"-id">> => <<"1234">>,
<<"-something">> => <<"value">>
}
})).
-endif.