lib/carta/plato.ex

defmodule Camarero.Plato do
  @moduledoc """
  This behaviour is high-level abstraction of the container begind handlers.

  All handlers are supposed to implement this behaviour. The simplest way
  is to `use Camarero.Plato` in the handler module; that will inject
  the default boilerlate using `%{binary() => any()}` map as a container behind.

  Default implementation uses `Camarero.Tapas` as low-level container implementation.
  """

  @typedoc "HTTP status code"
  @type status_code :: non_neg_integer()

  @doc "Returns the container itself, as is"
  @callback plato_all() :: Camarero.Tapas.t()
  @doc "Returns the value for the key specified"
  @callback plato_get(key :: binary() | atom()) ::
              {:ok, any()} | :error | {:error, {status_code(), map()}}
  @doc "Sets the value for the key specified (intended to be used from the application)"
  @callback plato_put(key :: binary() | atom(), value :: any()) ::
              :ok | {binary(), status_code()} | binary()
  @doc "Deletes the key-value pair for the key specified"
  @callback plato_delete(key :: binary() | atom()) ::
              nil | {binary(), status_code()} | binary()
  @doc "Returns the route this module is supposed to be mounted to"
  @callback plato_route() :: binary()
  @doc "Returns the key-value map out of a random input"
  @callback reshape(map()) :: map()

  @doc false
  defmacro __using__(opts \\ []) do
    {into, opts} = Keyword.pop(opts, :into, {:%{}, [], []})
    {deep, opts} = Keyword.pop(opts, :deep, false)

    into =
      quote location: :keep do
        Enum.into(unquote(into), %{}, fn {k, v} -> {to_string(k), v} end)
      end

    quote do
      use GenServer
      use Camarero.Tapas, into: unquote(into)

      @behaviour Camarero.Plato

      @impl Camarero.Plato
      def plato_all, do: GenServer.call(__MODULE__, :plato_all)

      @impl Camarero.Plato
      def plato_get(key) when is_atom(key),
        do: key |> to_string() |> plato_get()

      @impl Camarero.Plato
      def plato_get(key) when is_binary(key),
        do: GenServer.call(__MODULE__, {:plato_get, key})

      @impl Camarero.Plato
      def plato_put(key, value) when is_atom(key),
        do: key |> to_string() |> plato_put(value)

      @impl Camarero.Plato
      def plato_put(key, value) when is_binary(key),
        do: GenServer.cast(__MODULE__, {:plato_put, {key, value}})

      @impl Camarero.Plato
      def plato_delete(key) when is_atom(key),
        do: key |> to_string() |> plato_delete()

      @impl Camarero.Plato
      def plato_delete(key) when is_binary(key),
        do: GenServer.call(__MODULE__, {:plato_delete, key})

      @impl Camarero.Plato
      case unquote(deep) do
        false ->
          def plato_route do
            __MODULE__
            |> Macro.underscore()
            |> String.split("/")
            |> Enum.reverse()
            |> hd()
          end

        true ->
          def plato_route do
            __MODULE__
            |> Macro.underscore()
            |> String.trim_leading("/")
            |> String.trim_leading("camarero/carta")
            |> String.trim_leading("/")
          end

        path when is_binary(path) ->
          def plato_route do
            path = String.trim(path, "/")

            __MODULE__
            |> Macro.underscore()
            |> String.trim_leading("/")
            |> String.trim_leading("path")
            |> String.trim_leading("/")
          end
      end

      @impl Camarero.Plato
      def reshape(%{"key" => _, "value" => _} = map), do: map
      def reshape(%{"id" => id} = map), do: %{"key" => id, "value" => map}
      def reshape(%{"uuid" => id} = map), do: %{"key" => id, "value" => map}
      def reshape(map), do: map

      @doc ~s"""
      Starts the `#{__MODULE__}` linked to the current process.
      """
      @spec start_link(into :: Enum.t(), opts :: Keyword.t()) ::
              {:ok, pid()} | {:error, {:already_started, pid()} | term()}
      def start_link(into \\ [], opts \\ unquote(opts))

      def start_link([], opts), do: start_link(tapas_into(), opts)

      def start_link(into, opts) do
        GenServer.start_link(
          __MODULE__,
          into,
          Keyword.put_new(opts, :name, __MODULE__)
        )
      end

      @impl GenServer
      def init(into), do: {:ok, into}

      @impl GenServer
      def handle_call(:plato_all, _from, state),
        do: {:reply, state, state}

      @impl GenServer
      def handle_call({:plato_get, key}, _from, state) do
        {:reply, tapas_get(state, key), state}
      end

      @impl GenServer
      def handle_cast({:plato_put, {key, value}}, state) do
        {_, result} = tapas_put(state, key, value)
        {:noreply, result}
      end

      @impl GenServer
      def handle_call({:plato_delete, key}, _from, state) do
        {value, result} = tapas_delete(state, key)
        {:reply, value, result}
      end

      defoverridable Camarero.Plato
    end
  end
end