lib/ash/resource/calculation/calculation.ex

defmodule Ash.Resource.Calculation do
  @moduledoc "Represents a named calculation on a resource"
  defstruct [
    :name,
    :type,
    :calculation,
    :arguments,
    :description,
    :constraints,
    :private?,
    :allow_nil?,
    :select,
    :load,
    :allow_async?,
    filterable?: true
  ]

  @schema [
    name: [
      type: :atom,
      required: true,
      links: [],
      doc: "The field name to use for the calculation value"
    ],
    type: [
      type: :any,
      links: [
        modules: [
          "ash:module:Ash.Type"
        ]
      ],
      required: true
    ],
    constraints: [
      type: :keyword_list,
      default: [],
      links: [
        modules: [
          "ash:module:Ash.Type"
        ]
      ],
      doc: "Constraints to provide to the type."
    ],
    allow_async?: [
      type: :boolean,
      default: false,
      links: [
        guides: [
          "ash:guide:Actions"
        ]
      ],
      doc: """
      If set to `true`, then the calculation may be run after the main query.
      """
    ],
    calculation: [
      type: {:custom, __MODULE__, :calculation, []},
      required: true,
      links: [],
      doc: "The module or `{module, opts}` to use for the calculation"
    ],
    description: [
      type: :string,
      links: [
        guides: [
          "ash:guide:Documentation"
        ]
      ],
      doc: "An optional description for the calculation"
    ],
    private?: [
      type: :boolean,
      default: false,
      links: [
        guides: [
          "ash:guide:Security"
        ]
      ],
      doc:
        "Whether or not the calculation will appear in any interfaces created off of this resource, e.g AshJsonApi and AshGraphql"
    ],
    select: [
      type: {:list, :atom},
      default: [],
      links: [],
      doc: "A list of fields to ensure selected if the calculation is used."
    ],
    load: [
      type: :any,
      default: [],
      links: [],
      doc: "A load statement to be applied if the calculation is used."
    ],
    allow_nil?: [
      type: :boolean,
      default: true,
      links: [],
      doc: "Whether or not the calculation can return nil."
    ],
    filterable?: [
      type: {:or, [:boolean, {:in, [:simple_equality]}]},
      default: true,
      links: [],
      doc: "Whether or not the calculation should be usable in filters."
    ]
  ]

  @type t :: %__MODULE__{
          name: atom(),
          calculation: {:ok, {atom(), any()}} | {:error, String.t()},
          arguments: list(any()),
          description: String.t() | nil,
          private?: boolean,
          allow_nil?: boolean
        }

  defmodule Argument do
    @moduledoc "An argument to a calculation"
    defstruct [:name, :type, :default, :allow_nil?, :constraints]

    @schema [
      name: [
        type: :atom,
        required: true,
        doc: "The name to use for the argument",
        links: []
      ],
      type: [
        type: :ash_type,
        required: true,
        links: [
          modules: [
            "ash:module:Ash.Type"
          ]
        ],
        doc: "The type of the argument"
      ],
      default: [
        type: {:or, [{:mfa_or_fun, 0}, :literal]},
        required: false,
        links: [],
        doc: "A default value to use for the argument if not provided"
      ],
      allow_nil?: [
        type: :boolean,
        default: true,
        links: [],
        doc: "Whether or not the argument value may be nil (or may be not provided)"
      ],
      constraints: [
        type: :keyword_list,
        default: [],
        links: [
          modules: [
            "ash:module:Ash.Type"
          ]
        ],
        doc:
          "Constraints to provide to the type when casting the value. See the type's documentation for more information."
      ]
    ]

    def schema, do: @schema
  end

  def schema, do: @schema

  def calculation({module, opts}) when is_atom(module) and is_list(opts),
    do: {:ok, {module, opts}}

  def calculation(module) when is_atom(module), do: {:ok, {module, []}}

  def calculation(other) do
    {:ok, {Ash.Resource.Calculation.Expression, expr: other}}
  end
end