defmodule Exml do
@moduledoc """
This module provides a simple interface to parse XML documents and
provide XPath access to the document.
The module provides two functions:
- `parse/{1,2}` parses an XML document and returns a document tree.
- `get/2` provides XPath access to the document.
Example:
```
xml = "<book><title>The Giver</title><author>John Grisham</author></book>"
doc = Exml.parse xml
Exml.get doc, "//book/title"
# => "The Giver"
```
"""
require Record
~w(xmlElement xmlAttribute xmlText xmlObj)a
|> Enum.map(&Record.defrecord(&1, Record.extract(&1, from_lib: "xmerl/include/xmerl.hrl")))
@doc """
Parse XML and return a document tree
"""
def parse(xml_string, options \\ [quiet: true]) when is_binary(xml_string) do
{doc, []} =
xml_string
|> :binary.bin_to_list()
|> :xmerl_scan.string(options)
doc
end
@doc """
Get the value of an XML attribute using XPath from a document tree
"""
def get(node, path) do
xpath(node, path) |> text
end
defp xpath(node, path) do
:xmerl_xpath.string(to_charlist(path), node)
end
defp text([]), do: nil
defp text([item]), do: text(item)
defp text(xmlElement(content: content)), do: text(content)
defp text(xmlAttribute(value: value)), do: List.to_string(value)
defp text(xmlText(value: value)), do: List.to_string(value)
defp text(list) when is_list(list) do
# credo:disable-for-next-line Credo.Check.Refactor.CondStatements
cond do
Enum.all?(list, &parsable/1) -> Enum.map(list, &text(&1))
true -> fatal(list)
end
end
defp text(xmlObj(value: value, type: :number)), do: value
defp text(xmlObj(value: value)), do: List.to_string(value)
defp text(term), do: fatal(term)
defp parsable(term) do
Record.is_record(term, :xmlText) or Record.is_record(term, :xmlElement) or
Record.is_record(term, :xmlAttribute)
end
defp fatal(term) do
raise "Could not extract text value for xmerl node #{inspect(term)}"
end
end