lib/ex_teal/resource.ex

defmodule ExTeal.Resource do
  @type params :: map()

  @moduledoc """
  When used, includes all aspects of the functionality required to
  manage the resource.

  Example usage in a ExTeal Resource:

      defmodule ExampleWeb.ExTeal.BlogResource do
        use ExTeal.Resource

        alias Example.Content.Blog

        def resource, do: Blog

        def title, do: "blog"
      end
  """

  @type orderable_key ::
          :asc | :asc_nulls_first | :asc_nulls_last | :desc | :desc_nulls_first | :desc_nulls_last

  @type orderable_option :: {orderable_key, atom()}

  @type attributes :: map()

  @type id :: String.t() | integer()

  @type record :: struct()

  @type records :: [record()]

  @type t :: module()

  @type validation_errors :: Ecto.Changeset.t() | keyword()

  @doc """
  Hide the Resource from the sidenav in the user interface, defaults to false.
  """
  @callback hide_from_nav() :: boolean()

  @doc """
  If you would like to separate resources into different sidebar groups, you can
  override the `nav_group/1` function on the resource.
  """
  @callback nav_group(Plug.Conn.t()) :: String.t() | nil

  @doc """
  Specifies the field to use as the basis for a drag and drop interface for the collection.
  """
  @callback sortable_by() :: String.t() | nil

  @doc """
  Allows skipping sanitize for all fields on a resource
  """
  @callback skip_sanitize() :: boolean() | nil

  @doc """
  Provide a list of action cards to render them above the resource index
  """
  @callback cards(Plug.Conn.t()) :: [module]

  @doc """
  Override the default ordering of the index.
  """
  @callback default_order() :: [orderable_option]

  @type pivot_resource :: %{_pivot: struct(), _row: struct(), pivot: true}

  @type indexed_resource :: struct() | pivot_resource()

  @doc """
  Override the detail or edit link for a resource on the resource index.

  When defining this callback, be aware that resources referenced by a many to many relationship
  will also call this function when rendering the index of that many to many.
  """
  @callback navigate_to(:detail | :edit, Plug.Conn.t(), indexed_resource()) :: %{
              resource_name: String.t(),
              resource_id: String.t()
            }

  defmacro __using__(_opts) do
    quote do
      @behaviour ExTeal.Resource
      use ExTeal.Resource.Create
      use ExTeal.Resource.Index
      use ExTeal.Resource.Show
      use ExTeal.Resource.Update
      use ExTeal.Resource.Delete
      use ExTeal.Resource.Export
      use ExTeal.Resource.Policy

      import ExTeal.FieldVisibility
      import ExTeal.Field, only: [get: 2]

      def hide_from_nav, do: false

      def nav_group(_), do: nil

      def sortable_by, do: nil

      def default_order, do: [asc: :id]

      def cards(_conn), do: []

      def skip_sanitize, do: false

      def navigate_to(_, _conn, %{pivot: true, _row: row}),
        do: %{resource_name: uri(), resource_id: row.id}

      def navigate_to(_, _conn, schema), do: %{resource_name: uri(), resource_id: schema.id}

      defoverridable(
        hide_from_nav: 0,
        nav_group: 1,
        sortable_by: 0,
        default_order: 0,
        cards: 1,
        skip_sanitize: 0,
        navigate_to: 3
      )
    end
  end

  def map_to_json(resources, conn) do
    Enum.map(resources, &to_json(&1, conn))
  end

  def to_json(resource, conn) do
    %{
      title: resource.title(),
      singular: resource.singular_title(),
      group: resource.nav_group(conn),
      uri: resource.uri(),
      hidden: resource.hide_from_nav(),
      skip_sanitize: resource.skip_sanitize(),
      searchable: !Enum.empty?(resource.search()),
      can_create_any: resource.policy().create_any?(conn),
      can_view_any: resource.policy().view_any?(conn),
      can_update_any: resource.policy().update_any?(conn),
      can_delete_any: resource.policy().delete_any?(conn),
      default_filters: resource.default_filters()
    }
  end
end