lib/exampple/xml/xmlel.ex

defmodule Exampple.Xml.Xmlel do
  @moduledoc """
  Xmlel is a struct data which is intended to help with the parsing
  of the XML elements.
  """

  alias Exampple.Xml.Xmlel

  @type attr_name :: binary
  @type attr_value :: binary
  @type attrs :: %{attr_name => attr_value}

  @typedoc """
  Xmlel.`t` defines the `xmlel` element which contains the `name`, the
  `attrs` (attributes) and `children` for the XML tags.
  """
  @type t :: %__MODULE__{name: binary, attrs: attrs, children: [t | binary | struct]}
  @type children :: [t] | [String.t()]

  defstruct name: nil, attrs: %{}, children: []

  @doc """
  Creates a Xmlel struct passing the `name` of the stanza, the `attrs`
  as a map or keyword list to create the attributes and `children` for
  the payload of the XML tag. This is not recursive so it's intended
  the children has to be in a correct format.

  The children could be or binaries (strings) representing CDATA or other
  `Exampple.Xml.Xmlel` elements.

  Examples:
      iex> Exampple.Xml.Xmlel.new("foo")
      %Exampple.Xml.Xmlel{attrs: %{}, children: [], name: "foo"}

      iex> Exampple.Xml.Xmlel.new("bar", %{"id" => "10"})
      %Exampple.Xml.Xmlel{attrs: %{"id" => "10"}, children: [], name: "bar"}

      iex> Exampple.Xml.Xmlel.new("bar", [{"id", "10"}])
      %Exampple.Xml.Xmlel{attrs: %{"id" => "10"}, children: [], name: "bar"}
  """
  @spec new(name :: binary, attrs | [{attr_name, attr_value}], children) :: t
  def new(name, attrs \\ %{}, children \\ [])

  def new(name, attrs, children) when is_list(attrs) do
    new(name, Enum.into(attrs, %{}), children)
  end

  def new(name, attrs, children) when is_map(attrs) do
    %Xmlel{name: name, attrs: attrs, children: children}
  end

  @doc """
  Sigil to use ~X to provide XML `string` and transform it to Xmlel struct.
  Note that we are not using `addons`.

  Examples:
      iex> import Exampple.Xml.Xmlel
      iex> ~X|<foo>
      iex> </foo>
      iex> |
      %Exampple.Xml.Xmlel{attrs: %{}, children: ["\\n "], name: "foo"}
  """
  def sigil_X(string, _addons) do
    {xml, _rest} = parse(string)
    xml
  end

  @doc """
  Sigil to use ~x to provide XML `string` and transform it to Xmlel struct
  removing spaces and breaking lines.
  Note that we are not using `addons`.

  Examples:
      iex> import Exampple.Xml.Xmlel
      iex> ~x|<foo>
      iex> </foo>
      iex> |
      %Exampple.Xml.Xmlel{attrs: %{}, children: [], name: "foo"}
  """
  def sigil_x(string, _addons) do
    string
    |> parse()
    |> clean_spaces()
  end

  @doc """
  Parser a `xml` string into `Exampple.Xml.Xmlel` struct.

  Examples:
      iex> Exampple.Xml.Xmlel.parse("<foo/>")
      {%Exampple.Xml.Xmlel{name: "foo", attrs: %{}, children: []}, ""}

      iex> Exampple.Xml.Xmlel.parse("<foo bar='10'>hello world!</foo>")
      {%Exampple.Xml.Xmlel{name: "foo", attrs: %{"bar" => "10"}, children: ["hello world!"]}, ""}

      iex> Exampple.Xml.Xmlel.parse("<foo><bar>hello world!</bar></foo>")
      {%Exampple.Xml.Xmlel{name: "foo", attrs: %{}, children: [%Exampple.Xml.Xmlel{name: "bar", attrs: %{}, children: ["hello world!"]}]}, ""}

      iex> Exampple.Xml.Xmlel.parse("<foo/><bar/>")
      {%Exampple.Xml.Xmlel{name: "foo", attrs: %{}, children: []}, "<bar/>"}
  """
  def parse(xml) when is_binary(xml) do
    {:halt, [xmlel], more} = Saxy.parse_string(xml, Exampple.Xml.Parser.Simple, [])
    {decode(xmlel), more}
  end

  @doc """
  This function is a helper function to translate the tuples coming
  from Saxy into de `data` parameter to the `Exampple.Xml.Xmlel` structs.

  Examples:
      iex> Exampple.Xml.Xmlel.decode({"foo", [], []})
      %Exampple.Xml.Xmlel{name: "foo", attrs: %{}, children: []}

      iex> Exampple.Xml.Xmlel.decode({"foo", [], [{:characters, "1&1"}]})
      %Exampple.Xml.Xmlel{name: "foo", children: ["1&1"]}

      iex> Exampple.Xml.Xmlel.decode({"bar", [{"id", "10"}], ["Hello!"]})
      %Exampple.Xml.Xmlel{name: "bar", attrs: %{"id" => "10"}, children: ["Hello!"]}
  """
  def decode(data) when is_binary(data), do: data

  def decode({:characters, data}), do: data

  def decode(%Xmlel{attrs: attrs, children: children} = xmlel) do
    children = Enum.map(children, &decode/1)
    %Xmlel{xmlel | attrs: attrs, children: children}
  end

  def decode({name, attrs, children}) do
    attrs = Enum.into(attrs, %{})
    decode(%Xmlel{name: name, attrs: attrs, children: children})
  end

  @doc """
  This function is a helper function to translate the content of the
  `xmlel` structs to the tuples needed by Saxy.

  Examples:
      iex> Exampple.Xml.Xmlel.encode(%Exampple.Xml.Xmlel{name: "foo"})
      {"foo", [], []}

      iex> Exampple.Xml.Xmlel.encode(%Exampple.Xml.Xmlel{name: "bar", attrs: %{"id" => "10"}, children: ["Hello!"]})
      {"bar", [{"id", "10"}], [{:characters, "Hello!"}]}

      iex> Exampple.Xml.Xmlel.encode(%TestBuild{name: "bro"})
      "<bro/>"
  """
  def encode(%Xmlel{} = xmlel) do
    children = Enum.map(xmlel.children, &encode/1)
    {xmlel.name, Enum.into(xmlel.attrs, []), children}
  end

  def encode(content) when is_binary(content), do: {:characters, content}

  def encode(%struct_name{} = struct) do
    builder = Module.concat(Saxy.Builder, struct_name)

    struct
    |> builder.build()
    |> Saxy.encode!(nil)
  end

  defimpl String.Chars, for: __MODULE__ do
    alias Exampple.Xml.Xmlel
    alias Saxy.Encoder
    alias Saxy.Builder

    @doc """
    Implements `to_string/1` to convert a XML entity to a `xmlel`
    representation.

    Examples:
        iex> Exampple.Xml.Xmlel.new("foo") |> to_string()
        "<foo/>"

        iex> Exampple.Xml.Xmlel.new("bar", %{"id" => "10"}) |> to_string()
        "<bar id=\\"10\\"/>"

        iex> query = Exampple.Xml.Xmlel.new("query", %{"xmlns" => "urn:jabber:iq"})
        iex> Exampple.Xml.Xmlel.new("iq", %{"type" => "get"}, [query]) |> to_string()
        "<iq type=\\"get\\"><query xmlns=\\"urn:jabber:iq\\"/></iq>"

        iex> Exampple.Xml.Xmlel.new("query", %{}, ["<going >"]) |> to_string()
        "<query>&lt;going &gt;</query>"
    """
    def to_string(xmlel) do
      xmlel
      |> Xmlel.encode()
      |> Builder.build()
      |> Encoder.encode_to_iodata(nil)
      |> IO.chardata_to_string()
    end
  end

  defimpl Saxy.Builder, for: Xmlel do
    @moduledoc false
    @doc """
    Generates the Saxy tuples from `xmlel` structs.

    Examples:
        iex> Saxy.Builder.build(Exampple.Xml.Xmlel.new("foo", %{}, []))
        {"foo", [], []}
    """
    def build(xmlel) do
      Xmlel.encode(xmlel)
    end
  end

  @doc """
  Retrieve an attribute by `name` from a `xmlel` struct. If the value
  is not found the `default` value is used instead. If `default` is
  not provided then `nil` is used as default value.

  Examples:
      iex> attrs = %{"id" => "100", "name" => "Alice"}
      iex> xmlel = %Exampple.Xml.Xmlel{attrs: attrs}
      iex> Exampple.Xml.Xmlel.get_attr(xmlel, "name")
      "Alice"
      iex> Exampple.Xml.Xmlel.get_attr(xmlel, "surname")
      nil
  """
  def get_attr(%Xmlel{attrs: attrs}, name, default \\ nil) do
    Map.get(attrs, name, default)
  end

  @doc """
  Deletes an attribute by `name` from a `xmlel` struct.

  Examples:
      iex> attrs = %{"id" => "100", "name" => "Alice"}
      iex> xmlel = %Exampple.Xml.Xmlel{attrs: attrs}
      iex> Exampple.Xml.Xmlel.get_attr(xmlel, "name")
      "Alice"
      iex> Exampple.Xml.Xmlel.delete_attr(xmlel, "name")
      iex> |> Exampple.Xml.Xmlel.get_attr("name")
      nil
  """
  def delete_attr(%Xmlel{attrs: attrs} = xmlel, name) do
    %Xmlel{xmlel | attrs: Map.delete(attrs, name)}
  end

  @doc """
  Add or set a `value` by `name` as attribute inside of the `xmlel` struct
  passed as parameter.

  Examples:
      iex> attrs = %{"id" => "100", "name" => "Alice"}
      iex> %Exampple.Xml.Xmlel{attrs: attrs}
      iex> |> Exampple.Xml.Xmlel.put_attr("name", "Bob")
      iex> |> Exampple.Xml.Xmlel.get_attr("name")
      "Bob"
  """
  def put_attr(%Xmlel{attrs: attrs} = xmlel, name, value) do
    %Xmlel{xmlel | attrs: Map.put(attrs, name, value)}
  end

  @doc """
  Add or set one or several attributes using `fields` inside of the `xmlel`
  struct passed as parameter. The `fields` data are in keyword list format.

  Examples:
      iex> fields = %{"id" => "100", "name" => "Alice", "city" => "Cordoba"}
      iex> Exampple.Xml.Xmlel.put_attrs(%Exampple.Xml.Xmlel{name: "foo"}, fields) |> to_string()
      "<foo city=\\"Cordoba\\" id=\\"100\\" name=\\"Alice\\"/>"

      iex> fields = %{"id" => "100", "name" => "Alice", "city" => :"Cordoba"}
      iex> Exampple.Xml.Xmlel.put_attrs(%Exampple.Xml.Xmlel{name: "foo"}, fields) |> to_string()
      "<foo id=\\"100\\" name=\\"Alice\\"/>"
  """
  def put_attrs(xmlel, fields) do
    Enum.reduce(fields, xmlel, fn
      {_field, value}, acc when is_atom(value) -> acc
      {field, value}, acc -> put_attr(acc, field, value)
    end)
  end

  @doc """
  This function removes the extra spaces inside of the stanzas starting from
  `xmlel` to ensure we can perform matching in a proper way.

  Examples:
      iex> "<foo>\\n    <bar>\\n        Hello<br/>world!\\n    </bar>\\n</foo>"
      iex> |> Exampple.Xml.Xmlel.parse()
      iex> |> Exampple.Xml.Xmlel.clean_spaces()
      iex> |> to_string()
      "<foo><bar>Hello<br/>world!</bar></foo>"
  """
  def clean_spaces({xmlel, _rest}), do: clean_spaces(xmlel)

  def clean_spaces(%Xmlel{children: []} = xmlel), do: xmlel

  def clean_spaces(%Xmlel{children: children} = xmlel) do
    children =
      Enum.reduce(children, [], fn
        content, acc when is_binary(content) ->
          content = String.trim(content)
          if content != "", do: [content | acc], else: acc

        %Xmlel{} = x, acc ->
          [clean_spaces(x) | acc]
      end)
      |> Enum.reverse()

    %Xmlel{xmlel | children: children}
  end

  @behaviour Access

  defp split_children(children, name) do
    children
    |> Enum.reduce(
      %{match: [], nonmatch: []},
      fn
        %Xmlel{name: ^name} = el, acc ->
          %{acc | match: [el | acc.match]}

        el, acc ->
          %{acc | nonmatch: [el | acc.nonmatch]}
      end
    )
    |> Enum.map(fn {k, v} -> {k, Enum.reverse(v)} end)
    |> Enum.into(%{})
  end

  @impl Access
  @doc """
  Access the value stored under `key` passing the stanza in
  `Exampple.Xml.Xmlel` format into the `xmlel` parameter.

  Examples:
      iex> import Exampple.Xml.Xmlel
      iex> el = ~x(<foo><c1 v="1"/><c1 v="2"/><c2/></foo>)
      iex> fetch(el, "c1")
      {:ok, [%Exampple.Xml.Xmlel{attrs: %{"v" => "1"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{"v" => "2"}, children: [], name: "c1"}]}
      iex> fetch(el, "nonexistent")
      :error
  """
  def fetch(%Xmlel{children: children}, key) do
    %{match: values} = split_children(children, key)

    if Enum.empty?(values) do
      :error
    else
      {:ok, values}
    end
  end

  @impl Access
  @doc """
  Access the value under `key` and update it at the same time for the `xmlel`
  using the `function` passed as paramter.

  Examples:
      iex> import Exampple.Xml.Xmlel
      iex> el = ~x(<foo><c1 v="1"/><c1 v="2"/><c2/></foo>)
      iex> fun = fn els ->
      iex> values = Enum.map(els, fn %Exampple.Xml.Xmlel{attrs: %{"v" => v}} = el -> %Exampple.Xml.Xmlel{el | attrs: %{"v" => "v" <> v}} end)
      iex> {els, values}
      iex> end
      iex> get_and_update(el, "c1", fun)
      {[%Exampple.Xml.Xmlel{attrs: %{"v" => "1"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{"v" => "2"}, children: [], name: "c1"}], %Exampple.Xml.Xmlel{attrs: %{}, children: [%Exampple.Xml.Xmlel{attrs: %{"v" => "v1"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{"v" => "v2"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{}, children: [], name: "c2"}], name: "foo"}}
      iex> fun = fn _els -> :pop end
      iex> get_and_update(el, "c1", fun)
      {[%Exampple.Xml.Xmlel{attrs: %{"v" => "1"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{"v" => "2"}, children: [], name: "c1"}], %Exampple.Xml.Xmlel{attrs: %{}, children: [%Exampple.Xml.Xmlel{attrs: %{}, children: [], name: "c2"}], name: "foo"}}
  """
  def get_and_update(%Xmlel{children: children} = xmlel, key, function) do
    %{match: match, nonmatch: nonmatch} = split_children(children, key)

    case function.(if Enum.empty?(match), do: nil, else: match) do
      :pop ->
        {match, %Xmlel{xmlel | children: nonmatch}}

      {get_value, update_value} ->
        {get_value, %Xmlel{xmlel | children: update_value ++ nonmatch}}
    end
  end

  @impl Access
  @doc """
  Pop the value under `key` passed an `Exampple.Xml.Xmlel` struct as `element`.

  Examples:
      iex> import Exampple.Xml.Xmlel
      iex> el = ~x(<foo><c1 v="1"/><c1 v="2"/><c2/></foo>)
      iex> pop(el, "c1")
      {[%Exampple.Xml.Xmlel{attrs: %{"v" => "1"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{"v" => "2"}, children: [], name: "c1"}], %Exampple.Xml.Xmlel{attrs: %{}, children: [%Exampple.Xml.Xmlel{attrs: %{}, children: [], name: "c2"}], name: "foo"}}
      iex> pop(el, "nonexistent")
      {[], %Exampple.Xml.Xmlel{attrs: %{}, children: [%Exampple.Xml.Xmlel{attrs: %{"v" => "1"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{"v" => "2"}, children: [], name: "c1"}, %Exampple.Xml.Xmlel{attrs: %{}, children: [], name: "c2"}], name: "foo"}}
  """
  def pop(%Xmlel{children: children} = element, key) do
    case split_children(children, key) do
      %{match: []} ->
        {[], element}

      %{match: match, nonmatch: nonmatch} ->
        {match, %Xmlel{element | children: nonmatch}}
    end
  end
end