lib/ash/resource/attribute.ex

defmodule Ash.Resource.Attribute do
  @moduledoc "Represents an attribute on a resource"

  defstruct [
    :name,
    :type,
    :allow_nil?,
    :generated?,
    :primary_key?,
    :private?,
    :writable?,
    :always_select?,
    :default,
    :update_default,
    :description,
    :source,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: []
  ]

  defmodule Helpers do
    @moduledoc "Helpers for building attributes"

    defmacro timestamps(opts \\ []) do
      quote do
        create_timestamp :inserted_at, unquote(opts)
        update_timestamp :updated_at, unquote(opts)
      end
    end
  end

  @type t :: %__MODULE__{
          name: atom(),
          constraints: Keyword.t(),
          type: Ash.Type.t(),
          primary_key?: boolean(),
          private?: boolean(),
          default: nil | term | (() -> term),
          update_default: nil | term | (() -> term) | (Ash.Resource.record() -> term),
          sensitive?: boolean(),
          writable?: boolean()
        }

  alias Spark.OptionsHelpers

  @schema [
    name: [
      type: :atom,
      doc: "The name of the attribute."
    ],
    type: [
      type: Ash.OptionsHelpers.ash_type(),
      doc: "The type of the attribute. See `Ash.Type` for more."
    ],
    constraints: [
      type: :keyword_list,
      doc:
        "Constraints to provide to the type when casting the value. See the type's documentation for more information. See `Ash.Type` for more."
    ],
    description: [
      type: :string,
      doc: "An optional description for the attribute."
    ],
    sensitive?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the attribute value contains sensitive information, like PII.
      See the [Security guide](/documentation/topics/security.md) for more.
      """
    ],
    source: [
      type: :atom,
      doc: """
      If the field should be mapped to a different name in the data layer. Support varies by data layer.
      """
    ],
    always_select?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not to ensure this attribute is always selected when reading from the database.
      """
    ],
    primary_key?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the attribute is part of the primary key (one or more fields that uniquely identify a resource)."
      If primary_key? is true, allow_nil? must be false.
      """
    ],
    allow_nil?: [
      type: :boolean,
      default: true,
      doc: "Whether or not the attribute can be set to nil."
    ],
    generated?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the value may be generated by the data layer.
      """
    ],
    writable?: [
      type: :boolean,
      default: true,
      doc: "Whether or not the value can be written to."
    ],
    private?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the attribute can be provided as input, or will be shown when extensions work with the resource (i.e won't appear in a web api).

      See the [security guide](/documentation/topics/security.md) for more.
      """
    ],
    default: [
      type: {:or, [{:mfa_or_fun, 0}, :literal]},
      doc: "A value to be set on all creates, unless a value is being provided already."
    ],
    update_default: [
      type: {:or, [{:mfa_or_fun, 0}, :literal]},
      doc: "A value to be set on all updates, unless a value is being provided already."
    ],
    filterable?: [
      type: {:or, [:boolean, {:in, [:simple_equality]}]},
      default: true,
      doc: "Whether or not the attribute can be referenced in filters."
    ],
    match_other_defaults?: [
      type: :boolean,
      default: false,
      doc: """
      Ensures that other attributes that use the same "lazy" default (a function or an mfa), use the same default value.
      Has no effect unless `default` is a zero argument function.
      For example, create and update timestamps use this option, and have the same lazy function `&DateTime.utc_now/0`, so they
      get the same value, instead of having slightly different timestamps.
      """
    ]
  ]

  @create_timestamp_schema @schema
                           |> OptionsHelpers.set_default!(:writable?, false)
                           |> OptionsHelpers.set_default!(:private?, true)
                           |> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
                           |> OptionsHelpers.set_default!(:match_other_defaults?, true)
                           |> OptionsHelpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
                           |> OptionsHelpers.set_default!(:allow_nil?, false)

  @update_timestamp_schema @schema
                           |> OptionsHelpers.set_default!(:writable?, false)
                           |> OptionsHelpers.set_default!(:private?, true)
                           |> OptionsHelpers.set_default!(:match_other_defaults?, true)
                           |> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
                           |> OptionsHelpers.set_default!(
                             :update_default,
                             &DateTime.utc_now/0
                           )
                           |> OptionsHelpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
                           |> OptionsHelpers.set_default!(:allow_nil?, false)

  @uuid_primary_key_schema @schema
                           |> OptionsHelpers.set_default!(:writable?, false)
                           |> OptionsHelpers.set_default!(:default, &Ash.UUID.generate/0)
                           |> OptionsHelpers.set_default!(:primary_key?, true)
                           |> OptionsHelpers.set_default!(:type, :uuid)
                           |> Keyword.delete(:allow_nil?)

  @integer_primary_key_schema @schema
                              |> OptionsHelpers.set_default!(:writable?, false)
                              |> OptionsHelpers.set_default!(:primary_key?, true)
                              |> OptionsHelpers.set_default!(:generated?, true)
                              |> OptionsHelpers.set_default!(:type, :integer)
                              |> Keyword.delete(:allow_nil?)

  def transform(attribute) do
    Ash.Type.set_type_transformation(%{attribute | source: attribute.source || attribute.name})
  end

  @doc false
  def attribute_schema, do: @schema
  def create_timestamp_schema, do: @create_timestamp_schema
  def update_timestamp_schema, do: @update_timestamp_schema
  def uuid_primary_key_schema, do: @uuid_primary_key_schema
  def integer_primary_key_schema, do: @integer_primary_key_schema
end