Spek.Macros (Spek v0.4.0)

Copy Markdown View Source

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

build_check(fun, args \\ [:ctx])

(since 0.1.0) (macro)

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)
end

This will compile a {fun}_check function like this:

def active_user_check(args \\ [:ctx]) do
  %Check{module: MyApp.MyModule, fun: :active_user, args: args}
end

You 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

defcheck(arg, list)

(since 0.1.0) (macro)

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 a Spek.Check struct.

{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
end

Options

  • :args - The list of arguments as used in the Spek.Check struct. Its length must match an arity of the check function, or an ArgumentError is 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)
end

An unrecognized option raises an ArgumentError at compile time.

Do-block

The do-block is required to return a boolean, :ok, :error, {:ok, term}, or {:error, term}.

Example

This macro call:

defmodule MyApp.MyModule do
  import Spek.Macros

  defcheck account_balanced(account, reason: :account_unbalanced) do
    account.balance >= 0
  end
end

Will 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}
end

The 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}
end

The 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}
)