How to parse sub-element list with SweetXml?

I have to parse an XML with the following structure:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

    <root >
        <node id="1"/>
        <node id="2"/>
    </root>

And I have tried the following approache based on xpath:


  def parse_root(root_xml) do
    root_xml
    |> xpath(~x"//root",
      nodes: ~x"./node"l |> transform_by(&parse_node/1)
    )
  end

  def parse_node(node_xml) do
    node_xml
    |> xpath(~x"node", id: ~x"@id")
  end

  test "parse root" do
    xml = """
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>

    <root >
        <node id="1"/>
        <node id="2"/>
    </root>
    """

    assert %{
             nodes: [
               %{id: "id1"},
               %{id: "id2"}
             ]
           } == parse_root(xml)
  end

This fails with the following runtime error:

  2) test parse root (TestSweet)
     test/sweet_test.exs:172
     ** (ArgumentError) errors were found at the given arguments:

       * 1st argument: not a bitstring

     code: } == parse_root(xml)
     stacktrace:
       :erlang.byte_size({:xmlElement, :node, :node, [], {:xmlNamespace, [], []}, [root: 1], 2, [{:xmlAttribute, :id, [], [], [], [node: 2, root: 1], 1, [], ~c"1", false}], [], [], ~c"/Users/ovi/work/eb/playground", :undeclared})
       (sweet_xml 0.7.4) lib/sweet_xml.ex:834: SweetXml.split_last_whitespace/1
       (sweet_xml 0.7.4) lib/sweet_xml.ex:825: anonymous fn/2 in SweetXml.split_by_whitespace/1
       (elixir 1.15.7) lib/stream.ex:990: Stream.do_transform_user/6
       (sweet_xml 0.7.4) lib/sweet_xml.ex:788: anonymous fn/4 in SweetXml.continuation_opts/2
       (xmerl 1.3.31) xmerl_scan.erl:571: :xmerl_scan.scan_document/2
       (xmerl 1.3.31) xmerl_scan.erl:294: :xmerl_scan.string/2
       (sweet_xml 0.7.4) lib/sweet_xml.ex:296: SweetXml.do_parse/2
       (sweet_xml 0.7.4) lib/sweet_xml.ex:281: SweetXml.parse/2
       (sweet_xml 0.7.4) lib/sweet_xml.ex:610: SweetXml.xpath/3
       (sweet_xml 0.7.4) lib/sweet_xml.ex:642: SweetXml.xpath/3
       (sweet_xml 0.7.4) lib/sweet_xml.ex:737: anonymous fn/3 in SweetXml.xmap/3
       (elixir 1.15.7) lib/map.ex:957: Map.get_and_update/3
       (sweet_xml 0.7.4) lib/sweet_xml.ex:737: SweetXml.xmap/3
       test/sweet_test.exs:187: (test)

I also tried to parse the same XML using xmap as you can see below:

def parse_root2(root_xml) do
    root_xml
    |> xmap(~x"//root",
      nodes: [
        ~x"./node",
        id: ~x"@id"
      ]
    )
  end

  test "parse root 2" do
    xml = """
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>

    <root >
        <node id="1"/>
        <node id="2"/>
    </root>
    """

    assert %{
             nodes: [
               %{id: "id1"},
               %{id: "id2"}
             ]
           } == parse_root2(xml)
  end

…and it fails with the following error:

  1) test parse root 2 (TestSweet)
     test/sweet_test.exs:200
     ** (FunctionClauseError) no function clause matching in SweetXml.xmap/3

     The following arguments were given to SweetXml.xmap/3:

         # 1
         "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n\n<root >\n    <node id=\"1\"/>\n    <node id=\"2\"/>\n</root>\n"

         # 2
         %SweetXpath{path: ~c"//root", is_value: true, is_list: false, is_keyword: false, is_optional: false, cast_to: false, transform_fun: &SweetXpath.Priv.self_val/1, namespaces: []}

         # 3
         [nodes: [%SweetXpath{path: ~c"./node", is_value: true, is_list: false, is_keyword: false, is_optional: false, cast_to: false, transform_fun: &SweetXpath.Priv.self_val/1, namespaces: []}, {:id, %SweetXpath{path: ~c"@id", is_value: true, is_list: false, is_keyword: false, is_optional: false, cast_to: false, transform_fun: &SweetXpath.Priv.self_val/1, namespaces: []}}]]

     Attempted function clauses (showing 6 out of 6):

         def xmap(nil, _, %{is_optional: true})
         def xmap(parent, [], atom) when is_atom(atom)
         def xmap(_, [], %{is_keyword: false})
         def xmap(_, [], %{is_keyword: true})
         def xmap(parent, [{label, spec} | tail], is_keyword) when is_list(spec)
         def xmap(parent, [{label, sweet_xpath} | tail], is_keyword)

     code: } == parse_root2(xml)
     stacktrace:
       (sweet_xml 0.7.4) lib/sweet_xml.ex:721: SweetXml.xmap/3
       test/sweet_test.exs:215: (test)

Any suggestions will be highly appreciated.

Thanks in advance!

I tried this in a livebook.

import SweetXml
xml = """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

    <root >
        <node id="1"/>
        <node id="2"/>
    </root>
"""

xml
|> xpath(~x"//root", nodes: [~x"./node"l, id: ~x"@id"i])

%{nodes: [%{id: 1}, %{id: 2}]}

xml
|> xpath(~x"//root", nodes: [~x"./node"l, id: ~x"@id"s |> transform_by(fn x -> "id" <> x end)])

%{nodes: [%{id: "id1"}, %{id: "id2"}]}
2 Likes

Sorry for the late reply. It works!