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 aims to attack the issue of Service Object patterns in a way that focuses on reusing logic and ease of testability.
Add this line to your application's Gemfile:
gem 'atacama'
And then execute:
$ bundle
Or install it yourself as:
$ gem install atacama
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
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/fulcrumapp/atacama.