lib/gsmlg/socket/udp.ex

defmodule GSMLG.Socket.UDP do
  @moduledoc """
  This module wraps a UDP socket using `gen_udp`.

  ## Options

  When creating a socket you can pass a series of options to use for it.

  * `:as` sets the kind of value returned by recv, either `:binary` or `:list`,
    the default is `:binary`.
  * `:mode` can be either `:passive` or `:active`, default is `:passive`
  * `:local` must be a keyword list
    - `:address` the local address to use
    - `:fd` an already opened file descriptor to use
  * `:version` sets the IP version to use
  * `:broadcast` enables broadcast sending

  ## Examples

      server = GSMLG.Socket.UDP.open!(1337)

      { data, client } = server |> GSMLG.Socket.Datagram.recv!
      server |> GSMLG.Socket.Datagram.send! data, client

  """

  @type t :: port

  use GSMLG.Socket.Helpers

  @doc """
  Create a UDP socket listening on an OS chosen port, use `local` to know the
  port it was bound on.
  """
  @spec open :: {:ok, t} | {:error, Error.t()}
  def open do
    open(0, [])
  end

  @doc """
  Create a UDP socket listening on an OS chosen port, use `local` to know the
  port it was bound on, raising if an error occurs.
  """
  @spec open! :: t | no_return
  defbang(open)

  @doc """
  Create a UDP socket listening on the given port or using the given options.
  """
  @spec open(:inet.port_number() | Keyword.t()) :: {:ok, t} | {:error, Error.t()}
  def open(port) when port |> is_integer do
    open(port, [])
  end

  def open(options) when options |> is_list do
    open(0, options)
  end

  @doc """
  Create a UDP socket listening on the given port or using the given options,
  raising if an error occurs.
  """
  @spec open!(:inet.port_number() | Keyword.t()) :: t | no_return
  defbang(open(port_or_options))

  @doc """
  Create a UDP socket listening on the given port and using the given options.
  """
  @spec open(:inet.port_number(), Keyword.t()) :: {:ok, t} | {:error, Error.t()}
  def open(port, options) do
    options = Keyword.put_new(options, :mode, :passive)

    :gen_udp.open(port, arguments(options))
  end

  @doc """
  Create a UDP socket listening on the given port and using the given options,
  raising if an error occurs.
  """
  @spec open!(:inet.port_number(), Keyword.t()) :: t | no_return
  defbang(open(port, options))

  @doc """
  Set the process which will receive the messages.
  """
  @spec process(t | port, pid) :: :ok | {:error, :closed | :not_owner | Error.t()}
  def process(socket, pid) when socket |> is_port do
    :gen_udp.controlling_process(socket, pid)
  end

  @doc """
  Set the process which will receive the messages, raising if an error occurs.
  """
  @spec process!(t | port, pid) :: :ok | no_return
  def process!(socket, pid) do
    case process(socket, pid) do
      :ok ->
        :ok

      :closed ->
        raise RuntimeError, message: "the socket is closed"

      :not_owner ->
        raise RuntimeError, message: "the current process isn't the owner"

      code ->
        raise GSMLG.Socket.Error, reason: code
    end
  end

  @doc """
  Set options of the socket.
  """
  @spec options(t, Keyword.t()) :: :ok | {:error, Error.t()}
  def options(socket, opts) when socket |> is_port do
    :inet.setopts(socket, arguments(opts))
  end

  @doc """
  Convert UDP options to `:inet.setopts` compatible arguments.
  """
  @spec arguments(Keyword.t()) :: list
  def arguments(options) do
    options =
      options
      |> Keyword.put_new(:as, :binary)

    options =
      Enum.group_by(options, fn
        {:as, _} -> true
        {:local, _} -> true
        {:version, _} -> true
        {:broadcast, _} -> true
        {:multicast, _} -> true
        {:membership, _} -> true
        _ -> false
      end)

    {local, global} = {
      Map.get(options, true, []),
      Map.get(options, false, [])
    }

    GSMLG.Socket.arguments(global) ++
      Enum.flat_map(local, fn
        {:as, :binary} ->
          [:binary]

        {:as, :list} ->
          [:list]

        {:local, options} ->
          Enum.flat_map(options, fn
            {:address, address} ->
              [{:ip, GSMLG.Socket.Address.parse(address)}]

            {:fd, fd} ->
              [{:fd, fd}]
          end)

        {:version, 4} ->
          [:inet]

        {:version, 6} ->
          [:inet6]

        {:broadcast, broadcast} ->
          [{:broadcast, broadcast}]

        {:multicast, options} ->
          Enum.flat_map(options, fn
            {:address, address} ->
              [{:multicast_if, GSMLG.Socket.Address.parse(address)}]

            {:loop, loop} ->
              [{:multicast_loop, loop}]

            {:ttl, ttl} ->
              [{:multicast_ttl, ttl}]
          end)

        {:membership, membership} ->
          [{:add_membership, membership}]
      end)
  end
end