Skip to content

DRYer than a desert

Notifications You must be signed in to change notification settings

fulcrumapp/atacama

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Under Development

This project is under active development, so be prepared for APIs to just break until we get to a more stable version number.

Atacama

Build Status

Atacama aims to attack the issue of Service Object patterns in a way that focuses on reusing logic and ease of testability.

Installation

Add this line to your application's Gemfile:

gem 'atacama'

And then execute:

$ bundle

Or install it yourself as:

$ gem install atacama

Usage

The basic object is Contract. It enforces type contracts by utilizing dry-types.

class UserFetcher < Atacama::Contract
  option :id, Types::Strict::Number.gt(0)
  returns Types.Instance(User)

  def call
    User.find(id)
  end
end

UserFetcher.call(id: 1)

With the use of two classes, we can compose together multiple Contracts to yield a pipeline of changes to execute.

Steps contain two flow control objects:

  • Option(key: value) which informs the Transformer to take this value and yield it to the subsequent steps in the chain.
  • Return(value) halts execution and early returns from the pipeline. Useful for things like validation and error handling.

The Transformer always returns a value object.

class UserFetcher < Atacama::Step
  option :id, type: Types::Strict::Number.gt(0)
  returns Types.Option(model: Types.Instance(User))

  # Both #Option and #Return are flow control values that tell the transaction what is a
  # value object and what should halt execution and return.
  def call
    Option(model: User.find!(id))
  rescue ActiveRecord::RecordNotFound
    Return(Error.new('Not found'))
  end
end

# Around steps allow for yielding to child steps for things like instrumentation or
# ActiveRecord::Transactions.
class Duration < Atacama::Step
  def call
    start = Time.now
    yield
    $redis.avg('duration', Time.now - start)
  end
end

# The transaction class descends the queue of steps, yielding options to each step
# defined.
#
# Steps can be defined with:
#   * Procs
#   * Class references
#   * Instance methods
#
class UpdateUser < Atacama::Transformer
  option :id, type: Types::Strict::Number.gt(0)
  option :attributes, type: Types::Strict::Hash

  returns_option :model, Types.Instance(User) | Types.Instance(Error)

  step :duration, with: Duration do
    step :find, with: UserFetcher
    step :save
  end

  private

  def save
    context.model.update_attributes(attributes)
  end
end

UpdateUser.call(id: 1, attributes: {
  email: 'hello@world.com'
})

Any step can be mocked out without the need for a third party library. Just pass any object that responds to #call in the class initializer.

UpdateUser.new(steps: {
  save: lambda do
    puts "skipping save"
  end
})

Sometimes you need to compose these objects together and inject dependencies. Those injected values will be passed in to the object when it's later invoked with #call.

UpdateUser.inject(id: 1).call(attributes: { email: 'hello@world.com' })

Injected contracts can then be used inside of a Contract. Useful for Polymorphic objects.

class HistoryCreate < Atacama::Step
  option :history_class, type: Types::Strict::Class
  option :model, type: Types.Instance(ActiveRecord::Base)

  def call
    history_class.from_model(model)
  end
end

class UpdateUser < Atacama::Transformer
  option :id, type: Types::Strict::Number.gt(0)
  option :attributes, type: Types::Strict::Hash

  returns_option :model, Types.Instance(User) | Types.Instance(Error)

  step :duration, with: Duration do
    step :find, with: UserFetcher
    step :save, with: Saver
    step :history, with: HistoryCreate.inject(history_class: UserHistory)
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/fulcrumapp/atacama.