2.4 KiB · text History 4b6fe3b
defmodule OhMyExOhIrcd.IRC.Message do
  @moduledoc """
  Parses and renders RFC 1459 / 2812-ish IRC lines.

      :prefix COMMAND param1 param2 :trailing with spaces

  Tags (IRCv3) are not handled here.
  """

  @type t :: %__MODULE__{
          prefix: nil | String.t(),
          command: String.t(),
          params: [String.t()]
        }

  defstruct prefix: nil, command: "", params: []

  @doc "Parse a single line (no CRLF) into a Message struct."
  @spec parse(String.t()) :: {:ok, t()} | {:error, :empty}
  def parse(line) when is_binary(line) do
    case String.trim(line) do
      "" -> {:error, :empty}
      trimmed -> {:ok, do_parse(trimmed)}
    end
  end

  defp do_parse(":" <> rest) do
    [prefix, rest2] = String.split(rest, " ", parts: 2)
    %__MODULE__{} = parsed = do_parse(rest2)
    %{parsed | prefix: prefix}
  end

  defp do_parse(rest) do
    {command, params} =
      case String.split(rest, " ", parts: 2) do
        [cmd] -> {cmd, []}
        [cmd, params_str] -> {cmd, parse_params(params_str)}
      end

    %__MODULE__{command: String.upcase(command), params: params}
  end

  defp parse_params(""), do: []

  defp parse_params(":" <> trailing), do: [trailing]

  defp parse_params(str) do
    case String.split(str, " ", parts: 2) do
      [param] -> [param]
      [param, rest] -> [param | parse_params(rest)]
    end
  end

  @doc "Render a Message struct to a wire line, *without* CRLF."
  @spec encode(t()) :: iodata()
  def encode(%__MODULE__{prefix: prefix, command: cmd, params: params}) do
    [prefix_part(prefix), cmd, params_part(params)]
  end

  @doc "Render a Message struct to a wire line, *with* CRLF, ready to send."
  @spec to_iodata(t()) :: iodata()
  def to_iodata(msg), do: [encode(msg), "\r\n"]

  defp prefix_part(nil), do: ""
  defp prefix_part(p), do: [":", p, " "]

  defp params_part([]), do: ""

  defp params_part(params) do
    {leading, [last]} = Enum.split(params, length(params) - 1)
    leading_part = Enum.map(leading, &[" ", &1])
    last_part = [" ", maybe_colon(last)]
    [leading_part, last_part]
  end

  defp maybe_colon(""), do: ":"
  defp maybe_colon(":" <> _ = p), do: [":", p]

  defp maybe_colon(p) do
    if String.contains?(p, " "), do: [":", p], else: p
  end

  @doc "Convenience: build a Message struct."
  def build(command, params, prefix \\ nil) do
    %__MODULE__{prefix: prefix, command: command, params: params}
  end
end