defmodule AWS.XML do
@moduledoc """
Specifies the behaviour of a XML parser.
You can switch the default XML parser which uses AWS.Util underneath
by defining a different implementation by setting the `:xml_module`
configuration in `AWS.Client`:
client = %AWS.Client{xml_module: MyCustomXMLParser}
AWS.SNS.publish(client, %{})
"""
@doc """
Encodes a map into XML iodata. Raises in case of errors.
"""
@callback encode_to_iodata!(input :: map(), options :: keyword()) :: iodata()
@doc """
Decodes a XML into a map. Raises in case of errors.
"""
@callback decode!(input :: iodata(), options :: keyword()) :: map()
import Record
@text "__text"
defrecord(:xmlElement, extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl"))
defrecord(:xmlText, extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl"))
def encode_to_iodata!(map, _opts \\ []) do
map
|> Map.to_list()
|> Enum.map(&encode_xml_key_value/1)
|> :erlang.iolist_to_binary()
end
def decode!(xml, _opts \\ []) do
# See: https://elixirforum.com/t/utf-8-issue-with-erlang-xmerl-scan-function/1668/9
xml_str = :erlang.binary_to_list(xml)
opts = [{:hook_fun, &hook_fun/2}]
{element, []} = :xmerl_scan.string(xml_str, opts)
element
end
defp encode_xml_key_value({k, v}) when is_binary(k) and is_binary(v) do
["<", k, ">", v, "</", k, ">"]
end
defp encode_xml_key_value({k, values}) when is_binary(k) and is_list(values) do
for v <- values do
encode_xml_key_value({k, v})
end
end
defp encode_xml_key_value({k, v}) when is_binary(k) and is_integer(v) do
["<", k, ">", Integer.to_charlist(v), "</", k, ">"]
end
defp encode_xml_key_value({k, v}) when is_binary(k) and is_float(v) do
["<", k, ">", Float.to_charlist(v), "</", k, ">"]
end
defp encode_xml_key_value({k, v}) when is_binary(k) and is_boolean(v) do
["<", k, ">", to_string(v), "</", k, ">"]
end
defp encode_xml_key_value({k, v}) when is_binary(k) and is_map(v) do
[
"<",
k,
">",
v
|> Map.to_list()
|> Enum.map(&encode_xml_key_value/1),
"</",
k,
">"
]
end
# allow passing a map to use as xml attributes
defp encode_xml_key_value({{k, attrmap}, v})
when is_binary(k) and is_map(attrmap) and is_map(v) do
# note: assume that special chars in av are quoted by caller
attrlist =
for {a, av} <- attrmap do
[" ", to_string(a), "=", "\"#{av}\""]
end
|> List.flatten()
[
"<",
k,
attrlist,
">",
v
|> Map.to_list()
|> Enum.map(&encode_xml_key_value/1),
"</",
k,
">"
]
end
_ = """
Callback hook_fun for xmerl parser
"""
defp hook_fun(element, global_state) when Record.is_record(element, :xmlElement) do
tag = xmlElement(element, :name)
content = xmlElement(element, :content)
value =
case List.foldr(content, :none, &content_to_map/2) do
v = %{@text => text} ->
case String.trim(text) do
"" -> Map.delete(v, @text)
trimmed -> Map.put(v, @text, trimmed)
end
v ->
v
end
{%{Atom.to_string(tag) => value}, global_state}
end
defp hook_fun(text, global_state) when Record.is_record(text, :xmlText) do
text = xmlText(text, :value)
{:unicode.characters_to_binary(text), global_state}
end
_ = """
Convert the content of an Xml node into a map.
When there is more than one element with the same tag name, their
values get merged into a list.
If the content is only text then that is what gets returned.
If the content is a mix between text and child elements, then the
elements are processed as described above and all the text parts
are merged under the `__text' key.
"""
defp content_to_map(x, :none) do
x
end
defp content_to_map(x, acc) when is_map(x) and is_map(acc) do
[{tag, value}] = Map.to_list(x)
case Map.has_key?(acc, tag) do
true ->
update_fun = fn
l when is_list(l) -> [value | l]
v -> [value, v]
end
Map.update!(acc, tag, update_fun)
false ->
Map.put(acc, tag, value)
end
end
defp content_to_map(x, %{@text => text} = acc) when is_binary(x) and is_map(acc) do
%{acc | @text => <<x::binary, text::binary>>}
end
defp content_to_map(x, acc) when is_binary(x) and is_map(acc) do
Map.put(acc, @text, x)
end
defp content_to_map(x, acc) when is_binary(x) and is_binary(acc) do
<<x::binary, acc::binary>>
end
defp content_to_map(x, acc) when is_map(x) and is_binary(acc) do
Map.put(x, @text, acc)
end
end