lib/ash/resource/builder.ex

defmodule Ash.Resource.Builder do
  @moduledoc """
  Tools for transforming resources in DSL Transformers.
  """

  alias Spark.Dsl.Transformer
  use Spark.Dsl.Builder

  @doc """
  Builds and adds a new action unless an action with that name already exists
  """
  @spec add_new_action(
          Spark.Dsl.Builder.input(),
          type :: Ash.Resource.Actions.action_type(),
          name :: atom,
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_new_action(dsl_state, type, name, opts \\ []) do
    if Ash.Resource.Info.action(dsl_state, name) do
      dsl_state
    else
      add_action(dsl_state, type, name, opts)
    end
  end

  @doc """
  Builds and adds an action
  """
  @spec add_action(
          Spark.Dsl.Builder.input(),
          type :: Ash.Resource.Actions.action_type(),
          name :: atom,
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_action(dsl_state, type, name, opts \\ []) do
    with {:ok, action} <- build_action(type, name, opts) do
      Transformer.add_entity(dsl_state, [:actions], action)
    end
  end

  @doc """
  Builds an action
  """
  @spec build_action(
          type :: Ash.Resource.Actions.action_type(),
          name :: atom,
          opts :: Keyword.t()
        ) ::
          {:ok, Ash.Resource.Actions.action()} | {:error, term}
  def build_action(type, name, opts \\ []) do
    with {:ok, opts} <-
           handle_nested_builders(opts, [:changes, :arguments, :metadata, :pagination]) do
      Transformer.build_entity(
        Ash.Resource.Dsl,
        [:actions],
        type,
        Keyword.merge(opts, name: name)
      )
    end
  end

  @doc """
  Builds and adds a new relationship unless a relationship with that name already exists
  """
  @spec add_new_relationship(
          Spark.Dsl.Builder.input(),
          type :: Ash.Resource.Relationships.type(),
          name :: atom,
          destination :: module,
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_new_relationship(dsl_state, type, name, destination, opts \\ []) do
    if Ash.Resource.Info.relationship(dsl_state, name) do
      dsl_state
    else
      add_relationship(dsl_state, type, name, destination, opts)
    end
  end

  @doc """
  Builds and adds an action
  """
  @spec add_relationship(
          Spark.Dsl.Builder.input(),
          type :: Ash.Resource.Relationships.type(),
          name :: atom,
          destination :: module,
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_relationship(dsl_state, type, name, destination, opts \\ []) do
    with {:ok, relationship} <- build_relationship(type, name, destination, opts) do
      Transformer.add_entity(dsl_state, [:relationships], relationship)
    end
  end

  @doc """
  Builds a relationship
  """
  @spec build_relationship(
          type :: Ash.Resource.Relationships.type(),
          name :: atom,
          destination :: module,
          opts :: Keyword.t()
        ) ::
          {:ok, Ash.Resource.Relationships.relationship()} | {:error, term}
  def build_relationship(type, name, destination, opts \\ []) do
    with {:ok, opts} <- handle_nested_builders(opts, [:changes, :arguments, :metadata]) do
      Transformer.build_entity(
        Ash.Resource.Dsl,
        [:relationships],
        type,
        Keyword.merge(opts, name: name, destination: destination)
      )
    end
  end

  @doc """
  Builds and adds a new identity unless an identity with that name already exists
  """
  @spec add_new_identity(
          Spark.Dsl.Builder.input(),
          name :: atom,
          fields :: atom | list(atom),
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_new_identity(dsl_state, name, fields, opts \\ []) do
    if Ash.Resource.Info.identity(dsl_state, name) do
      dsl_state
    else
      add_identity(dsl_state, name, fields, opts)
    end
  end

  @doc """
  Builds and adds an identity
  """
  @spec add_identity(
          Spark.Dsl.Builder.input(),
          name :: atom,
          fields :: atom | list(atom),
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_identity(dsl_state, name, fields, opts \\ []) do
    with {:ok, identity} <- build_identity(name, fields, opts) do
      Transformer.add_entity(dsl_state, [:identities], identity)
    end
  end

  @doc """
  Builds an action
  """
  @spec build_identity(
          name :: atom,
          fields :: atom | list(atom),
          opts :: Keyword.t()
        ) ::
          {:ok, Ash.Resource.Relationships.relationship()} | {:error, term}
  def build_identity(name, fields, opts \\ []) do
    with {:ok, opts} <- handle_nested_builders(opts, [:changes, :arguments, :metadata]) do
      Transformer.build_entity(
        Ash.Resource.Dsl,
        [:identities],
        :identity,
        Keyword.merge(opts, name: name, keys: fields)
      )
    end
  end

  @doc """
  Builds and adds a change
  """
  @spec add_change(
          Spark.Dsl.Builder.input(),
          ref :: module | {module, Keyword.t()},
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_change(dsl_state, ref, opts \\ []) do
    ref =
      case ref do
        {module, opts} -> {module, opts}
        module -> {module, []}
      end

    with {:ok, change} <- build_change(ref, opts) do
      Transformer.add_entity(dsl_state, [:changes], change)
    end
  end

  @doc """
  Builds a change
  """
  @spec build_change(
          ref :: module | {module, Keyword.t()},
          opts :: Keyword.t()
        ) ::
          {:ok, Ash.Resource.Change.t()} | {:error, term}
  def build_change(ref, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      [:changes],
      :change,
      Keyword.merge(opts, change: ref)
    )
  end

  @doc """
  Builds and adds a preparation
  """
  @spec add_preparation(
          Spark.Dsl.Builder.input(),
          ref :: module | {module, Keyword.t()},
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_preparation(dsl_state, ref, opts \\ []) do
    ref =
      case ref do
        {module, opts} -> {module, opts}
        module -> {module, []}
      end

    with {:ok, preparation} <- build_preparation(ref, opts) do
      Transformer.add_entity(dsl_state, [:preparations], preparation)
    end
  end

  @doc """
  Builds a preparation
  """
  @spec build_preparation(
          ref :: module | {module, Keyword.t()},
          opts :: Keyword.t()
        ) ::
          {:ok, Ash.Resource.Preparation.t()} | {:error, term}
  def build_preparation(ref, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      [:preparations],
      :prepare,
      Keyword.merge(opts, preparation: ref)
    )
  end

  @doc """
  Builds a pagination object
  """
  @spec build_pagination(pts :: Keyword.t()) ::
          {:ok, Ash.Resource.Actions.Read.Pagination.t()} | {:error, term}
  def build_pagination(opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      # All action types that support arguments have the same entity, so we just say `create` here
      [:actions, :read],
      :pagination,
      opts
    )
  end

  @doc """
  Builds an action argument
  """
  @spec build_action_argument(name :: atom, type :: Ash.Type.t(), opts :: Keyword.t()) ::
          {:ok, Ash.Resource.Actions.Argument.t()} | {:error, term}
  def build_action_argument(name, type, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      # All action types that support arguments have the same entity, so we just say `create` here
      [:actions, :create],
      :argument,
      Keyword.merge(opts, name: name, type: type)
    )
  end

  @doc """
  Builds an action metadata
  """
  @spec build_action_metadata(name :: atom, type :: Ash.Type.t(), opts :: Keyword.t()) ::
          {:ok, Ash.Resource.Actions.Metadata.t()} | {:error, term}
  def build_action_metadata(name, type, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      # All action types that support arguments have the same entity, so we just say `create` here
      [:actions, :create],
      :metadata,
      Keyword.merge(opts, name: name, type: type)
    )
  end

  @doc """
  Builds an action change
  """
  @spec build_action_change(change :: Ash.Resource.Change.ref(), opts :: Keyword.t()) ::
          {:ok, Ash.Resource.Change.t()} | {:error, term}
  def build_action_change(change, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      # All action types  that support changes have the same change entity, so we just say `create` here
      [:actions, :create],
      :change,
      Keyword.put(opts, :change, change)
    )
  end

  @doc """
  Builds and adds an update_timestamp unless an update_timestamp with that name already exists
  """
  @spec add_new_update_timestamp(Spark.Dsl.Builder.input(), name :: atom, opts :: Keyword.t()) ::
          Spark.Dsl.Builder.result()
  defbuilder add_new_update_timestamp(dsl_state, name, opts \\ []) do
    if Ash.Resource.Info.attribute(dsl_state, name) do
      dsl_state
    else
      add_update_timestamp(dsl_state, name, opts)
    end
  end

  @doc """
  Builds and adds an update_timestamp
  """
  @spec add_update_timestamp(Spark.Dsl.Builder.input(), name :: atom, opts :: Keyword.t()) ::
          Spark.Dsl.Builder.result()
  defbuilder add_update_timestamp(dsl_state, name, opts \\ []) do
    with {:ok, update_timestamp} <- build_update_timestamp(name, opts) do
      Transformer.add_entity(dsl_state, [:attributes], update_timestamp)
    end
  end

  @doc """
  Builds an update_timestamp with the given name, type, and options
  """
  @spec build_update_timestamp(name :: atom, opts :: Keyword.t()) ::
          {:ok, Ash.Resource.Attribute.t()} | {:error, term}
  def build_update_timestamp(name, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      [:attributes],
      :update_timestamp,
      Keyword.merge(opts, name: name)
    )
  end

  @doc """
  Builds and adds a create_timestamp unless a create_timestamp with that name already exists
  """
  @spec add_new_create_timestamp(Spark.Dsl.Builder.input(), name :: atom, opts :: Keyword.t()) ::
          Spark.Dsl.Builder.result()
  defbuilder add_new_create_timestamp(dsl_state, name, opts \\ []) do
    if Ash.Resource.Info.attribute(dsl_state, name) do
      dsl_state
    else
      add_create_timestamp(dsl_state, name, opts)
    end
  end

  @doc """
  Builds and adds a create_timestamp to a resource
  """
  @spec add_create_timestamp(
          Spark.Dsl.Builder.input(),
          name :: atom,
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_create_timestamp(dsl_state, name, opts \\ []) do
    with {:ok, create_timestamp} <- build_create_timestamp(name, opts) do
      Transformer.add_entity(dsl_state, [:attributes], create_timestamp)
    end
  end

  @doc """
  Builds an create_timestamp with the given name, type, and options
  """
  @spec build_create_timestamp(name :: atom, opts :: Keyword.t()) ::
          {:ok, Ash.Resource.Attribute.t()} | {:error, term}
  def build_create_timestamp(name, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      [:attributes],
      :create_timestamp,
      Keyword.merge(opts, name: name)
    )
  end

  @doc """
  Builds and adds an attribute unless an attribute with that name already exists
  """
  @spec add_new_attribute(
          Spark.Dsl.Builder.input(),
          name :: atom,
          type :: Ash.Type.t(),
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_new_attribute(dsl_state, name, type, opts \\ []) do
    if Ash.Resource.Info.attribute(dsl_state, name) do
      {:ok, dsl_state}
    else
      add_attribute(dsl_state, name, type, opts)
    end
  end

  @doc """
  Builds and adds an attribute to a resource
  """
  @spec add_attribute(
          Spark.Dsl.Builder.input(),
          name :: atom,
          type :: Ash.Type.t(),
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_attribute(dsl_state, name, type, opts \\ []) do
    with {:ok, attribute} <- build_attribute(name, type, opts) do
      Transformer.add_entity(dsl_state, [:attributes], attribute)
    end
  end

  @doc """
  Builds an attribute with the given name, type, and options
  """
  @spec build_attribute(name :: atom, type :: Ash.Type.t(), opts :: Keyword.t()) ::
          {:ok, Ash.Resource.Attribute.t()} | {:error, term}
  def build_attribute(name, type, opts \\ []) do
    Transformer.build_entity(
      Ash.Resource.Dsl,
      [:attributes],
      :attribute,
      Keyword.merge(opts, name: name, type: type)
    )
  end

  @doc """
  Builds and adds a calculation unless a calculation with that name already exists
  """
  @spec add_new_calculation(
          Spark.Dsl.Builder.input(),
          name :: atom,
          type :: Ash.Type.t(),
          calculation :: module | {module, Keyword.t()} | Ash.Expr.t(),
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_new_calculation(dsl_state, name, type, calculation, opts \\ []) do
    if Ash.Resource.Info.calculation(dsl_state, name) do
      {:ok, dsl_state}
    else
      add_calculation(dsl_state, name, type, calculation, opts)
    end
  end

  @doc """
  Builds and adds a calculation to a resource
  """
  @spec add_calculation(
          Spark.Dsl.Builder.input(),
          name :: atom,
          type :: Ash.Type.t(),
          calculation :: module | {module, Keyword.t()},
          opts :: Keyword.t()
        ) ::
          Spark.Dsl.Builder.result()
  defbuilder add_calculation(dsl_state, name, type, calculation, opts \\ []) do
    with {:ok, calculation} <- build_calculation(name, type, calculation, opts) do
      Transformer.add_entity(dsl_state, [:calculations], calculation)
    end
  end

  @doc """
  Builds a calculation with the given name, type, and options
  """
  @spec build_calculation(
          name :: atom,
          type :: Ash.Type.t(),
          calculation :: module | {module, Keyword.t()},
          opts :: Keyword.t()
        ) ::
          {:ok, Ash.Resource.Calculation.t()} | {:error, term}
  def build_calculation(name, type, calculation, opts \\ []) do
    opts = Keyword.put_new(opts, :arguments, [])

    Transformer.build_entity(
      Ash.Resource.Dsl,
      [:calculations],
      :calculate,
      Keyword.merge(opts, name: name, type: type, calculation: calculation)
    )
  end
end