GoodServices
This is a very simple gem I've created to keep the logic I've been frequently using in my apps Service Objects.
I intend to keep it as simple as possible so it can be versatile and easy to use, but ideas and pull-requests are welcome :)
Installation
Add this line to your application's Gemfile:
gem 'good_services'
And then execute:
$ bundle
Or install it yourself as:
$ gem install good_services
Usage
The best way to understand this gem is to have a look at the GoodServices::Base file. It is the only file that matters for this gem (really).
To get started just create a new class inheriting from GoodServices::Base
Sample Service Object:
module Pet
#
# Errors
#
InvalidPetError = Class.new(StandardError)
NoNewNameError = Class.new(StandardError)
#
# Service
#
class Renamer < GoodServices::Base
rescuable_from InvalidPetError, NoNewNameError
attr_reader :new_name
def initialize(pet: pet, new_name: new_name)
@record = pet
@new_name = new_name
end
def perform
@record.name = new_name
end
def validate
raise InvalidPetError unless record.respond_to? :name
raise NoNewNameError unless new_name
end
end
end
Generators
When using Rails you can use our generator to get a simple Service boilerplate:
rails generate good_services:service PetRenamer
If you prefer using namespaces for your services (we recommend it), you can use it like that:
rails generate good_services:service Pets::Renamer
Basic usage
The run method is considered the safe way to run the service (always returns true or false)
Pet::Renamer.run(pet: Pet.new, name: "Flipsy")
# => true
Pet::Renamer.run(pet: nil, name: "Flipsy")
# => false
The run! method does the same, except that it doesn't rescue any errors (always returns true or raises an exception)
Pet::Renamer.run!(pet: Pet.new, name: "Flipsy")
# => true
Pet::Renamer.run!(pet: nil, name: "Flipsy")
# => InvalidPetError
By default the following variables are available as attr_readers:
# Used when the service operates on only one object
@record
# Used when the service operates on a collection of objects
@collection
# If you run the service with the safe ".run" method, the @error
# variable will be filled with any rescuable error defined in the service.
@error
--
Rescuable Exceptions
You can define a list of exceptions that are actually expected to happen eventually and the safe ".run" method will rescue you out of them returning false instaead.
The exception will be saved in the @error variable and accessible to the outside for custom error messages and etc. Here is how you use it:
class Renamer < GoodServices::Base
rescuable_from InvalidPetError, NoNewNameError
(...)
end
--
Methods
Perform
The only required method for your service is the perform method. It will have the logic called by your .run and .run! methods. Try to always use methods that raise exceptions, like the example below (in Rails)
def perform
@record.update!(name: new_name)
end
Validate
This method is optional, but will be called before your perform method when you ".run" or ".run!" the service. It should raise predictable errors based on your validations, for example:
def validate
raise InvalidPetError unless record.respond_to? :name
raise NoNewNameError unless new_name
end
*I'm not proud of this syntax, I appreciate any insights on a better way to do this.
Development
To-do
- Improve the way validations are done (right now the "raise x if y" stuff is kinda ugly)
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/victoryam/good_services.
License
The gem is available as open source under the terms of the MIT License.