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() 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})} [type: "collection"] -> %{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.trim() |> List.wrap() |> Enum.reject(&(&1 == "")) |> List.first() 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({[type: "collection"], elements}), do: Enum.map(only_collection(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) defp only_collection(elements), do: elements |> without_nil() |> Enum.reject(fn {_, value, _} -> value != [] end) end