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
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.