Exop.Operation behaviour (Exop v1.4.4) View Source

Provides macros for an operation's contract definition and process/1 function.

Example

defmodule SomeOperation do
  use Exop.Operation

  parameter :param1, type: :integer, required: true
  parameter :param2, type: :string, length: %{max: 3}, format: ~r/foo/

  def process(params) do
    "This is the operation's result with one of the params = " <> params[:param1]
  end
end

Link to this section Summary

Functions

Authorizes an action with predefined policy (see Policy check macro docs). If authorization fails, any code after (below) auth check will be postponed (an error {:error, {:auth, _reason}} will be returned immediately)

Returns policy that was defined in an operation.

Defines a fallback module that will be used for an operation's non-ok-tuple (fail) result handling.

Defines a parameter with name and opts in an operation contract. Options could include the parameter value checks and transformations (like coercion).

Defines a policy that will be used for authorizing the possibility of a user to invoke an operation.

Callbacks

Operation's entry point. Takes defined contract as the single parameter. Contract itself is a list of maps: [%{name: atom(), opts: keyword()}]

Link to this section Functions

Link to this macro

authorize(opts \\ nil)

View Source (macro)

Specs

authorize(any()) :: any()

Authorizes an action with predefined policy (see Policy check macro docs). If authorization fails, any code after (below) auth check will be postponed (an error {:error, {:auth, _reason}} will be returned immediately)

Link to this macro

current_policy()

View Source (macro)

Specs

current_policy() :: any()

Returns policy that was defined in an operation.

Link to this macro

fallback(fallback_module, opts \\ [])

View Source (macro)

Specs

fallback(module(), any()) :: any()

Defines a fallback module that will be used for an operation's non-ok-tuple (fail) result handling.

defmodule MultiplyByTenOperation do
  use Exop.Operation

  fallback LoggerFallback

  parameter :a, type: :integer, required: true

  def process(%{a: a}), do: a * 10
end

A fallback module itself might be:

defmodule LoggerFallback do
  use Exop.Fallback
  require Logger

  def process(operation_module, params_passed_to_the_operation, operation_error_result) do
    Logger.error("Oops")
  end
end

If return: true option is provided then failed operation's run/1 will return the fallback's process/3 result.

Link to this macro

parameter(name, opts \\ [])

View Source (macro)

Specs

parameter(atom() | binary(), keyword()) :: any()

Defines a parameter with name and opts in an operation contract. Options could include the parameter value checks and transformations (like coercion).

A parameter name could be either an atom or a string. You could even mix atom-named and string-named parameters in an operation's contract.

Example

parameter :some_param, type: :map, required: true
parameter "my parameter", type: :map, required: true

Available checks are:

type

Checks whether a parameter's value is of declared type.

parameter :some_param, type: :map

required

Checks the presence of a parameter in passed params collection.

parameter :some_param, required: true

default

Checks if the parameter is missed and assigns default value to it if so.

parameter :some_param, default: "default value"

numericality

Checks whether a parameter's value is a number and passes constraints (if constraints were defined).

parameter :some_param, numericality: %{equal_to: 10, greater_than: 0,
                                       greater_than_or_equal_to: 10,
                                       less_than: 20,
                                       less_than_or_equal_to: 10}

equals

Checks whether a parameter's value exactly equals given value (with type equality).

parameter :some_param, equals: 100.5

in

Checks whether a parameter's value is within a given list.

parameter :some_param, in: ~w(a b c)

not_in

Checks whether a parameter's value is not within a given list.

parameter :some_param, not_in: ~w(a b c)

format

Checks wether parameter's value matches given regex.

parameter :some_param, format: ~r/foo/

length

Checks the length of a parameter's value.

parameter :some_param, length: %{min: 5, max: 10, is: 7, in: 5..8}

inner

Checks the inner of either Map or Keyword parameter.

parameter :some_param, type: :map, inner: %{
  a: [type: :integer, required: true],
  b: [type: :string, length: %{min: 1, max: 6}]
}

struct

Checks whether the given parameter is expected structure.

parameter :some_param, struct: %SomeStruct{}

list_item

Checks whether each of list items conforms defined checks. An item's checks could be any that Exop offers:

parameter :list_param, list_item: %{type: :string, length: %{min: 7}}

func

Checks whether an item is valid over custom validation function.

parameter :some_param, func: &__MODULE__.your_validation/2

def your_validation({param_name, param_value}, all_received_params_map) do
  # your validation logic based on given arguments is here
end

allow_nil

It is not a parameter check itself, because it doesn't return any validation errors. It is a parameter attribute which allow you to have other checks for a parameter whilst have a possibility to pass nil as the parameter's value. If nil is passed all the parameter's checks are ignored during validation.

from

This option allows you to pass a parameter to run/1 and run!/1 functions with one name and work with this parameter within an operation under another name.

parameter :a, type: :integer, from: "a"

subset_of

Checks whether a parameter's value (list) is a subset of a defined check-list. To pass this check, all items within given into an operation parameter should be included into check-list, otherwise the check is failed.

parameter :some_param, subset_of: [1, 2, :a, "b", C]

Interrupt

In some cases you might want to make an 'early return' from process/1 function. For this purpose you can call interrupt/1 function within process/1 and pass an interruption reason to it. An operation will be interrupted and return {:interrupt, your_reason}

def process(_params) do
  interrupt(%{fail: "oops"})
  :ok # will not return it
end

Coercion

It is possible to coerce a parameter before the contract validation, all validation checks will be invoked on coerced parameter value. Since coercion changes a parameter before any validation has been invoked, default values are resolved (with :default option) before the coercion. The flow looks like: Resolve param default value -> Coerce -> Validate coerced

parameter :a, type: :string, coerce_with: &__MODULE__.to_string/2

def to_string({:a, value}, %{} = _received_params) when is_integer(value) do
  Integer.to_string(value)
end
def to_string({:a, value}, %{} = _received_params) when is_binary(value) do
  value
end

For more information and examples check out general Exop docs.

Link to this macro

policy(policy_module, action_name)

View Source (macro)

Specs

policy(module(), atom()) :: any()

Defines a policy that will be used for authorizing the possibility of a user to invoke an operation.

defmodule ReadOperation do
  use Exop.Operation

  policy MonthlyReportPolicy, :can_read?

  parameter :user, required: true, struct: User

  def process(params) do
    authorize(params.user)

    # make some reading...
  end
end

A policy itself might be:

defmodule MonthlyReportPolicy do
  # not only Keyword or Map as an argument since 1.1.1
  def can_read?(%User{role: "manager"}), do: true
  def can_read?(_opts), do: false

  def can_write?(%User{role: "manager"}), do: true
  def can_write?(_opts), do: false
end

Link to this section Callbacks

Specs

process(map()) ::
  {:ok, any()}
  | Exop.Validation.validation_error()
  | {:interrupt, any()}
  | {:error, any()}
  | :ok
  | no_return()

Operation's entry point. Takes defined contract as the single parameter. Contract itself is a list of maps: [%{name: atom(), opts: keyword()}]