Current section
Files
Jump to
Current section
Files
lib/ash/resource/attribute.ex
defmodule Ash.Resource.Attribute do
@moduledoc false
defstruct [
:name,
:type,
:allow_nil?,
:generated?,
:primary_key?,
:writable?,
:default,
:update_default,
constraints: []
]
@type t :: %__MODULE__{
name: atom(),
constraints: Keyword.t(),
type: Ash.Type.t(),
primary_key?: boolean(),
default: (() -> term),
update_default: (() -> term) | (Ash.record() -> term),
writable?: boolean
}
alias Ash.OptionsHelpers
@schema [
name: [
type: :atom,
doc: "The name of the attribute."
],
type: [
type: {:custom, OptionsHelpers, :ash_type, []},
doc: "The type of the attribute."
],
constraints: [
type: :keyword_list,
doc:
"Constraints to provide to the type when casting the value. See the type's documentation for more information."
],
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)"
],
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. If it is, the data layer will know to read the value back after writing."
],
writable?: [
type: :boolean,
default: true,
doc: "Whether or not the value can be written to"
],
update_default: [
type: {:custom, __MODULE__, :validate_default, [:update]},
doc:
"A zero argument function, an {mod, fun, args} triple or `{:constant, value}`. If no value is provided for the attribute on update, this value is used."
],
default: [
type: {:custom, __MODULE__, :validate_default, [:create]},
doc:
"A zero argument function, an {mod, fun, args} triple or `{:constant, value}`. If no value is provided for the attribute on create, this value is used."
]
]
@create_timestamp_schema @schema
|> OptionsHelpers.set_default!(:writable?, false)
|> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
|> OptionsHelpers.set_default!(:type, :utc_datetime)
@update_timestamp_schema @schema
|> OptionsHelpers.set_default!(:writable?, false)
|> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
|> OptionsHelpers.set_default!(:update_default, &DateTime.utc_now/0)
|> OptionsHelpers.set_default!(:type, :utc_datetime)
def transform(%{constraints: []} = attribute), do: {:ok, attribute}
def transform(%{constraints: constraints, type: type} = attribute) do
case type do
{:array, type} ->
with {:ok, new_constraints} <-
NimbleOptions.validate(
Keyword.delete(constraints, :items),
Ash.Type.list_constraints()
),
{:ok, item_constraints} <- validate_item_constraints(type, constraints) do
{:ok,
%{attribute | constraints: Keyword.put(new_constraints, :items, item_constraints)}}
end
type ->
schema = Ash.Type.constraints(type)
case NimbleOptions.validate(constraints, schema) do
{:ok, constraints} ->
{:ok, %{attribute | constraints: constraints}}
{:error, error} ->
{:error, error}
end
end
end
defp validate_item_constraints(type, constraints) do
if Keyword.has_key?(constraints, :items) do
schema = Ash.Type.constraints(type)
case NimbleOptions.validate(constraints[:items], schema) do
{:ok, item_constraints} ->
{:ok, item_constraints}
{:error, error} ->
{:error, error}
end
else
{:ok, constraints}
end
end
def validate_default(value, _) when is_function(value, 0), do: {:ok, value}
def validate_default({:constant, value}, _), do: {:ok, {:constant, value}}
def validate_default({module, function, args}, _)
when is_atom(module) and is_atom(function) and is_list(args),
do: {:ok, {module, function, args}}
def validate_default(nil, _), do: {:ok, nil}
def validate_default(other, _) do
{:error,
"#{inspect(other)} is not a valid default. To provide a constant value, use `{:constant, #{
inspect(other)
}}`"}
end
@doc false
def attribute_schema, do: @schema
def create_timestamp_schema, do: @create_timestamp_schema
def update_timestamp_schema, do: @update_timestamp_schema
end