Spek.Macros (Spek v0.2.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])

(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/0 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)

(macro)

Generates three functions from a single check definition.

Generated functions

The arity of the generated function depends on the number of arguments passed to the macro.

  • {name}? - A predicate function that returns the result of the boolean expression defined in the do-block.
  • {name} - A function that runs the boolean expression defined in the do-block and returns :ok or {:error, term}.
  • {name}_check - A function that returns a Spek.Check struct.

Options

  • :args - The list of arguments as used in the Spek.Check struct. Defaults to [:ctx].
  • :reason - The reason used in the error tuple. Defaults to :failed.

Do-block

The do-block is required to evaluate to a boolean value.

Example

This macro call:

defmodule MyApp.MyModule do
  import Spek.Macros

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

end

Will result in these three functions:

def account_balanced?(account) do
  account.balance >= 0
end

def account_balanced(account) do
  if account_balanced(account),
    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 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})

You can also override the check arguments, e.g. if you combine multiple checks that work on different data:

def transfer_rule do
  Spek.all_of([
    account_balanced_check([{:ctx, :account}]),
    # additional checks
  ])
end

Spek.eval(transfer_rule(), account: %Account{balance: 100})

The generated functions can have an arbitrary number of arguments. For example, this macro call defines two arguments, user and organization:

defcheck matching_organization(user, organization,
           args: [{:ctx, :user}, {:ctx, :organization}],
           reason: :no_organization_match
         ) do
  user.organization_id == organization.id
end

Which is expanded to:

def matching_organization?(user, account) do
  user.organization_id == organization.id
end

def matching_organization(user, account) do
  if matching_organization?(user, account),
    do: :ok,
    else: {:error, :no_organization_match}
end

def matching_organization_check(args \\ [{:ctx, :user}, {:ctx, :organization}]) do
  %Check{
    module: MyApp.MyModule,
    fun: :matching_organization,
    args: args
  }
end

In this case, we would call the Spek evaluation functions like this:

Spek.eval(matching_organization_check(),
  user: %User{organization_id: 1},
  organization: %Organization{id: 1}
)