lib/edgedb/types/named_tuple.ex

defmodule EdgeDB.NamedTuple do
  @moduledoc """
  An immutable value representing an EdgeDB named tuple value.

  `EdgeDB.NamedTuple` implements `Access` behavior to access fields
    by index or key and `Enumerable` protocol for iterating over tuple values.

  ```elixir
  iex(1)> {:ok, pid} = EdgeDB.start_link()
  iex(2)> nt = EdgeDB.query_required_single!(pid, "select (a := 1, b := 'a', c := [3])")
  #EdgeDB.NamedTuple<a: 1, b: "a", c: [3]>
  iex(3)> nt[:b]
  "a"
  iex(4)> nt["c"]
  [3]
  iex(4)> nt[0]
  1
  ```
  """

  @behaviour Access

  defstruct [
    :__fields_ordering__,
    :__items__
  ]

  @typedoc """
  An immutable value representing an EdgeDB named tuple value.
  """
  @opaque t() :: %__MODULE__{}

  @doc """
  Convert a named tuple to a regular erlang tuple.

  ```elixir
  iex(1)> {:ok, pid} = EdgeDB.start_link()
  iex(2)> nt = EdgeDB.query_required_single!(pid, "select (a := 1, b := 'a', c := [3])")
  iex(3)> EdgeDB.NamedTuple.to_tuple(nt)
  {1, "a", [3]}
  ```
  """
  @spec to_tuple(t()) :: tuple()
  def to_tuple(%__MODULE__{} = nt) do
    nt
    |> Enum.into([])
    |> List.to_tuple()
  end

  @doc since: "0.3.0"
  @doc """
  Convert a named tuple into a regular map.

  ```elixir
  iex(1)> {:ok, pid} = EdgeDB.start_link()
  iex(2)> nt = EdgeDB.query_required_single!(pid, "select (a := 1, b := 'a', c := [3])")
  iex(3)> EdgeDB.NamedTuple.to_map(nt)
  %{"a" => 1, "b" => "a", "c" => [3]}
  ```
  """
  @spec to_map(t()) :: %{String.t() => term()}
  def to_map(%__MODULE__{__items__: items}) do
    items
  end

  @doc """
  Get named tuple keys.

  ```elixir
  iex(1)> {:ok, pid} = EdgeDB.start_link()
  iex(2)> nt = EdgeDB.query_required_single!(pid, "select (a := 1, b := 'a', c := [3])")
  iex(3)> EdgeDB.NamedTuple.keys(nt)
  ["a", "b", "c"]
  ```
  """
  @spec keys(t()) :: list(String.t())
  def keys(%__MODULE__{__fields_ordering__: fields_order}) do
    fields_order
    |> Enum.sort()
    |> Enum.map(fn {_index, name} ->
      name
    end)
  end

  @impl Access
  def fetch(%__MODULE__{__items__: items, __fields_ordering__: fields_order}, index)
      when is_integer(index) do
    with {:ok, name} <- Map.fetch(fields_order, index) do
      Map.fetch(items, name)
    end
  rescue
    ArgumentError ->
      :error
  end

  @impl Access
  def fetch(%__MODULE__{} = tuple, key) when is_atom(key) do
    fetch(tuple, Atom.to_string(key))
  end

  @impl Access
  def fetch(%__MODULE__{__items__: items}, key) when is_binary(key) do
    Map.fetch(items, key)
  end

  @impl Access
  def get_and_update(%__MODULE__{}, _key, _function) do
    raise EdgeDB.InterfaceError.new("named tuples can't be mutated")
  end

  @impl Access
  def pop(%__MODULE__{}, _key) do
    raise EdgeDB.InterfaceError.new("named tuples can't be mutated")
  end
end

defimpl Enumerable, for: EdgeDB.NamedTuple do
  @impl Enumerable
  def count(%EdgeDB.NamedTuple{__items__: items}) do
    Enumerable.count(items)
  end

  @impl Enumerable
  def member?(%EdgeDB.NamedTuple{__items__: items}, element) do
    items
    |> Map.values()
    |> Enumerable.member?(element)
  end

  @impl Enumerable
  def reduce(%EdgeDB.NamedTuple{__items__: items, __fields_ordering__: fields_order}, acc, fun) do
    fields_order
    |> Enum.sort()
    |> Enum.map(fn {_index, name} ->
      items[name]
    end)
    |> Enumerable.reduce(acc, fun)
  end

  @impl Enumerable
  def slice(%EdgeDB.NamedTuple{__items__: items, __fields_ordering__: fields_order}) do
    fields_order
    |> Enum.sort()
    |> Enum.map(fn {_index, name} ->
      items[name]
    end)
    |> Enumerable.slice()
  end
end

defimpl Inspect, for: EdgeDB.NamedTuple do
  import Inspect.Algebra

  @impl Inspect
  def inspect(%EdgeDB.NamedTuple{__items__: items, __fields_ordering__: fields_order}, _opts) do
    {max_index, _name} =
      Enum.max(fields_order, fn ->
        {nil, nil}
      end)

    elements_docs =
      fields_order
      |> Enum.sort()
      |> Enum.map(fn {index, name} ->
        {index, glue(name, ": ", inspect(items[name]))}
      end)
      |> Enum.map(fn
        {^max_index, doc} ->
          doc

        {_index, doc} ->
          concat(doc, ", ")
      end)

    concat(["#EdgeDB.NamedTuple<", concat(elements_docs), ">"])
  end
end