Convenience macros for defining check functions.
The usage of these macros is optional, but they can make your rules more readable.
Summary
Functions
Defines a function that returns a Spek.Check struct that uses an existing
function in the same module.
Generates three functions from a single check definition.
Functions
Defines a function that returns a Spek.Check struct that uses an existing
function in the same module.
Example
Let's say you have an existing active_user/1 function that you want to use
in a Spek expression. Instead of defining the Check struct manually, you can
use build_check and pass the function name and the arguments.
defmodule MyApp.MyModule do
def active_user(%{state: :active}), do: :ok
def active_user(%{state: :inactive}), do: {:error, :user_inactive}
build_check(:active_user)
endThis will compile a {fun}_check function like this:
def active_user_check(args \\ [:ctx]) do
%Check{module: MyApp.MyModule, fun: :active_user, args: args}
endYou can then use this function when building complex rules:
Spek.all_of([
MyApp.MyModule.active_user_check(),
# ...
])The second argument sets the default args. This:
build_check(:active_user, [{:ctx, :user}])Compiles to:
def active_user_check(args \\ [{:ctx, :user}]) do
%Check{module: MyApp.MyModule, fun: :active_user, args: args}
end
Generates three functions from a single check definition.
Generated functions
{name}?- A predicate function that returns the result of the boolean expression defined in the do-block.{name}- A function that runs the expression defined in the do-block and returns:ok,:error,{:ok, term}, or{:error, term}.{name}_check- A function that returns aSpek.Checkstruct, or aSpek.Literalstruct if the do-block returns a literal.
{name}? and {name} take the arguments of the check definition. The
arguments may be patterns, and the definition may have a guard. Both apply to
{name} alone, since {name}? delegates to it, so an argument that does not
match the pattern or does not satisfy the guard raises a
FunctionClauseError naming {name}.
defcheck positive(number) when is_integer(number) do
number > 0
endOptions
:args- The list of arguments as used in theSpek.Checkstruct. Its length must match an arity of the check function, or anArgumentErroris raised at compile time. Defaults to[:ctx]for a check that takes one argument, and to[]for one that takes none.:reason- The reason used in the error tuple. Defaults to:failed. This value is only used if the do-block returns a boolean.
A check that always takes two or more arguments has no default :args, so
{name}_check/1 is generated without a default argument and the arguments
are passed at every call site.
Options are passed as the last argument of the check definition. They are recognized as a keyword list rather than by position, so a check without arguments can take options too.
defcheck maintenance_mode(reason: :under_maintenance) do
Application.get_env(:my_app, :maintenance_mode, false)
endAn unrecognized option raises an ArgumentError at compile time.
Because options are recognized by shape, the last argument of a check cannot be a keyword list pattern: it is read as options and rejected as unknown. Bind the argument and match on it in the do-block instead.
Do-block
The do-block is required to return a boolean, :ok, :error, {:ok, term},
or {:error, term}.
Literal do-blocks
If the do-block returns a literal, {name}_check returns a Spek.Literal
struct instead of a Spek.Check struct. The :result of the Spek.Literal
is the value that {name} returns.
defcheck maintenance_mode(reason: :under_maintenance) do
false
end
maintenance_mode()
#=> {:error, :under_maintenance}
maintenance_mode_check()
#=> %Spek.Literal{result: {:error, :under_maintenance}, satisfied?: false}A do-block that references a check argument does not return a literal, even when it returns a tuple.
Example
This macro call:
defmodule MyApp.MyModule do
import Spek.Macros
defcheck account_balanced(account, reason: :account_unbalanced) do
account.balance >= 0
end
endWill result in these three functions:
def account_balanced?(account) do
Spek.to_boolean(account_balanced(account))
end
def account_balanced(account) do
if account.balance >= 0,
do: :ok,
else: {:error, :account_unbalanced}
end
def account_balanced_check(args \\ [:ctx]) do
%Check{module: MyApp.MyModule, fun: :account_balanced, args: args}
endThe do-block can return :ok and :error values instead of a boolean, with
the same result:
defcheck account_balanced(account) do
if account.balance >= 0, do: :ok, else: {:error, :account_unbalanced}
endThe account_balanced?/1 and account_balanced/1 functions can be used
directly, and the account_balanced_check/0 function can be used
with the Spek evaluation functions, or be combined with additional checks to
define complex rules.
def transfer_rule do
Spek.all_of([
account_balanced_check(),
# additional checks
])
end
Spek.eval(transfer_rule(), %Account{balance: 100})Passing arguments to account_balanced_check/1 overrides the default, e.g. if
you combine checks that work on different data. With
account_balanced_check([{:ctx, :account}]) in the rule above, the check
receives the :account key of the context instead of the whole context:
Spek.eval(transfer_rule(), account: %Account{balance: 100})A check can take any number of arguments. With two or more, the arguments are
passed when the check is built, or given as the :args option:
defcheck matching_organization(user, organization,
reason: :no_organization_match
) do
user.organization_id == organization.id
end
Spek.eval(
matching_organization_check([{:ctx, :user}, {:ctx, :organization}]),
user: %User{organization_id: 1},
organization: %Organization{id: 1}
)