[![Build Status](](

# Arpeggiate

Write step operations with input validation, type casting, and error handling.

## Steps

Quite often in software you want to perform a series of steps towards an end result while gracefully handling error conditions. This is the goal of arpeggiate. With arpeggiate, you can define any number of steps to perform. Each step hands its state to the next step in a functional way using `{:ok, state}`. Error handlers can be defined for each step to facilitate cleanup or other such error-handling activities. Triggering the error state is as simple as returning an `{:error, state}` tuple. Error handlers are optional. Steps without error handlers will proceed to the next step regardless of their outcome.

Generally you want to work with step state, but occasionally directly accessing the raw params as passed into the operation is useful. As such, steps may be either arity 1 `(state)` or, if you require access to the params in a step or error handler, arity 2 `(state, params)` is also supported.

## Schema

Arpeggiate leverages many of the robust casting and validation features of the Ecto project to process each operation's input parameters. Arpeggiate's operation state is an `Ecto.Changeset`, which provides a familiar interface for changing the state and handling error messages.

The schema of the operation state is defined by passing a `schema` block.

## Loading

Typically input parameters are cast to the state struct and validation is optionally run. We do this by defining a load method that receives the params and converts it into state.

## Processing

To run the operation, we call `process` with the input parameters. If the whole operation succeeds, an `{:ok, state}` tuple is returned, with state being the state returned by the last step. In the error case, the step sequence is halted, an `{:error, state, validation_step}` tuple is returned, with state being the state returned by the failing step's error handler and `validation_step` being the name of the step that failed (represented as an atom).

If you need validation, call `validate` right away and pass the result to the steps. If you don't need validation, you can call `step` directly.

## Example

Let's say we want to take some money from Sam in exchange for baking him a pie. If baking the pie fails, we want to clean up by sending Sam a refund and an apology email.

defmodule PayForPie do
  use Arpeggiate

  schema do
    field :email, :string
    field :pie_type, :string
    field :credit_card_number, :integer

    # let's say we have a Payment struct defined in our app, we can cast the
    # result of payment into a Payment struct using Ecto embedding
    embeds_one :payment, Payment

  load fn params ->
    # you can use any Ecto validation you want here, including any custom
    # validators you have written
    |> cast(params, [:email, :pie_type, :credit_card_number])
    |> validate_required([:email, :pie_type, :credit_card_number])

  def process(params) do
    |> step(:run_credit_card, error: :payment_failed)
    |> step(:bake_pie, error: :baking_failed)
    |> run(params)

  # --- step 1

  # steps can be defined with arity 1 or arity 2, taking either (state, params)
  # arguments, or just (state) if params aren't needed for the particular step
  def run_credit_card(state, params) do
    {status, payment} = CreditCard.charge(state.credit_card_number)

    # if the result of CreditCard.charge is an {:ok, payment} tuple, the
    # operation will continue to the next step with the updated state. if the
    # result is an {:error, payment} tuple, the operation will halt and run the
    # error handler specified for the step. in either case we want to cast the
    # payment into the state
    {status, state |> cast_embed(:payment, payment)}

  def payment_failed(state) do
    # no need for a status tuple since this is always an error condition

  # --- step 2

  # here's an example of an arity 1 step where params aren't used
  def bake_pie(state) do
    # if Pie.bake returns an {:ok, pie} tuple, the operation will return the
    # pie. if Pie.bake returns an {:error, _something_else} tuple, the
    # operation will run the error handler specified for the step

  def baking_failed(state) do
    {:ok, refund} = CreditCard.refund(
    {:ok, email} = Mailer.send_apology(
    {:error, state}

With this operation, you'd call it like so:

case PayForPie.process(
    "email" => "",
    "pie_type" => "cherry",
    "credit_card_number" => "4242424242424242"
) do
  {:ok, pie} ->
    # everything succeeded!
  {:error, :payment_failed, :run_credit_card} ->
    # uh oh