Coder Social home page Coder Social logo

call_sheet's Introduction

Call Sheet

Gem Version Code Climate API Documentation Coverage

Call Sheet is a business transaction DSL. It provides a simple way to define a complex business transaction that includes processing by many different objects. It makes error handling a primary concern by using a “Railway Oriented Programming” approach for capturing and returning errors from any step in the transaction.

Call Sheet is based on the following ideas, drawn mostly from Transflow:

  • A business transaction is a series of operations where each can fail and stop processing.
  • A business transaction resolves its dependencies using an external container object and it doesn’t know any details about the individual operation objects except their identifiers.
  • A business transaction can describe its steps on an abstract level without being coupled to any details about how individual operations work.
  • A business transaction doesn’t have any state.
  • Each operation shouldn’t accumulate state, instead it should receive an input and return an output without causing any side-effects.
  • The only interface of a an operation is #call(input).
  • Each operation provides a meaningful functionality and can be reused.
  • Errors in any operation can be easily caught and handled as part of the normal application flow.

Why?

Requiring a business transaction's steps to exist as independent operations directly addressable voa a container means that they can be tested in isolation and easily reused throughout your application. Following from this, keeping the business transaction to a series of high-level, declarative steps ensures that it's easy to understand at a glance.

The output of each step is wrapped in a Kleisli Either object (Right for success or Left for failure). This allows the steps to be chained together and ensures that processing stops in the case of a failure. Returning an Either from the overall transaction also allows for error handling to remain a primary concern without it getting in the way of tidy, straightforward operation logic. Wrapping the step output also means that you can work with a wide variety of operations within your application – they don’t need to return an Either already.

Usage

Invoking a call sheet requires two things: a container that contains available operations. And the actual call sheet that configures which operations to run, and the order.

Container

The container groups arbitray operations, and could be a plain hash.

container = {
  # name:   -> operation
  process:  -> input { {name: input["name"], email: input["email"]} },
  validate: -> input { input[:email].nil? ? raise(ValidationFailure, "not valid") : input },
  persist:  -> input { DB << input and true }
}

Each operation has to respond to #call(input).

Note that we use a hash here for simplicity, for larger apps you may like to consider something like dry-container.

Call Sheet

To integrate your operations into a specific business transaction, you define a call sheet.

save_user = CallSheet(container: container) do
  # adapter :operation_name
  map       :process
  try       :validate, catch: ValidationFailure
  tee       :persist
end

In a call sheet, you reference operations from your container using adapter methods.

When invoking the call sheet, these operations will be called in the specified order.

DB = []

save_user.call("name" => "Jane", "email" => "[email protected]")
# => Right({:name=>"Jane", :email=>"[email protected]"})

DB
# => [{:name=>"Jane", :email=>"[email protected]"}]

As a result, all three user operations :process, :validate and :persist will be called. Their result is interpreted by the adapter that you specified in the call sheet.

A full invocation of a call sheet is called transaction.

Adapter

The following adapters to plug operations into the sheet are available.

  • map – any output is considered successful and returned as Right(output)
  • try – the operation may raise an exception in an error case. This is caught and returned as Left(exception). The output is otherwise returned as Right(output).
  • tee – the operation interacts with some external system and has no meaningful output. The original input is passed through and returned as Right(input).
  • raw or step – the operation already returns its own Either object, and needs no special handling.

Transaction Execution

Each transaction returns a result value wrapped in a Left or Right object. You can handle these different results (including errors arising from particular steps) with a match block:

save_user.call(name: "Jane", email: "[email protected]") do |m|
  m.success do
    puts "Succeeded!"
  end

  m.failure do |f|
    f.on :validate do |errors|
      # In a more realistic example, you’d loop through a list of messages in `errors`.
      puts "Couldn’t save this user. Please provide an email address."
    end

    f.otherwise do |error|
      puts "Couldn’t save this user."
    end
  end
end

Passing additional step arguments

Additional arguments for step operations can be passed at the time of calling your transaction. Provide these arguments as an array, and they’ll be splatted into the front of the operation’s arguments. This means that transactions can effectively support operations with any sort of #call(*args, input) interface.

DB = []

container = {
  process:  -> input { {name: input["name"], email: input["email"]} },
  validate: -> allowed, input { input[:email].include?(allowed) ? raise(ValidationFailure, "not allowed") : input },
  persist:  -> input { DB << input and true }
}

save_user = CallSheet(container: container) do
  map :process
  try :validate, catch: ValidationFailure
  tee :persist
end

input = {"name" => "Jane", "email" => "[email protected]"}
save_user.call(input, validate: ["doe.com"])
# => Right({:name=>"Jane", :email=>"[email protected]"})

save_user.call(input, validate: ["smith.com"])
# => Left("not allowed")

Subscribing to step notifications

As well as pattern matching on the final transaction result, you can subscribe to individual steps and trigger specific behaviour based on their success or failure:

NOTIFICATIONS = []

module UserPersistListener
  extend self

  def persist_success(user)
    NOTIFICATIONS << "#{user[:email]} persisted"
  end

  def persist_failure(user)
    NOTIFICATIONS << "#{user[:email]} failed to persist"
  end
end


input = {"name" => "Jane", "email" => "[email protected]"}

save_user.subscribe(persist: UserPersistListener)
save_user.call(input, validate: ["doe.com"])

NOTIFICATIONS
# => ["[email protected] persisted"]

This pub/sub mechanism is provided by the Wisper gem. You can subscribe to specific steps using the #subscribe(step_name: listener) API, or subscribe to all steps via #subscribe(listener).

Working with a larger container

In practice, your container won’t be a trivial collection of generically named operations. You can keep your transaction step names simple by using the with: option to provide the identifiers for the operations within your container:

save_user = CallSheet(container: large_whole_app_container) do
  map :process, with: "attributes.user"
  try :validate, with: "validations.user", catch: ValidationFailure
  tee :persist, with: "persistance.commands.update_user"
end

A raw step (also aliased as step) can be used if the operation in your container already returns an Either and therefore doesn’t need any special handling.

Installation

Add this line to your application’s Gemfile:

gem "call_sheet"

Run bundle to install the gem.

Contributing

Bug reports and pull requests are welcome on GitHub.

Credits

Call Sheet is developed and maintained by Icelab.

Call Sheet’s error handling is based on Scott Wlaschin’s Railway Oriented Programming, found via Zohaib Rauf’s Railway Oriented Programming in Elixir blog post. Call Sheet’s behavior as a business transaction library draws heavy inspiration from Piotr Solnica’s Transflow and Gilbert B Garza’s Solid Use Case. Josep M. Bach’s Kleisli gem makes functional programming patterns in Ruby accessible and fun. Thank you all!

License

Copyright © 2015 Icelab. Call Sheet is free software, and may be redistributed under the terms specified in the license.

call_sheet's People

Contributors

apotonick avatar timriley avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.