GoodServices

Code Climate Gem Version Build Test Coverage

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.