Telephone ☎️

Telephone is a light weight utility that helps you create and call service objects from anywhere within your application.

Telepone comes with a simple interface that helps with:

  • Keeping your code DRY
  • Encapsulating complex logic in a more readable way
  • Making your code easier to test
  • Gracefully handling errors and validations

Installation

Add this line to your application's Gemfile:

gem 'telephone'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install telephone

Usage

Telephone::Service is a simple utility class for creating and calling service objects. It allows you to define arguments and validations, and provides a simple interface for calling the service and determining its success.

To start, define an ApplicationService and inherit from Telephone::Service.

# /app/services/application_service.rb

class ApplicationService < Telephone::Service
end

A very simple example of a service object:

class SimpleExample < ApplicationService
  def call
    "Hello World"
  end
end

s = SimpleExample.call #=> <#SimpleExample @result="Hello World">
s.success? #=> true
s.result #=> "Hello World"

Arguments

You can define arguments for the service object. These arguments will be passed to the service object's call method, and will be available as an attribute.

class SimpleExample < ApplicationService
  argument :name

  def call
    "Hello, #{name}."
  end
end

SimpleExample.call(name: "Benjamin").result #=> "Hello, Benjamin."

Arguments can also be required, which will prevent the service object from executing unless they are present.

class SimpleExample < ApplicationService
  argument :name, required: true

  def call
    "Hello, #{name}."
  end
end

s = SimpleExample.call
s.success? #=> false
s.errors.full_messages #=> ["Name can't be blank"]
s.result #=> nil

You can also give a default value for an argument.

argument :name, default: "Benjamin"

Defaults can also be callable (procs or lambdas) that are evaluated at runtime. Callable defaults have access to other attributes and are processed in definition order:

class GreetingService < ApplicationService
  argument :first_name, default: "John"
  argument :last_name, default: "Doe"
  argument :full_name, default: -> { "#{first_name} #{last_name}" }
  argument :created_at, default: -> { Time.current }

  def call
    "Hello, #{full_name}!"
  end
end

GreetingService.call.result #=> "Hello, John Doe!"
GreetingService.call(first_name: "Jane").result #=> "Hello, Jane Doe!"

Arguments can also be validated against a type. When type: is provided, the argument is checked with is_a? before the service runs. nil is allowed unless the argument is also marked as required:.

class UpdateUserService < ApplicationService
  argument :user, type: User, required: true

  def call
    user.update!(status: "active")
  end
end

s = UpdateUserService.call(user: "not a user")
s.success? #=> false
s.errors.full_messages #=> ["User must be a User"]
s.result #=> nil

Type validation works with any class or module:

argument :tags, type: Array
argument :metadata, type: Enumerable

Validations

Since Telephone::Service includes ActiveModel::Model, you can define validations in the same way you would for an ActiveRecord model.

validates :name, format: { with: /\A[a-zA-Z]+\z/ }
validate :admin_user?

def admin_user?
  errors.add(:user, "not admin") unless user.admin?
end

If a validation fails, the service object will not execute and return nil as the result of the call. You can check the status of the service object by calling success?.

s = SomeService.call
s.success? #=> false

Internationalization

Type validation errors are translated through I18n. Telephone ships with translations for:

  • en — English
  • es — Spanish
  • fr — French
  • de — German
  • pt — Portuguese
  • ja — Japanese
  • zh-CN — Chinese (Simplified)
  • it — Italian
  • nl — Dutch
  • ru — Russian
  • ko — Korean

In a Rails app, these are used automatically based on the current locale:

I18n.locale = :es

s = UpdateUserService.call(user: "not a user")
s.errors.full_messages #=> ["User debe ser un User"]

You can also override the default message in your own locale files:

# config/locales/telephone.en.yml
en:
  activemodel:
    errors:
      messages:
        invalid_type: "must be an instance of %{type}"

Best Practices

Service objects are a great way to keep your code DRY and encapsulate business logic. As a rule of thumb, try to keep your service objects to a single responsibility. If you find yourself dealing with very complex logic, consider breaking it up into smaller services.

Development

After checking out the repo, run bin/setup to install dependencies. This will install the dependencies for each subgem and run the entire test suite.

To experiment, you can run bin/console for an interactive prompt.

Documentation

This project is documented using YARD. All code should be well documented via the YARD syntax prior to merging.

You can access the docmentation by starting up the YARD server.

yard server --reload

The --reload, or -r, flag tells the server to auto reload the documentation on each request.

Once the server is running, the documentation will by available at http://localhost:8808