lib/elven_gard/network/socket.ex

defmodule ElvenGard.Network.Socket do
  @moduledoc ~S"""
  Manage a socket.

  This module provides functionality for managing a socket in the network protocol.
  A socket is a connection between the server and a client. It maintains various
  socket fields, such as the socket ID, socket assigns, transport information,
  and the packet network encoder used for sending data.

  ## Socket fields

  - `:id`: The unique string ID of the socket.
  - `:assigns`: A map of socket assigns, which can be used to store custom data
    associated with the socket. The default value is `%{}`.
  - `:transport`: The [Ranch transport](https://ninenines.eu/docs/en/ranch/2.0/guide/transports/)
    used for the socket.
  - `:transport_pid`: The PID (Process ID) of the socket's transport process.
  - `:remaining`: The remaining bytes after receiving and packet deserialization.
  - `:encoder`: The `ElvenGard.Network.NetworkCodec` module used to encode packets
    in the `send/2` function.

  """

  alias __MODULE__
  alias ElvenGard.Network.UUID

  defstruct id: nil,
            transport: nil,
            transport_pid: nil,
            remaining: <<>>,
            assigns: %{},
            encoder: :unset

  @type t :: %Socket{
          id: String.t(),
          transport: atom,
          transport_pid: pid,
          remaining: bitstring,
          assigns: map,
          encoder: module | :unset
        }

  @doc """
  Create a new socket structure.

  This function initializes a new socket with the given `transport_pid`, `transport`,
  and `encoder` module.
  """
  @spec new(pid, atom, module) :: Socket.t()
  def new(transport_pid, transport, encoder) do
    %Socket{
      id: UUID.uuid4(),
      transport_pid: transport_pid,
      transport: transport,
      encoder: encoder
    }
  end

  @doc """
  Send a packet to the client.

  This function sends a packet to the client through the socket's transport.
  If the socket's `encoder` is set to `:unset`, the data is sent as is.
  Otherwise, the `encoder` module is used to serialize the data before sending it.

  ## Examples

      iex> ElvenGard.Network.Socket.send(socket, %LoginResponse{status: 200, message: "Welcome!"})
      :ok

  """
  @spec send(Socket.t(), struct() | iodata()) :: :ok | {:error, atom}
  def send(%Socket{encoder: :unset} = socket, data) do
    %Socket{transport: transport, transport_pid: transport_pid} = socket
    transport.send(transport_pid, data)
  end

  def send(%Socket{} = socket, message) do
    %Socket{transport: transport, transport_pid: transport_pid, encoder: encoder} = socket
    data = encoder.encode(message, socket)
    transport.send(transport_pid, data)
  end

  @doc """
  Adds key value pairs to socket assigns.

  A single key value pair may be passed, a keyword list or map
  of assigns may be provided to be merged into existing socket
  assigns.

  ## Examples

      iex> assign(socket, :name, "ElvenGard")
      iex> socket.assigns.name == "ElvenGard"
      true

      iex> assign(socket, name: "ElvenGard", logo: "🌸")
      iex> socket.assigns.name == "ElvenGard"
      true
      iex> socket.assigns.logo == "🌸"
      true
  """
  @spec assign(Socket.t(), atom, any) :: Socket.t()
  def assign(%Socket{} = socket, key, value) do
    assign(socket, [{key, value}])
  end

  @spec assign(Socket.t(), map | keyword) :: Socket.t()
  def assign(%Socket{} = socket, attrs) when is_map(attrs) or is_list(attrs) do
    %{socket | assigns: Map.merge(socket.assigns, Map.new(attrs))}
  end
end