defmodule Ash.CustomExpression do @moduledoc """ A module for defining custom functions that can be called in Ash expressions. For example: ```elixir defmodule MyApp.Expressions.LevenshteinDistance do use Ash.CustomExpression, name: :levenshtein, arguments: [ [:string, :string] ] def expression(AshPostgres.DataLayer, [left, right]) do {:ok, expr(fragment("levenshtein(?, ?)", ^left, ^right))} end # It is good practice to always define an expression for `Ash.DataLayer.Simple`, # as that is what Ash will use to run your custom expression in Elixir. # This allows us to completely avoid communicating with the database in some cases. # Always use a fragment like this to evaluate code in simple data layers. The value you # get here for arguments may be expressions at this point. def expression(data_layer, [left, right]) when data_layer in [ Ash.DataLayer.Ets, Ash.DataLayer.Simple ] do {:ok, expr(fragment(&__MODULE__.levenshtein/2, ^left, ^right))} end # always define this fallback clause as well def expression(_data_layer, _args), do: :unknown @doc "Computes the levenshtein distance of two strings" def levenshtein(left, right) do # ...... end end ``` ### Options - `name` - The name of the custom expression. This is the name that will be used in Ash expressions. - `arguments` - A list of lists of types that the custom expression accepts. Each list represents a set of arguments that the custom expression can accept. - `predicate?` - Whether this expression can be exposed as a predicate in filter interfaces. Defaults to `false`. ### Registering Your Expression Use compile-time configuration to register your custom expressions ```elixir config :ash, :custom_expressions, [MyApp.Expressions.LevenshteinDistance] ``` """ defstruct [:module, :arguments, :expression, :simple_expression] defimpl Inspect do import Inspect.Algebra def inspect(%{expression: expression}, opts) do to_doc(expression, opts) end end @callback expression( data_layer :: Ash.DataLayer.t(), arguments :: list(Ash.Expr.t()) ) :: {:ok, Ash.Expr.t()} | :unknown @callback name() :: atom @callback arguments() :: list(list(Ash.Type.t() | {Ash.Type.t(), Keyword.t()})) defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @behaviour Ash.CustomExpression import Ash.Expr if !opts[:name] do raise ArgumentError, "You must provide a name for the custom expression" end if !opts[:arguments] do raise ArgumentError, "You must provide arguments for the custom expression" end def predicate?, do: unquote(!!opts[:predicate?]) def arguments, do: unquote(opts[:arguments]) def name, do: unquote(opts[:name]) end end end