defmodule Braintree.XML.Decoder do @moduledoc """ XML dumping tailored to encoding params sent by Braintree. """ @type xml :: binary import Braintree.Util, only: [underscorize: 1] import Braintree.XML.Entity, only: [decode: 1] @doc ~S""" Converts an XML document, or fragment, into a map. Type annotation attributes are respected, but all other attributes are ignored. ## Examples iex> Braintree.XML.Decoder.load("12") %{"a" => %{"b" => 1, "c" => "2"}} iex> Braintree.XML.Decoder.load("José") %{"a" => %{"b" => "José"}} iex> Braintree.XML.Decoder.load("First & Last") %{"a" => %{"b" => "First & Last"}} iex> Braintree.XML.Decoder.load(""air quotes"") %{"a" => %{"b" => ~s("air quotes")}} """ @spec load(xml) :: Map.t def load(""), do: %{} def load(xml) do {name, attributes, values} = xml |> decode |> :erlang.bitstring_to_list |> :xmerl_scan.string |> elem(0) |> parse case attributes do [type: "array"] -> %{name => transform({attributes, values})} _ -> %{name => transform(values)} end end defp parse(elements) when is_list(elements), do: Enum.map(elements, &parse/1) defp parse({:xmlElement, name, _, _, _, _, _, attributes, elements, _, _, _}), do: {underscorize(name), parse(attributes), parse(elements)} defp parse({:xmlAttribute, name, _, _, _, _, _, _, value, _}), do: {name, to_string(value)} defp parse({:xmlText, _, _, _, value, _}) do value |> to_string |> String.strip |> List.wrap |> Enum.reject(&(&1 == "")) |> List.first end defp transform(elements) when is_list(elements) do if is_text_list?(elements) do Enum.join(elements, " ") else Enum.into(without_nil(elements), %{}, &transform/1) end end defp transform({[type: "array"], elements}), do: Enum.map(without_nil(elements), &(elem(transform(&1), 1))) defp transform({name, [type: "integer"], [value]}), do: {name, String.to_integer(value)} defp transform({name, [type: "array"], elements}), do: {name, Enum.map(without_nil(elements), &(elem(transform(&1), 1)))} defp transform({name, [type: "boolean"], [value]}), do: {name, value == "true"} defp transform({name, [nil: "true"], []}), do: {name, nil} defp transform({name, _, [value]}), do: {name, value} defp transform({name, _, []}), do: {name, ""} defp transform({name, _, values}), do: {name, transform(values)} defp is_text_list?([last]) when is_binary(last), do: true defp is_text_list?([hd|rest]) when is_binary(hd), do: is_text_list?(rest) defp is_text_list?(_), do: false defp without_nil(list), do: Enum.reject(list, &is_nil/1) end