defmodule Ash.Query.Calculation do @moduledoc "Represents a calculated attribute requested on a query" defstruct [ :name, :module, :opts, :load, :type, :constraints, :calc_name, context: %{}, required_loads: [], select: [], filterable?: true, async?: false, sortable?: true, sensitive?: false ] @type t :: %__MODULE__{} @opt_schema [ arguments: [ type: :map, doc: "Arguments to pass to the calculation", default: %{} ], async?: [ type: :boolean, default: false, doc: "Whether or not this calculation should be run asynchronously" ], filterable?: [ type: :boolean, doc: "Whether or not this calculation can be filtered on", default: true ], sortable?: [ type: :boolean, doc: "Whether or not this calculation can be sorted on", default: true ], sensitive?: [ type: :boolean, doc: "Whether or not references to this calculation will be considered sensitive", default: false ], load: [ type: :any, doc: "Loads that are required for the calculation." ], calc_name: [ type: :any, hide: true, doc: "A resource calculation this calculation maps to." ], actor: [ type: :any, doc: "The actor performing the calculation." ], tenant: [ type: :any, doc: "The tenant performing the calculation." ], authorize?: [ type: :boolean, doc: "Whether or not authorization is being performed" ], tracer: [ type: :any, doc: "The tracer or tracers used in the calculation." ], source_context: [ type: :map, doc: "Context from the source query or changeset.", default: %{} ] ] opt_schema = @opt_schema defmodule Opts do @moduledoc false use Spark.Options.Validator, schema: opt_schema end @doc """ Creates a new query calculation. ## Options #{Spark.Options.docs(@opt_schema)} """ def new( name, module, calc_opts, type, constraints, opts \\ [] ) do with {:ok, opts} <- Opts.validate(opts), {:ok, calc_opts} <- Ash.Resource.Calculation.init(module, calc_opts) do authorize? = if :authorize? in opts.__set__ do opts.authorize? else opts.source_context[:private][:authorize?] end context = %Ash.Resource.Calculation.Context{ arguments: opts.arguments, type: type, constraints: constraints, authorize?: authorize?, actor: opts.actor || opts.source_context[:private][:actor], tenant: opts.tenant || opts.source_context[:private][:tenant], tracer: opts.tracer || opts.source_context[:private][:tracer], source_context: opts.source_context } calc_name = if :calc_name in opts.__set__ do opts.calc_name end {:ok, %__MODULE__{ name: name, module: module, type: type, opts: calc_opts, calc_name: calc_name, constraints: constraints, context: context, async?: opts.async?, required_loads: opts.load, filterable?: opts.filterable?, sortable?: opts.sortable?, sensitive?: opts.sensitive? }} end end @from_resource_schema [ args: [ type: :map, doc: "Arguments to pass to the calculation", default: %{} ], source_context: [ type: :map, doc: "Context from the source query or changeset.", default: %{} ] ] from_resource_schema = @from_resource_schema defmodule FromResourceOpts do @moduledoc false use Spark.Options.Validator, schema: from_resource_schema end @doc """ Creates a new query calculation from a resource calculation, raising any errors. See `from_resource_calculation/3` for more. """ def from_resource_calculation!( resource, name, opts \\ [] ) do case from_resource_calculation(resource, name, opts) do {:ok, calculation} -> calculation {:error, error} -> raise Ash.Error.to_ash_error(error) end end @doc """ Creates a new query calculation from a resource calculation. ## Options #{Spark.Options.docs(@from_resource_schema)} """ def from_resource_calculation( resource, name, opts \\ [] ) do {name, resource_calculation} = case name do %Ash.Resource.Calculation{} = calc -> {calc.name, calc} name -> {name, Ash.Resource.Info.calculation(resource, name) || raise( ArgumentError, "No calculation called #{inspect(name)} found on #{inspect(resource)}" )} end %{calculation: {module, calc_opts}} = resource_calculation with {:ok, opts} <- FromResourceOpts.validate(opts), {:ok, args} <- Ash.Query.validate_calculation_arguments(resource_calculation, opts.args) do new( name, module, calc_opts, resource_calculation.type, resource_calculation.constraints, arguments: args, async?: resource_calculation.async?, filterable?: resource_calculation.filterable?, sortable?: resource_calculation.sortable?, sensitive?: resource_calculation.sensitive?, load: resource_calculation.load, source_context: opts.source_context, calc_name: resource_calculation.name ) end end defimpl Inspect do import Inspect.Algebra def inspect(%{context: context, calc_name: calc_name}, inspect_opts) when not is_nil(calc_name) do calc_name = if is_atom(calc_name) or is_binary(calc_name) do to_string(calc_name) else to_doc(calc_name, inspect_opts) end if context.arguments == %{} do calc_name else concat([ calc_name, "(", to_doc(Map.to_list(context.arguments), inspect_opts), ")" ]) end end def inspect(%{module: module, opts: calculation_opts, context: context}, _opts) do if context.arguments == %{} do module.describe(calculation_opts) else concat([module.describe(calculation_opts), " - ", inspect(context.arguments)]) end end end end