defmodule Expression.V2.Callbacks do
@moduledoc """
Use this module to implement one's own callbacks.
The standard callbacks available are implemented in `Expression.V2.Callbacks.Standard`.
```elixir
defmodule MyCallbacks do
use Expression.V2.Callbacks
@doc \"\"\"
Roll a dice and randomly return a number between 1 and 6.
\"\"\"
def dice_roll(ctx) do
Enum.random(1..6)
end
end
```
"""
alias Expression.V2.Callbacks.Standard
@reserved_words ~w[and if or not]
@doc """
Convert a string function name into an atom meant to handle
that function
Reserved words such as `and`, `if`, and `or` are automatically suffixed
with an `_` underscore.
"""
def atom_function_name(function_name) when function_name in @reserved_words,
do: atom_function_name("#{function_name}_")
def atom_function_name(function_name) do
String.to_atom(function_name)
end
@doc """
Callback a function while evaluating the context against an expression.
Callback functions in this module are either:
1. The function name as is
2. The function name with an underscore suffix if the function name is a reserved word
3. The function name suffixed with `_vargs` if the takes a variable set of arguments
"""
@spec callback(
module :: module,
context :: map,
function_name :: binary,
arguments :: [any]
) :: any
def callback(module \\ Standard, context, function_name, arguments) do
case implements(module, function_name, arguments) do
{:exact, module, function_name} ->
apply(
module,
function_name,
[context] ++ Enum.map(arguments, &Expression.V2.default_value(&1, context))
)
{:vargs, module, function_name} ->
apply(module, function_name, [
context,
Enum.map(arguments, &Expression.V2.default_value(&1, context))
])
{:error, reason} ->
reason
end
end
@spec implements(module, function_name :: String.t(), arguments :: [any]) ::
{:exact, module, function_name :: atom}
| {:vargs, module, function_name :: atom}
| {:error, reason :: String.t()}
def implements(module \\ Standard, function_name, arguments) do
exact_function_name = atom_function_name(function_name)
vargs_function_name = atom_function_name("#{function_name}_vargs")
Code.ensure_loaded!(module)
cond do
function_exported?(module, exact_function_name, length(arguments) + 1) ->
{:exact, module, exact_function_name}
# Check if it's been implemented to accept a variable amount of arguments
function_exported?(module, vargs_function_name, 2) ->
{:vargs, module, vargs_function_name}
# Check if the exact function signature has been implemented
function_exported?(Standard, exact_function_name, length(arguments) + 1) ->
{:exact, Standard, exact_function_name}
# Check if it's been implemented to accept a variable amount of arguments
function_exported?(Standard, vargs_function_name, 2) ->
{:vargs, Standard, vargs_function_name}
# Otherwise fail
true ->
{:error, "#{function_name} is not implemented."}
end
end
defmacro __using__(_opts) do
quote do
def callback(module \\ __MODULE__, context, function_name, args)
defdelegate callback(module, context, function_name, arguments),
to: Expression.V2.Callbacks
end
end
end