lib/ex_teal/fields/boolean_group.ex

defmodule ExTeal.Fields.BooleanGroup do
  @moduledoc """
  A group of boolean inputs that represent a map, or embedded schema
  where each value is a boolean.  Useful for embedding features, permissions,
  or roles into a schema.

  If the field represents and embedded schema as an `embeds_one`, the field
  will set a default set of options based on the fields of the embedded schema.
  It assumes that every field in the embed is a boolean:

      defmodule Permissions do
        use Ecto.Schema

        embedded_schema do
          field :read, :boolean, default: false
          field :write, :boolean, default: false
          field :delete, :boolean, default: false
        end

        def changeset(permissions, attrs) do
          cast(permission, attrs, [:read, :write, :delete])
        end
      end

      defmodule Post do
        use Ecto.Schema

        schema "posts" do
          field :title, :string
          embeds_one :permissions, Permissions
        end
      end


      def PostResource do
        use ExTeal.Resource
        def model, do: Post

        def fields, do: [
          Text.make(:title),
          BooleanGroup.make(:permissions)
        ]
      end

  You can also define a boolean group over a plain map type and manually
  define the options as a map of keys and labels:

      defmodule Post do
        use Ecto.Schema

        schema "posts" do
          field :title, :string
          field :permissions, :map
        end
      end

      def PostResource do
        use ExTeal.Resource
        def model, do: Post

        def fields, do: [
          Text.make(:title),
          BooleanGroup.make(:permissions)
          |> BooleanGroup.options(%{
            "read" => "Read",
            "write" => "Write"
          })
        ]
      end

  The `options` function can also be used to override the default options
  when used with an embedded schema if the options need to be translated into
  human readable values.
  """

  use ExTeal.Field
  alias ExTeal.Field

  def component, do: "boolean-group"

  @impl true
  def default_sortable, do: false

  @impl true
  def apply_options_for(field, model, _conn, _type) do
    schema = model.__struct__

    case schema.__schema__(:type, field.field) do
      :map ->
        field

      {:embed, embedded} ->
        parameterize_an_embed(field, embedded)

      {:parameterized, Ecto.Embedded, %Ecto.Embedded{cardinality: :one} = embedded} ->
        parameterize_an_embedded(field, embedded)
    end
  end

  @impl true
  def sanitize_as, do: :json

  @doc """
  Add the available options to manage in the boolean group
  """
  @spec options(Field.t(), any()) :: Field.t()
  def options(field, options) do
    opts = Map.merge(field.options, %{group_options: ExTeal.Field.transform_options(options)})
    %{field | options: opts}
  end

  @doc """
  Customize the text displayed in the event that a field contains no values.
  """
  @spec no_value_text(Field.t(), String.t()) :: Field.t()
  def no_value_text(field, text) do
    %{field | options: Map.put(field.options, :no_value, text)}
  end

  defp parameterize_an_embed(field, embedded_schema) do
    case Map.fetch(field.options, :group_options) do
      {:ok, _options} ->
        field

      _ ->
        fields = embedded_schema.related.__schema__(:fields)
        options(field, fields -- [:id])
    end
  end

  defp parameterize_an_embedded(field, embedded) do
    case Map.fetch(field.options, :group_options) do
      {:ok, _options} ->
        field

      _ ->
        embedded_fields = embedded.related.__schema__(:fields) -- [:id]
        options(field, embedded_fields)
    end
  end
end