lib/iso_20022.ex

defmodule ISO20022 do
  @moduledoc """
  ISO 20022 message parsing for Elixir.

  Currently supported message types:

  - `ISO20022.Camt053` — Bank to Customer Statement (camt.053.001.02 through .014)

  ## Example

      {:ok, doc} = ISO20022.Camt053.parse(xml_string)
      [statement | _] = doc.statements
      IO.inspect(statement.account.iban)
  """

  @doc """
  Dispatches parsing to the correct module based on the XML namespace found in the
  document root. Returns an error tuple for unsupported message types.

  Currently only camt.053 is supported. More message types will be added in future releases.
  """
  @spec parse(binary()) ::
          {:ok, ISO20022.Camt053.Document.t()}
          | {:error, term()}
  def parse(xml) when is_binary(xml) do
    case detect_message_type(xml) do
      {:ok, :camt_053} -> ISO20022.Camt053.parse(xml)
      {:ok, other} -> {:error, {:unsupported_message_type, other}}
      {:error, _} = err -> err
    end
  end

  defp detect_message_type(xml) do
    case Saxy.SimpleForm.parse_string(xml, []) do
      {:ok, {_tag, attrs, _children}} ->
        ns = find_namespace(attrs)
        classify_namespace(ns)

      {:error, reason} ->
        {:error, {:parse_error, reason}}
    end
  end

  defp find_namespace(attrs) do
    Enum.find_value(attrs, fn
      {"xmlns", val} -> val
      _ -> nil
    end)
  end

  defp classify_namespace(nil), do: {:error, :no_namespace}

  defp classify_namespace(ns) do
    cond do
      String.contains?(ns, "camt.053") -> {:ok, :camt_053}
      String.contains?(ns, "camt.052") -> {:ok, :camt_052}
      String.contains?(ns, "camt.054") -> {:ok, :camt_054}
      String.contains?(ns, "pain.001") -> {:ok, :pain_001}
      String.contains?(ns, "pacs.008") -> {:ok, :pacs_008}
      true -> {:ok, {:unknown, ns}}
    end
  end
end