lib/ex_sdp/origin.ex

defmodule ExSDP.Origin do
  @moduledoc """
  This module represents the Origin field of SDP that represents the originator of the session.

  If the username is set to `-` the originating host does not support the concept of user IDs.

  The username MUST NOT contain spaces.

  For more details please see [RFC4566 Section 5.2](https://tools.ietf.org/html/rfc4566#section-5.2)
  """
  use Bunch.Access

  alias ExSDP.{Address, Utils}

  @enforce_keys [
    :session_id,
    :session_version,
    :address
  ]
  defstruct [username: "-", network_type: "IN"] ++ @enforce_keys

  @type t :: %__MODULE__{
          username: binary(),
          session_id: integer(),
          session_version: integer(),
          network_type: binary(),
          address: Address.t()
        }

  @doc """
  Returns new origin struct.

  By default:
  * `username` is `-`
  * `session_id` is random 64 bit number
  * `session_version` is `0`
  * `address` is `{127, 0, 0, 1}`
  """
  @spec new(
          username: binary(),
          session_id: integer(),
          session_version: integer(),
          address: Address.t()
        ) :: t()
  def new(opts \\ []) do
    %__MODULE__{
      username: Keyword.get(opts, :username, "-"),
      session_id: Keyword.get(opts, :session_id, generate_random()),
      session_version: Keyword.get(opts, :session_version, 0),
      address: Keyword.get(opts, :address, {127, 0, 0, 1})
    }
  end

  @spec parse(binary()) ::
          {:ok, t()} | {:error, :invalid_addrtype | :invalid_address}
  def parse(origin) do
    with {:ok, [username, sess_id, sess_version, nettype, addrtype, address]} <-
           Utils.split(origin, " ", 6),
         {:ok, addrtype} <- Address.parse_addrtype(addrtype),
         {:ok, address} <- Address.parse_address(address) do
      # check whether fqdn
      address = if is_binary(address), do: {addrtype, address}, else: address

      origin = %__MODULE__{
        username: username,
        session_id: String.to_integer(sess_id),
        session_version: String.to_integer(sess_version),
        network_type: nettype,
        address: address
      }

      {:ok, origin}
    else
      {:error, _reason} = error -> error
    end
  end

  @doc """
  Increments `session_version` field.

  Can be used while sending offer/answer again.
  """
  @spec bump_version(t()) :: {:ok, t()}
  def bump_version(origin), do: {:ok, %{origin | session_version: origin.session_version + 1}}

  defp generate_random(), do: :crypto.strong_rand_bytes(7) |> :binary.decode_unsigned()
end

defimpl String.Chars, for: ExSDP.Origin do
  alias ExSDP.Address

  @impl true
  def to_string(origin) do
    """
    #{origin.username} \
    #{origin.session_id} \
    #{origin.session_version} \
    #{origin.network_type} \
    #{Address.get_addrtype(origin.address)} \
    #{Address.serialize_address(origin.address)}\
    """
  end
end