lib/ami/packet.ex

defmodule AMI.Packet do
  defguardp is_valid_text(input) when is_bitstring(input) and byte_size(input) > 0
  defguardp is_valid_list(input) when is_map(input)

  @typedoc """
  AMI Event received from Asterisk. It is a map with fields as header name and
  header values. Header value is a list as AMI supports multiple fields with
  same name. For example, following AMI Event:

  ```
    Event: Status
    Name: Channels
    Var: foo=1
    Var: bar=AAA
  ```

  Will be represented as:

  ```
    %AMI.Packet{
      "Event" => ["Status"],
      "Name" => ["Channels"],
      "Var" => ["foo=1", "bar=AAA"]
    }
  ```

  """
  @type t :: map()

  @doc """
  Parse input Event text and convert to AMI.Packet

  `input` AMI event received from Asterisk
  """
  @spec parse(input :: String.t()) :: {:ok, AMI.Packet.t()} | :error
  def parse(input) when is_valid_text(input) do
    map =
      String.split(input, "\r\n")
      |> Enum.filter(&(String.length(&1) > 0))
      |> Enum.map(&String.split(&1, ~r{\s*:\s*}, parts: 2))
      |> fold

    {:ok, map}
  end

  def parse(_input) do
    {:error, :invalid_input}
  end

  @doc """
  Check if received AMI packet is event. Returns true or false
  """
  @spec is_event?(pack :: AMI.Packet.t()) :: boolean()
  def is_event?(pack) when is_valid_list(pack) do
    case field(pack, "Event") do
      {:ok, _} -> true
      _ -> false
    end
  end

  def is_event?(_map), do: false

  @doc """
  Search field value(s) in AMI Packet.

  `map` is AMI.Packet to search

  `name` field name to search
  """
  @spec field(pack :: t(), name :: String.t()) :: {:ok, list()} | {:error, :not_found}
  def field(pack, name) do
    case Enum.find(pack, fn {k, _} -> String.downcase(name) == String.downcase(k) end) do
      {_, val} -> {:ok, val}
      _ -> {:error, :not_found}
    end
  end

  @doc """
  Converts AMI.Packet to JSON format
  """
  def to_json(pack) when is_map(pack) do
    "{" <> to_json(Map.to_list(pack)) <> "}"
  end

  def to_json([{k, v} | t]) do
    ~s("#{k}":) <>
      cond do
        length(v) > 1 -> inspect(v)
        true -> ~s("#{v}")
      end <>
      cond do
        length(t) > 0 -> "," <> to_json(t)
        true -> ""
      end
  end

  def to_json([]), do: ""

  def to_json(_map), do: {:error, :invalid_input}

  defp fold(map) do
    map
    |> Enum.map(fn [k, v] -> [k, [v]] end)
    |> List.foldr(Map.new(), fn [k, v], map ->
      case Map.fetch(map, k) do
        {:ok, val} -> Map.put(map, k, v ++ val)
        _ -> Map.put(map, k, v)
      end
    end)
  end
end