Exop hex.pm version API Docs Build Status

Little library that provides a few macros which allow you to encapsulate business logic and validate incoming params over predefined contract.

Inspired by Trailblazer::Operation - a part of awesome high-level architecture for ruby/rails applications.

Installation

def deps do
  [{:exop, "~> 0.1.1"}]
end

Operation definition

defmodule IntegersDivision do
  use Exop.Operation

  parameter :a, type: :integer, default: 1
  parameter :b, type: :integer, required: true,
                numericality: %{greater_than: 0}

  def process(params) do
    result = params[:a] / params[:b]
    IO.inspect "The division result is: #{result}"
  end
end

Exop.Operation provides parameter macro, which is responsible for the contract definition. Its spec is @spec parameter(atom, Keyword.t) :: none, we define parameter name as the first atom attribute and parameter options as the second Keyword attribute.

Parameter options determine a contract of a parameter, a set of parameters contracts is an operation contract.

Business logic of an operation is defined in process/1 function, which is required by the Exop.Operation module behaviour.

After the contract and business logic were defined, you can invoke the operation simply by calling run/1 function:

iex> IntegersDivision.run(a: 50, b: 5)
"The division result is: 10"

Parameter options

A parameter options could have various checks. Here the list of checks available yet:

  • type
  • required
  • default
  • numericality
  • in
  • not_in
  • format
  • length
  • inner

type

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

parameter :some_param, type: :map

Exop handle almost all Elixir types:

  • :boolean
  • :integer
  • :float
  • :string
  • :tuple
  • :map
  • :struct
  • :list
  • :atom
  • :function

Unknown type always passes this check.

required

Checks the presence of a parameter in passed to run/1 params collection.

parameter :some_param, required: true

default

Checks the presence of a parameter in passed to run/1 params collection, and if the parameter is missed - assign default value to it.

parameter :some_param, default: "default value"

numericality

Checks whether a parameter’s value is a number and other numeric constraints. All possible constraints are listed in the example below.

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}

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. The value should be one of handled types:

  • list (items count)
  • string (chars count)
  • atom (treated as string)
  • map (key-value pairs count)
  • tuple (items count)

length check is complex as numericality (should define map of inner checks). All possible checks are listed in the example below.

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

inner

Checks the inner of either Map or Keyword parameter. It applies checks described in inner map to related inner items.

# some_param = %{a: 3, b: "inner_b_attr"}

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

And, of course, all checks on a parent parameter (:some_param in the example) are still applied.

Validation result

If received parameters passed a contract validation, a code defined in process/1 will be invoked. Or you will receive @type validation_error :: {:error, :validation_failed, Map.t} as a result otherwise. Map.t as errors reasons might look like this:

%{param1: ["has wrong type"], param2: ["is required", "must be equal to 3"]}

Operation invocation

As said earlier, operations in most cases called by run/1 function. This function receives parameters collection. It’s not required to pass to run/1 function parameters only described in the operation’s contract, but only described parameters will be validated.

run/1 function validate received parameters over the contract and if all parameters passed the validation, the run/1 function calls the code defined in process/1 function.

iex> SomeOperation.run(param1: 1, param2: "2")
_some_result_

If at least one of the given parameters didn’t pass the validation process/1 function’s code will not be invoked and corresponding warning in the application’s log will appear.

You always can bypass the validation simply by calling process/1 function itself, if needed.

LICENSE

Copyright © 2016 Andrey Chernykh ( andrei.chernykh@gmail.com )

This work is free. You can redistribute it and/or modify it under the
terms of the MIT License. See the LICENSE file for more details.