Active Call
Active Call provides a standardized way to build service objects.
It helps you extract complex business logic from models and controllers, and keeps your code clean and organized.
Encouraging single responsibility improves readability, testability, and long-term maintainability.
Installation
Install the gem and add to the application's Gemfile by executing:
If bundler is not being used to manage dependencies, install the gem by executing:
Usage
Your child classes should inherit from ActiveCall::Base.
You can add your own service object classes in your gem's lib folder or your project's app/services folder.
Each service object must define only one public method named call.
Logic Flow
- Before invoking
call.
-
Validate the service with
validates. -
Use the
before_callhook to set up anything, like network requests after validation passes. -
Validate the request with
validate on: :request.
- During
callinvocation.
- A
responseattribute gets set with the result of thecallmethod.
- After invoking
call.
-
Validate the response with
validate on: :response. -
Use the
after_callhook to set up anything after response validation passes. -
Return the service object.
If any validations fail during this flow, the service object gets returned without continuing with the subsequent steps.
Example Service Object
Define a service object with optional validations and callbacks.
require 'active_call' class YourGem::FooResource::CreateService < ActiveCall::Base attr_reader :message, :bar_service, :baz_service validates :message, presence: true before_call :call_mandatory_bar_service, :call_optional_baz_service validate on: :request do errors.add(:message, :invalid, message: 'bar is not happy') if bar_service.response[:bar] == '๐ก Leave me alone' end validate on: :response do errors.add(:message, :invalid, message: 'cannot be baz') if response[:foo] == '๐คจ Greetings from baz' end after_call :log_response def initialize(message: nil) @message = message end def call { foo: build_message_response } end private def call_mandatory_bar_service @bar_service = YourGem::BarResource::CreateService.call(message: message) unless bar_service.success? errors.merge!(bar_service.errors) throw :abort end end def call_optional_baz_service @baz_service = YourGem::BazResource::UpdateService.call(id: '1', message: message) unless baz_service.success? errors.merge!(baz_service.errors) # No need to `throw :abort` if the service doesn't depend on a result from the `baz_service`. end end def build_message_response return baz_service.response[:baz] if baz_service.success? bar_service.response[:bar] end def log_response puts "Successfully called #{response}" end end service = YourGem::FooResource::CreateService.call(message: '๐ Hi there') service.response # => { foo: '๐คฉ Greetings from bar' }
Using call
You will get an errors object when validation fails.
service = YourGem::FooResource::CreateService.call(message: '') service.success? # => false service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]> service.errors.full_messages # => ["Message can't be blank"] service.response # => nil
A response object on a successful call invocation.
service = YourGem::FooResource::CreateService.call(message: ' bar ') service.success? # => true service.response # => {:foo=>"bar"}
And an errors object if you added errors during the validate, on: :response validation.
service = YourGem::FooResource::CreateService.call(message: 'baz') service.success? # => false service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]> service.errors.full_messages # => ["Message cannot be baz"] service.response # => {:foo=>"baz"}
Using call!
An ActiveCall::ValidationError exception gets raised when validation fails.
begin service = YourGem::FooResource::CreateService.call!(message: '') rescue ActiveCall::ValidationError => exception exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=blank, options={}>]> exception.errors.full_messages # => ["Message can't be blank"] end
A response object on a successful call invocation.
service = YourGem::FooResource::CreateService.call!(message: ' bar ') service.success? # => true service.response # => {:foo=>"bar"}
And an ActiveCall::RequestError exception gets raised if you added errors during the validate, on: :response validation.
begin service = YourGem::FooResource::CreateService.call!(message: 'baz') rescue ActiveCall::RequestError => exception exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=message, type=invalid, options={:message=>"cannot be baz"}>]> exception.errors.full_messages # => ["Message cannot be baz"] exception.response # => {:foo=>"baz"} end
Configuration
If you have secrets, use a configuration block.
class YourGem::BaseService < ActiveCall::Base self.abstract_class = true config_accessor :api_key, default: ENV['API_KEY'], instance_writer: false end
Then in your application code you can override the configuration defaults.
YourGem::BaseService.configure do |config| config.api_key = Rails.application.credentials.api_key || ENV['API_KEY'] end
And implement a service object like so.
require 'net/http' class YourGem::FooResource::CreateService < YourGem::BaseService def call Net::HTTP.get_response(URI("http://example.com/api?#{URI.encode_www_form(api_key: api_key)}")) end end
Gem Creation
To create your own gem for a service.
Build your gem.
bundle gem your_service --test=rspec --linter=rubocop --ci=github --github-username=<your_profile_name> --git --changelog --mit
Then add Active Call as a dependency in your gemspec.
spec.add_dependency 'active_call'
Now start adding your service objects in the lib directory and make sure they inherit from ActiveCall::Base.
Active Call Extensions
Gems Using Active Call
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec 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 the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/activecall/active_call.
License
The gem is available as open source under the terms of the MIT License.