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

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

Summary

Callbacks

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

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)

Defines a callback module that will be used for a successful operation as side effect.

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

@callback 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()}]

Functions

Link to this macro

authorize(opts \\ nil)

View Source (macro)
@spec 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

callback(callback_module, opts \\ [])

View Source (macro)
@spec callback(module(), any()) :: any()

Defines a callback module that will be used for a successful operation as side effect.

defmodule MultiplyByTenOperation do
  use Exop.Operation

  callback LiveProdcast, topic: "math", type: "mutliplication"

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

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

A callback module itself might be:

defmodule LiveProdcast do
  use Exop.Callback

  def process(operation_module, params_passed_to_the_operation, successful_result, opts) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, opts[:topic], %{type: opts[:type], payload: successful_result})
  end
end

opts is an open keyword list to pass needed options and metadata to the successful call.

Link to this macro

current_policy()

View Source (macro)
@spec current_policy() :: any()

Returns policy that was defined in an operation.

Link to this macro

fallback(fallback_module, opts \\ [])

View Source (macro)
@spec 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)
@spec 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)
@spec 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