From 94d9d474ddb1f5a1e2e3eff059e2260999f6a5ed Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 7 Dec 2024 13:27:01 +0100 Subject: [PATCH 1/4] Add mutator helpers to exml --- src/exml.erl | 57 +++++++++++++++++++++++++++++++++++++++++- test/exml_tests.erl | 60 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/exml.erl b/src/exml.erl index b74715e..a40530f 100644 --- a/src/exml.erl +++ b/src/exml.erl @@ -17,9 +17,17 @@ to_binary/1, to_iolist/1, xml_size/1, - xml_sort/1, to_pretty_iolist/1]). +-export([filter_children/2, + append_children/2, + upsert_attr_value/3, + upsert_child/2, + insert_new_child/2, + remove_cdata/1, + remove_attr/2, + xml_sort/1]). + -export_type([attr/0, cdata/0, element/0, @@ -97,6 +105,53 @@ xml_sort({Key, Value}) -> xml_sort(Elements) when is_list(Elements) -> lists:sort([ xml_sort(E) || E <- Elements ]). +%% @doc Return the given `t:element/0' with the specified filter passed over its children. +-spec filter_children(element(), fun((element() | cdata()) -> boolean())) -> element(). +filter_children(#xmlel{children = Children} = El, Pred) -> + NoCdata = lists:filter(Pred, Children), + El#xmlel{children = NoCdata}. + +%% @doc Return the given `t:element/0' without any `t:cdata/0' on its children. +-spec remove_cdata(element()) -> element(). +remove_cdata(#xmlel{children = Children} = El) -> + Pred = fun(Child) -> not is_record(Child, xmlcdata) end, + NoCdata = lists:filter(Pred, Children), + El#xmlel{children = NoCdata}. + +%% @doc Remove a given attribute from a `t:element/0'. +-spec remove_attr(exml:element(), binary()) -> element(). +remove_attr(#xmlel{attrs = Attrs} = El, Key) -> + NewAttrs = lists:keydelete(Key, 1, Attrs), + El#xmlel{attrs = NewAttrs}. + +%% @doc Append new children elements to a `t:element/0'. +-spec append_children(element(), [element() | cdata()]) -> element(). +append_children(#xmlel{children = Children} = El, ExtraChildren) -> + El#xmlel{children = Children ++ ExtraChildren}. + +%% @doc Replace or insert the value of a given attribute. +-spec upsert_attr_value(element(), binary(), binary()) -> element(). +upsert_attr_value(#xmlel{attrs = Attrs} = El, Key, Value) -> + Attrs1 = lists:keydelete(Key, 1, Attrs), + Attrs2 = [{Key, Value} | Attrs1], + El#xmlel{attrs = Attrs2}. + +%% @doc Replace or insert a child by the given one. +-spec upsert_child(element(), element()) -> element(). +upsert_child(#xmlel{children = Children} = El, #xmlel{name = Name} = NewChild) -> + Children2 = lists:keystore(Name, #xmlel.name, Children, NewChild), + El#xmlel{children = Children2}. + +%% @doc Insert a child by the given one, if none existed. +-spec insert_new_child(element(), element()) -> element(). +insert_new_child(#xmlel{children = Children} = El, #xmlel{name = Name} = NewChild) -> + case lists:keymember(Name, #xmlel.name, Children) of + false -> + El#xmlel{children = [NewChild | Children]}; + true -> + El + end. + %% @equiv erlang:binary_to_list(to_binary(Element)) -spec to_list(exml_stream:element() | [exml_stream:element()]) -> string(). to_list(Element) -> diff --git a/test/exml_tests.erl b/test/exml_tests.erl index 6294827..ff9fd28 100644 --- a/test/exml_tests.erl +++ b/test/exml_tests.erl @@ -57,6 +57,66 @@ sort_xmlel_attributes_test() -> ToOrder = [{<<"attr2">>, <<"bar">>}, {<<"attr1">>, <<"foo">>}], ?assertEqual(Attrs, exml:xml_sort(ToOrder)). +remove_cdata_test() -> + Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Child1 = #xmlel{name = <<"el1">>, attrs = Attrs}, + Child2 = #xmlel{name = <<"el2">>, attrs = Attrs}, + CData = #xmlcdata{content = <<"some value">>}, + El = #xmlel{name = <<"foo">>, children = [Child1, CData, Child2]}, + Expected = #xmlel{name = <<"foo">>, children = [Child1, Child2]}, + ?exmlAssertEqual(Expected, exml:remove_cdata(El)). + +filter_children_test() -> + Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Child1 = #xmlel{name = <<"el1">>, attrs = [{<<"xmlns">>, <<"foo">>}]}, + Child2 = #xmlel{name = <<"el2">>, attrs = [{<<"xmlns">>, <<"bar">>} | Attrs]}, + Child3 = #xmlel{name = <<"el3">>, attrs = [{<<"xmlns">>, <<"baz">>} | Attrs]}, + El = #xmlel{name = <<"foo">>, children = [Child1, Child2, Child3]}, + Expected = #xmlel{name = <<"foo">>, children = [Child1, Child3]}, + Pred = fun(Child) -> <<"bar">> =/= exml_query:attr(Child, <<"xmlns">>) end, + ?exmlAssertEqual(Expected, exml:filter_children(El, Pred)). + +append_children_test() -> + Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Child1 = #xmlel{name = <<"el1">>, attrs = Attrs}, + Child2 = #xmlel{name = <<"el2">>, attrs = Attrs}, + CData = #xmlcdata{content = <<"some value">>}, + El = #xmlel{name = <<"foo">>, children = [Child1]}, + Expected = #xmlel{name = <<"foo">>, children = [Child1, Child2, CData]}, + ?exmlAssertEqual(Expected, exml:append_children(El, [Child2, CData])). + +replace_attribute_value_test() -> + Attrs1 = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Attrs2 = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"baz">>}], + El = #xmlel{name = <<"foo">>, attrs = Attrs1}, + Expected = #xmlel{name = <<"foo">>, attrs = Attrs2}, + ?exmlAssertEqual(Expected, exml:upsert_attr_value(El, <<"attr2">>, <<"baz">>)). + +remove_attribute_test() -> + Attrs1 = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Attrs2 = [{<<"attr2">>, <<"bar">>}], + El = #xmlel{name = <<"foo">>, attrs = Attrs1}, + Expected = #xmlel{name = <<"foo">>, attrs = Attrs2}, + ?exmlAssertEqual(Expected, exml:remove_attr(El, <<"attr1">>)). + +replace_child_test() -> + Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Child1 = #xmlel{name = <<"el">>}, + Child2 = #xmlel{name = <<"el">>, attrs = Attrs}, + Child3 = #xmlel{name = <<"last">>, attrs = Attrs, children = [Child1]}, + El = #xmlel{name = <<"foo">>, children = [Child1, Child3]}, + Expected = #xmlel{name = <<"foo">>, children = [Child2, Child3]}, + ?exmlAssertEqual(Expected, exml:upsert_child(El, Child2)). + +insert_new_child_test() -> + Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Child1 = #xmlel{name = <<"el">>}, + Child2 = #xmlel{name = <<"el">>, attrs = Attrs}, + Child3 = #xmlel{name = <<"last">>, attrs = Attrs, children = [Child1]}, + El = #xmlel{name = <<"foo">>, children = [Child1, Child3]}, + Expected = #xmlel{name = <<"foo">>, children = [Child1, Child3]}, + ?exmlAssertEqual(Expected, exml:insert_new_child(El, Child2)). + sort_xmlel_test() -> Attrs = [{<<"attr1">>, <<"bar">>}, {<<"attr2">>, <<"baz">>}], El1 = #xmlel{ From 69e97d889ada0d73c6a9b62e50a6c926c4e0dad5 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 7 Dec 2024 20:12:49 +0100 Subject: [PATCH 2/4] Separate getting name and attrs from parsed doc --- c_src/exml.cpp | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/c_src/exml.cpp b/c_src/exml.cpp index cb5f1cc..8cc6304 100644 --- a/c_src/exml.cpp +++ b/c_src/exml.cpp @@ -147,13 +147,6 @@ ERL_NIF_TERM to_subbinary(ParseCtx &ctx, const unsigned char *text, return binary; } -ERL_NIF_TERM make_attr_tuple(ParseCtx &ctx, - rapidxml::xml_attribute *attr) { - ERL_NIF_TERM name = to_subbinary(ctx, attr->name(), attr->name_size()); - ERL_NIF_TERM value = to_subbinary(ctx, attr->value(), attr->value_size()); - return enif_make_tuple2(ctx.env, name, value); -} - ERL_NIF_TERM get_xmlcdata(ParseCtx &ctx, rapidxml::xml_node *node) { return enif_make_tuple3(ctx.env, atom_xmlcdata, @@ -248,9 +241,14 @@ ERL_NIF_TERM make_node_name_binary(ParseCtx &ctx, return to_subbinary(ctx, start, len); } -std::tuple -get_open_tag(ParseCtx &ctx, rapidxml::xml_node *node) { - ERL_NIF_TERM name_term = make_node_name_binary(ctx, node); +ERL_NIF_TERM make_attr_tuple(ParseCtx &ctx, + rapidxml::xml_attribute *attr) { + ERL_NIF_TERM name = to_subbinary(ctx, attr->name(), attr->name_size()); + ERL_NIF_TERM value = to_subbinary(ctx, attr->value(), attr->value_size()); + return enif_make_tuple2(ctx.env, name, value); +} + +ERL_NIF_TERM get_attributes(ParseCtx &ctx, rapidxml::xml_node *node) { std::vector &attrs = Parser::term_buffer; std::size_t begin = attrs.size(); @@ -265,14 +263,15 @@ get_open_tag(ParseCtx &ctx, rapidxml::xml_node *node) { : enif_make_list_from_array(ctx.env, attrs.data() + begin, size); attrs.erase(attrs.end() - size, attrs.end()); - return std::make_tuple(name_term, attrs_term); + return attrs_term; } ERL_NIF_TERM make_stream_start_tuple(ParseCtx &ctx, rapidxml::xml_node *node) { - auto name_and_attrs = get_open_tag(ctx, node); - return enif_make_tuple3(ctx.env, atom_xmlstreamstart, std::get<0>(name_and_attrs), - std::get<1>(name_and_attrs)); + + ERL_NIF_TERM name_term = make_node_name_binary(ctx, node); + ERL_NIF_TERM attrs_term = get_attributes(ctx, node); + return enif_make_tuple3(ctx.env, atom_xmlstreamstart, name_term, attrs_term); } ERL_NIF_TERM make_stream_end_tuple(ParseCtx &ctx) { @@ -287,10 +286,10 @@ ERL_NIF_TERM make_stream_end_tuple(ParseCtx &ctx) { ERL_NIF_TERM make_xmlel(ParseCtx &ctx, rapidxml::xml_node *node) { - auto name_and_attrs = get_open_tag(ctx, node); + ERL_NIF_TERM name_term = make_node_name_binary(ctx, node); + ERL_NIF_TERM attrs_term = get_attributes(ctx, node); ERL_NIF_TERM children_term = get_children_tuple(ctx, node); - return enif_make_tuple4(ctx.env, atom_xmlel, std::get<0>(name_and_attrs), - std::get<1>(name_and_attrs), children_term); + return enif_make_tuple4(ctx.env, atom_xmlel, name_term, attrs_term, children_term); } bool build_children(ErlNifEnv *env, xml_document &doc, ERL_NIF_TERM children, From 47acef77dceb337abaed050d55df01770b55be3e Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 10 Dec 2024 16:16:18 +0100 Subject: [PATCH 3/4] Parse and encode attributes as maps --- c_src/exml.cpp | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/c_src/exml.cpp b/c_src/exml.cpp index 8cc6304..c446d2d 100644 --- a/c_src/exml.cpp +++ b/c_src/exml.cpp @@ -252,17 +252,15 @@ ERL_NIF_TERM get_attributes(ParseCtx &ctx, rapidxml::xml_node *no std::vector &attrs = Parser::term_buffer; std::size_t begin = attrs.size(); - for (rapidxml::xml_attribute *attr = node->first_attribute(); - attr; attr = attr->next_attribute()) - attrs.push_back(make_attr_tuple(ctx, attr)); + ERL_NIF_TERM attrs_term = enif_make_new_map(ctx.env); - std::size_t size = attrs.size() - begin; - ERL_NIF_TERM attrs_term = - size == 0 - ? enif_make_list(ctx.env, 0) - : enif_make_list_from_array(ctx.env, attrs.data() + begin, size); + for (rapidxml::xml_attribute *attr = node->first_attribute(); + attr; attr = attr->next_attribute()) { + ERL_NIF_TERM key = to_subbinary(ctx, attr->name(), attr->name_size()); + ERL_NIF_TERM value = to_subbinary(ctx, attr->value(), attr->value_size()); + enif_make_map_put(ctx.env, attrs_term, key, value, &attrs_term); + } - attrs.erase(attrs.end() - size, attrs.end()); return attrs_term; } @@ -318,27 +316,28 @@ bool build_cdata(ErlNifEnv *env, xml_document &doc, const ERL_NIF_TERM elem[], bool build_attrs(ErlNifEnv *env, xml_document &doc, ERL_NIF_TERM attrs, rapidxml::xml_node &node) { - if (!enif_is_list(env, attrs)) + if (!enif_is_map(env, attrs)) return false; - for (ERL_NIF_TERM head; enif_get_list_cell(env, attrs, &head, &attrs);) { - int arity; - const ERL_NIF_TERM *tuple; - if (!enif_get_tuple(env, head, &arity, &tuple) || arity != 2) - return false; + ErlNifMapIterator iter; + enif_map_iterator_create(env, attrs, &iter, ERL_NIF_MAP_ITERATOR_FIRST); + ERL_NIF_TERM map_key, map_value; + while (enif_map_iterator_get_pair(env, &iter, &map_key, &map_value)) { ErlNifBinary key, value; - if (!enif_inspect_iolist_as_binary(env, tuple[0], &key)) + if (!enif_inspect_iolist_as_binary(env, map_key, &key)) return false; - if (!enif_inspect_iolist_as_binary(env, tuple[1], &value)) + if (!enif_inspect_iolist_as_binary(env, map_value, &value)) return false; auto attr = doc.impl.allocate_attribute(key.size > 0 ? key.data : EMPTY, value.size > 0 ? value.data : EMPTY, key.size, value.size); node.append_attribute(attr); + enif_map_iterator_next(env, &iter); } + enif_map_iterator_destroy(env, &iter); return true; } From 74957212acd96efbbd617a397d0127ae738ea645 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 10 Dec 2024 16:17:06 +0100 Subject: [PATCH 4/4] Update all usages of attributes as proplists into maps --- include/exml.hrl | 2 +- include/exml_stream.hrl | 2 +- src/exml.erl | 22 ++++++-------- src/exml_query.erl | 8 ++--- test/exml_query_tests.erl | 4 +-- test/exml_stream_tests.erl | 38 +++++++++++------------ test/exml_tests.erl | 62 +++++++++++++++----------------------- 7 files changed, 62 insertions(+), 76 deletions(-) diff --git a/include/exml.hrl b/include/exml.hrl index 5e8158f..34d8628 100644 --- a/include/exml.hrl +++ b/include/exml.hrl @@ -10,7 +10,7 @@ style = escaped :: escaped | cdata}). -record(xmlel, {name :: binary(), - attrs = [] :: [exml:attr()], + attrs = #{} :: exml:attrs(), children = [] :: [exml:element() | exml:cdata()]}). %% Implementation of the exmlAssertEqual/2 macro is a modification of diff --git a/include/exml_stream.hrl b/include/exml_stream.hrl index af9eb73..90ae931 100644 --- a/include/exml_stream.hrl +++ b/include/exml_stream.hrl @@ -1,6 +1,6 @@ -include("exml.hrl"). -record(xmlstreamstart, {name :: binary(), - attrs = [] :: [exml:attr()]}). + attrs = #{} :: exml:attrs()}). -record(xmlstreamend, {name :: binary()}). diff --git a/src/exml.erl b/src/exml.erl index a40530f..b6feb23 100644 --- a/src/exml.erl +++ b/src/exml.erl @@ -29,11 +29,13 @@ xml_sort/1]). -export_type([attr/0, + attrs/0, cdata/0, element/0, item/0]). -type attr() :: {binary(), binary()}. +-type attrs() :: #{binary() => binary()}. -type cdata() :: #xmlcdata{}. %% CDATA record. Printing escaping rules defaults to escaping character-wise. %% @@ -57,13 +59,13 @@ xml_size(#xmlcdata{content = Content, style = Style}) -> iolist_size(exml_nif:escape_cdata(Content, Style)); xml_size(#xmlel{ name = Name, attrs = Attrs, children = [] }) -> 3 % Self-closing: - + byte_size(Name) + xml_size(Attrs); + + byte_size(Name) + xml_size(maps:to_list(Attrs)); xml_size(#xmlel{ name = Name, attrs = Attrs, children = Children }) -> % Opening and closing: <> 5 + byte_size(Name)*2 - + xml_size(Attrs) + xml_size(Children); + + xml_size(maps:to_list(Attrs)) + xml_size(Children); xml_size(#xmlstreamstart{ name = Name, attrs = Attrs }) -> - byte_size(Name) + 2 + xml_size(Attrs); + byte_size(Name) + 2 + xml_size(maps:to_list(Attrs)); xml_size(#xmlstreamend{ name = Name }) -> byte_size(Name) + 3; xml_size({Key, Value}) when is_binary(Key) -> @@ -91,13 +93,12 @@ xml_size({Key, Value}) when is_binary(Key) -> (exml_stream:stop()) -> exml_stream:stop(). xml_sort(#xmlcdata{} = Cdata) -> Cdata; -xml_sort(#xmlel{ attrs = Attrs, children = Children } = El) -> +xml_sort(#xmlel{children = Children} = El) -> El#xmlel{ - attrs = lists:sort(Attrs), children = [ xml_sort(C) || C <- Children ] }; -xml_sort(#xmlstreamstart{ attrs = Attrs } = StreamStart) -> - StreamStart#xmlstreamstart{ attrs = lists:sort(Attrs) }; +xml_sort(#xmlstreamstart{} = StreamStart) -> + StreamStart; xml_sort(#xmlstreamend{} = StreamEnd) -> StreamEnd; xml_sort({Key, Value}) -> @@ -121,8 +122,7 @@ remove_cdata(#xmlel{children = Children} = El) -> %% @doc Remove a given attribute from a `t:element/0'. -spec remove_attr(exml:element(), binary()) -> element(). remove_attr(#xmlel{attrs = Attrs} = El, Key) -> - NewAttrs = lists:keydelete(Key, 1, Attrs), - El#xmlel{attrs = NewAttrs}. + El#xmlel{attrs = maps:remove(Key, Attrs)}. %% @doc Append new children elements to a `t:element/0'. -spec append_children(element(), [element() | cdata()]) -> element(). @@ -132,9 +132,7 @@ append_children(#xmlel{children = Children} = El, ExtraChildren) -> %% @doc Replace or insert the value of a given attribute. -spec upsert_attr_value(element(), binary(), binary()) -> element(). upsert_attr_value(#xmlel{attrs = Attrs} = El, Key, Value) -> - Attrs1 = lists:keydelete(Key, 1, Attrs), - Attrs2 = [{Key, Value} | Attrs1], - El#xmlel{attrs = Attrs2}. + El#xmlel{attrs = Attrs#{Key => Value}}. %% @doc Replace or insert a child by the given one. -spec upsert_child(element(), element()) -> element(). diff --git a/src/exml_query.erl b/src/exml_query.erl index c17ee79..635f80a 100644 --- a/src/exml_query.erl +++ b/src/exml_query.erl @@ -116,7 +116,7 @@ paths(#xmlel{} = Element, [{element_with_attr, AttrName, Value} | Rest]) -> paths(#xmlel{} = Element, [cdata]) -> [cdata(Element)]; paths(#xmlel{attrs = Attrs}, [{attr, Name}]) -> - lists:sublist([V || {N, V} <- Attrs, N =:= Name], 1); + lists:sublist([V || {N, V} <- maps:to_list(Attrs), N =:= Name], 1); paths(#xmlel{} = El, Path) when is_list(Path) -> erlang:error(invalid_path, [El, Path]). @@ -253,9 +253,9 @@ attr(Element, Name) -> %% @equiv path(Element, [{attr, Name}], Default) -spec attr(exml:element(), binary(), Default) -> binary() | Default. attr(#xmlel{attrs = Attrs}, Name, Default) -> - case lists:keyfind(Name, 1, Attrs) of - {Name, Value} -> + case maps:find(Name, Attrs) of + {ok, Value} -> Value; - false -> + error -> Default end. diff --git a/test/exml_query_tests.erl b/test/exml_query_tests.erl index f52033c..62a320b 100644 --- a/test/exml_query_tests.erl +++ b/test/exml_query_tests.erl @@ -132,8 +132,8 @@ element_with_name_and_ns_query_test() -> <<"urn:xmpp:chat-markers:0">>}])). element_with_name_and_ns_two_names_only_one_ns_query_test() -> - Elem1 = #xmlel{name = <<"a">>, attrs = [{<<"xmlns">>, <<"ns1">>}]}, - Elem2 = #xmlel{name = <<"a">>, attrs = [{<<"xmlns">>, <<"ns2">>}]}, + Elem1 = #xmlel{name = <<"a">>, attrs = #{<<"xmlns">> => <<"ns1">>}}, + Elem2 = #xmlel{name = <<"a">>, attrs = #{<<"xmlns">> => <<"ns2">>}}, Xml = #xmlel{name = <<"element">>, children = [Elem1, Elem2]}, ?assertEqual(Elem2, exml_query:subelement_with_name_and_ns(Xml, <<"a">>, <<"ns2">>)), ?assertEqual(Elem2, exml_query:path(Xml, [{element_with_ns, <<"a">>, <<"ns2">>}])). diff --git a/test/exml_stream_tests.erl b/test/exml_stream_tests.erl index f280a65..c2dedd3 100644 --- a/test/exml_stream_tests.erl +++ b/test/exml_stream_tests.erl @@ -14,14 +14,14 @@ basic_parse_test() -> exml_stream:parse(Parser1, <<" to='i.am.banana.com' xml:lang='en'>>), ?assertEqual( [#xmlstreamstart{name = <<"stream:stream">>, - attrs = [{<<"xmlns:stream">>, <<"http://etherx.jabber.org/streams">>}, - {<<"version">>, <<"1.0">>}, - {<<"to">>, <<"i.am.banana.com">>}, - {<<"xml:lang">>, <<"en">>}]}], + attrs = #{<<"xmlns:stream">> => <<"http://etherx.jabber.org/streams">>, + <<"version">> => <<"1.0">>, + <<"to">> => <<"i.am.banana.com">>, + <<"xml:lang">> => <<"en">>}}], StreamStart), {ok, Parser3, Auth} = exml_stream:parse(Parser2, <<" mechanism='DIGEST-MD5'/>">>), ?assertEqual( - [#xmlel{name = <<"auth">>, attrs = [{<<"mechanism">>, <<"DIGEST-MD5">>}]}], + [#xmlel{name = <<"auth">>, attrs = #{<<"mechanism">> => <<"DIGEST-MD5">>}}], Auth), {ok, Parser4, Empty1} = exml_stream:parse(Parser3, <<">), ?assertEqual([], Empty1), @@ -31,9 +31,9 @@ basic_parse_test() -> ?assertMatch( [#xmlel{name = <<"stream:features">>, children = [#xmlel{name = <<"bind">>, - attrs = [{<<"xmlns">>, <<"some_ns">>}]}, + attrs = #{<<"xmlns">> := <<"some_ns">>}}, #xmlel{name = <<"session">>, - attrs = [{<<"xmlns">>, <<"some_other">>}]}, + attrs = #{<<"xmlns">> := <<"some_other">>}}, _CData]}], Features), [#xmlel{children=[_, _, CData]}] = Features, @@ -49,9 +49,9 @@ parser_errors_test() -> -define(BANANA_STREAM, <<"I am a banana!">>). -define(assertIsBanana(Elements), (fun() -> % fun instead of begin/end because we bind CData in unhygenic macro ?assertMatch([#xmlstreamstart{name = <<"stream:stream">>, - attrs = [{<<"xmlns:stream">>, <<"something">>}]}, + attrs = #{<<"xmlns:stream">> := <<"something">>}}, #xmlel{name = <<"foo">>, - attrs = [{<<"attr">>, <<"bar">>}], + attrs = #{<<"attr">> := <<"bar">>}, children = [_CData, #xmlel{name = <<"baz">>}]}, #xmlstreamend{name = <<"stream:stream">>}], Elements), @@ -84,12 +84,12 @@ infinit_framed_stream_test() -> {ok, Parser0} = exml_stream:new_parser([{infinite_stream, true}, {autoreset, true}]), Els = [#xmlel{name = <<"open">>, - attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}, - {<<"to">>, <<"example.com">>}, - {<<"version">>, <<"1.0">>}]}, + attrs = #{<<"xmlns">> => <<"urn:ietf:params:xml:ns:xmpp-framing">>, + <<"to">> => <<"example.com">>, + <<"version">> => <<"1.0">>}}, #xmlel{name = <<"foo">>}, #xmlel{name = <<"message">>, - attrs = [{<<"to">>, <<"ala@example.com">>}], + attrs = #{<<"to">> => <<"ala@example.com">>}, children = [#xmlel{name = <<"body">>, children = [#xmlcdata{content = <<"Hi, How Are You?">>}]}]} ], @@ -147,7 +147,7 @@ conv_attr_test() -> AssertParses = fun(Input) -> {ok, Parser0} = exml_stream:new_parser(), {ok, _Parser1, Elements} = exml_stream:parse(Parser0, Input), - ?assertMatch([_, #xmlel{attrs = [{<<"attr">>, <<"&<>\"'\n\t\r">>}]} | _], + ?assertMatch([_, #xmlel{attrs = #{<<"attr">> := <<"&<>\"'\n\t\r">>}} | _], Elements), Elements end, @@ -233,18 +233,18 @@ infinite_stream_partial_chunk_test() -> {ok, Parser1, Open} = exml_stream:parse(Parser0, <<"">>), ?assertEqual( [#xmlel{name = <<"open">>, - attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}, - {<<"to">>, <<"i.am.banana.com">>}, - {<<"version">>, <<"1.0">>}]}], + attrs = #{<<"xmlns">> => <<"urn:ietf:params:xml:ns:xmpp-framing">>, + <<"to">> => <<"i.am.banana.com">>, + <<"version">> => <<"1.0">>}}], Open), {ok, Parser2, A} = exml_stream:parse(Parser1, <<"">>), - ?assertEqual([#xmlel{name = <<"a">>, attrs = []}], A), + ?assertEqual([#xmlel{name = <<"a">>, attrs = #{}}], A), {ok, Parser3, Empty0} = exml_stream:parse(Parser2, <<" ">>), ?assertEqual([], Empty0), {ok, Parser4, Empty1} = exml_stream:parse(Parser3, <<">), ?assertEqual([], Empty1), {ok, _Parser5, B} = exml_stream:parse(Parser4, <<">">>), - ?assertEqual([#xmlel{name = <<"b">>, attrs = []}], B). + ?assertEqual([#xmlel{name = <<"b">>, attrs = #{}}], B). null_character_test() -> {ok, P1} = exml_stream:new_parser(), diff --git a/test/exml_tests.erl b/test/exml_tests.erl index ff9fd28..81924fc 100644 --- a/test/exml_tests.erl +++ b/test/exml_tests.erl @@ -47,7 +47,7 @@ size_of_exml_with_cdata_test() -> sort_xmlel_identity_test() -> El = #xmlel{ name = <<"foo">>, - attrs = [{<<"attr1">>, <<"bar">>}], + attrs = #{<<"attr1">> => <<"bar">>}, children = [#xmlcdata{ content = <<"some value">> }] }, ?assertEqual(El, exml:xml_sort(El)). @@ -58,7 +58,7 @@ sort_xmlel_attributes_test() -> ?assertEqual(Attrs, exml:xml_sort(ToOrder)). remove_cdata_test() -> - Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Attrs = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"bar">>}, Child1 = #xmlel{name = <<"el1">>, attrs = Attrs}, Child2 = #xmlel{name = <<"el2">>, attrs = Attrs}, CData = #xmlcdata{content = <<"some value">>}, @@ -67,17 +67,17 @@ remove_cdata_test() -> ?exmlAssertEqual(Expected, exml:remove_cdata(El)). filter_children_test() -> - Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], - Child1 = #xmlel{name = <<"el1">>, attrs = [{<<"xmlns">>, <<"foo">>}]}, - Child2 = #xmlel{name = <<"el2">>, attrs = [{<<"xmlns">>, <<"bar">>} | Attrs]}, - Child3 = #xmlel{name = <<"el3">>, attrs = [{<<"xmlns">>, <<"baz">>} | Attrs]}, + Attrs = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"bar">>}, + Child1 = #xmlel{name = <<"el1">>, attrs = #{<<"xmlns">> => <<"foo">>}}, + Child2 = #xmlel{name = <<"el2">>, attrs = maps:merge(#{<<"xmlns">> => <<"bar">>}, Attrs)}, + Child3 = #xmlel{name = <<"el3">>, attrs = maps:merge(#{<<"xmlns">> => <<"baz">>}, Attrs)}, El = #xmlel{name = <<"foo">>, children = [Child1, Child2, Child3]}, Expected = #xmlel{name = <<"foo">>, children = [Child1, Child3]}, Pred = fun(Child) -> <<"bar">> =/= exml_query:attr(Child, <<"xmlns">>) end, ?exmlAssertEqual(Expected, exml:filter_children(El, Pred)). append_children_test() -> - Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Attrs = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"bar">>}, Child1 = #xmlel{name = <<"el1">>, attrs = Attrs}, Child2 = #xmlel{name = <<"el2">>, attrs = Attrs}, CData = #xmlcdata{content = <<"some value">>}, @@ -86,21 +86,21 @@ append_children_test() -> ?exmlAssertEqual(Expected, exml:append_children(El, [Child2, CData])). replace_attribute_value_test() -> - Attrs1 = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], - Attrs2 = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"baz">>}], + Attrs1 = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"bar">>}, + Attrs2 = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"baz">>}, El = #xmlel{name = <<"foo">>, attrs = Attrs1}, Expected = #xmlel{name = <<"foo">>, attrs = Attrs2}, ?exmlAssertEqual(Expected, exml:upsert_attr_value(El, <<"attr2">>, <<"baz">>)). remove_attribute_test() -> - Attrs1 = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], - Attrs2 = [{<<"attr2">>, <<"bar">>}], + Attrs1 = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"bar">>}, + Attrs2 = #{<<"attr2">> => <<"bar">>}, El = #xmlel{name = <<"foo">>, attrs = Attrs1}, Expected = #xmlel{name = <<"foo">>, attrs = Attrs2}, ?exmlAssertEqual(Expected, exml:remove_attr(El, <<"attr1">>)). replace_child_test() -> - Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Attrs = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"bar">>}, Child1 = #xmlel{name = <<"el">>}, Child2 = #xmlel{name = <<"el">>, attrs = Attrs}, Child3 = #xmlel{name = <<"last">>, attrs = Attrs, children = [Child1]}, @@ -109,7 +109,7 @@ replace_child_test() -> ?exmlAssertEqual(Expected, exml:upsert_child(El, Child2)). insert_new_child_test() -> - Attrs = [{<<"attr1">>, <<"foo">>}, {<<"attr2">>, <<"bar">>}], + Attrs = #{<<"attr1">> => <<"foo">>, <<"attr2">> => <<"bar">>}, Child1 = #xmlel{name = <<"el">>}, Child2 = #xmlel{name = <<"el">>, attrs = Attrs}, Child3 = #xmlel{name = <<"last">>, attrs = Attrs, children = [Child1]}, @@ -117,19 +117,8 @@ insert_new_child_test() -> Expected = #xmlel{name = <<"foo">>, children = [Child1, Child3]}, ?exmlAssertEqual(Expected, exml:insert_new_child(El, Child2)). -sort_xmlel_test() -> - Attrs = [{<<"attr1">>, <<"bar">>}, {<<"attr2">>, <<"baz">>}], - El1 = #xmlel{ - name = <<"foo">>, - attrs = Attrs, - children = [#xmlcdata{ content = <<"some value">> }] - }, - El2 = El1#xmlel{ attrs = lists:reverse(Attrs) }, - ?assertNotEqual(El1, El2), - ?assertEqual(exml:xml_sort(El1), exml:xml_sort(El2)). - sort_xmlel_nested_test() -> - Attrs = [{<<"attr1">>, <<"bar">>}, {<<"attr2">>, <<"baz">>}], + Attrs = #{<<"attr1">> => <<"bar">>, <<"attr2">> => <<"baz">>}, CData = [#xmlcdata{ content = <<"some value">> }], Nested1 = #xmlel{ name = <<"n1">>, @@ -138,7 +127,7 @@ sort_xmlel_nested_test() -> }, Nested2 = #xmlel{ name = <<"n2">>, - attrs = lists:reverse(Attrs), + attrs = Attrs, children = CData }, Children = [Nested1, Nested2], @@ -152,10 +141,9 @@ sort_xmlel_nested_test() -> ?assertNotEqual(exml:xml_sort(El1), exml:xml_sort(El2)). sort_xmlstreamstart_test() -> - Attrs = [{<<"attr1">>, <<"bar">>}, {<<"attr2">>, <<"baz">>}], + Attrs = #{<<"attr1">> => <<"bar">>, <<"attr2">> => <<"baz">>}, SS1 = #xmlstreamstart{name = <<"n1">>, attrs = Attrs}, - SS2 = SS1#xmlstreamstart{attrs = lists:reverse(Attrs)}, - ?assertNotEqual(SS1, SS2), + SS2 = SS1#xmlstreamstart{attrs = Attrs}, ?assertEqual(exml:xml_sort(SS1), exml:xml_sort(SS2)). sort_xmlstreamend_test() -> @@ -167,7 +155,7 @@ sort_xmlstreamend_test() -> sort_xmlel_list_test() -> El1 = #xmlel{ name = <<"foo">>, - attrs = [{<<"attr1">>, <<"bar">>}], + attrs = #{<<"attr1">> => <<"bar">>}, children = [#xmlcdata{ content = <<"some value">> }] }, El2 = El1#xmlel{ name = <<"baz">> }, @@ -177,22 +165,22 @@ sort_xmlel_list_test() -> ?assertEqual(exml:xml_sort(L1), exml:xml_sort(L2)). assert_xmlel_equal_macro_positive_test() -> - Attrs = [{<<"attr1">>, <<"bar">>}, {<<"attr2">>, <<"baz">>}], + Attrs = #{<<"attr1">> => <<"bar">>, <<"attr2">> => <<"baz">>}, El1 = #xmlel{ name = <<"foo">>, attrs = Attrs, - children = [#xmlcdata{ content = <<"some value">> }] + children = [#xmlcdata{content = <<"some value">>}] }, - El2 = El1#xmlel{ attrs = lists:reverse(Attrs) }, + El2 = El1#xmlel{attrs = Attrs}, ?exmlAssertEqual(El1, El2). assert_xmlel_equal_macro_negative_test() -> El1 = #xmlel{ name = <<"foo">>, - attrs = [{<<"attr1">>, <<"bar">>}, {<<"attr2">>, <<"baz">>}], - children = [#xmlcdata{ content = <<"some value">> }] + attrs = #{<<"attr1">> => <<"bar">>, <<"attr2">> => <<"baz">>}, + children = [#xmlcdata{content = <<"some value">>}] }, - El2 = El1#xmlel{ attrs = [] }, + El2 = El1#xmlel{attrs = #{}}, ?assertError({exmlAssertEqual, [_, _, _, {expected, El1}, {value, El2}]}, ?exmlAssertEqual(El1, El2)). @@ -203,7 +191,7 @@ throws_error_when_record_is_invalid_test() -> to_binary_arbitrary_stream_elements_test() -> Elements = [#xmlcdata{content = <<"content">>}, #xmlstreamend{name = <<"endname">>}, - #xmlstreamstart{name = <<"name">>, attrs = [{<<"a">>, <<"b">>}]}], + #xmlstreamstart{name = <<"name">>, attrs = #{<<"a">> => <<"b">>}}], ?assertEqual(<<"content">>, exml:to_binary(Elements)). parse(Doc) ->