Relevium

Gem

Simple ruby gem for rails projects with some useful patterns that would help you build more scalable apps

Table of contents

  1. Service object
    1. Listeners
  2. Background service
  3. Form object
    1. Serialization with forms

Service object

To the start

Interface for running services - a place to move your business logic.

Example:

class TestService < Relevium::Service
  attr_reader :user_id

  def initialize(user_id)
    @user_id = user_id
  end

  def call
    return broadcast(:fail) unless user_valid?

    do_some_stuff
    book = user.books.last
    user.delete
    broadcast(:ok, book)
  end

  private

  def user_valid?
    user.name == 'Kyle'
  end

  def do_some_stuff
    #..more_code_here..
  end

  def user
    @user ||= User.find_by(id: user_id)
  end
end

Results:

@user = User.last # Name is 'Kyle'

TestService.call(@user.id)
# Will delete user and broadcast method won't take affect

TestService.call(@user.id) do |obj|
  obj.on(:fail) { raise 'Some error' } # Listener for fail broadcast
  obj.on(:ok) { |book|  @book = book } # Listener for ok broadcast
end
# User will be deleted and @book variable will be assigned

@user.update(name: 'Stan')
TestService.call(@user.id) do |obj|
  obj.on(:fail) { raise 'Some error' }
  obj.on(:ok) { |book|  @book = book }
end
# User won't be deleted and Some error' will be raised

Adding listeners

To the start

Example:

class TestListener < Relevium::Service
  attr_reader :email

  def initialize(email)
    @email = email
  end

  def call
    unsubscribe_from_sendgrid
  end

  private

  def unsubscribe_from_sendgrid
    # your code
  end
end
class TestService < Relevium::Service
  set_listener TestListener, :ok
  attr_reader :user_id

  def initialize(user_id)
    @user_id = user_id
  end

  def call
    do_some_stuff
    user.delete
    user.persisted? ? broadcast(:ok, user.email) : broadcast(:fail)
  end

  private

  def do_some_stuff
    #..more_code_here..
  end

  def user
    @user ||= User.find_by(id: user_id)
  end
end

Results:

@user = User.last

TestService.call(@user.id)
# Will delete user and broadcast method won't take affect.
# However listener's `call` function will be called 
# with `email` passed as the argument to initialize function.

Specify which function from listener to call:

class TestListener < Relevium::Service
  def initialize(arg)
    @arg = arg
  end

  def on_ok
    # ok code
  end

  def on_fail
    # fail code
  end
end

set_listener TestListener, :ok, function: :on_ok
set_listener TestListener, :fail, function: :on_fail

Specify arguments that should be passed to the listener:

class TestListener < Relevium::Service
  attr_reader :arg

  def initialize(arg)
    @arg = arg
  end

  def call
    puts arg
  end
end

class TestService < Relevium::Service
  set_listener TestListener, :ok, args: :user_id

  def initialize(user_id)
    @user_id = user_id
  end

  def call
    # some code
    broadcast(:ok, 'test')
  end
end

Result:

TestService.call(1) do |service|
  service.on(:ok) { |argument| puts argument }
end

# Output:
# test
# 1

Set up condition to call listener:

class TestService < Relevium::Service
  set_listener TestListener, :ok, if: Proc.new { |service| !service.user.persisted? }

  def initialize(user_id)
    @user = User.find(user_id)
  end

  def call
    @user.delete
    broadcast(:ok)
  end
end

Results:

TestService.call(User.last.id)
# Listener would be trigger only if user was deleted.

Background service

To the start

Simple interface for running background jobs as services. Meant to be inherited like this:

class SidekiqService < Relevium::BackgroundService
  def initialize(options)
    super(options, ServiceObjectWorker)
  end
end

Needs a sidekiq worker in order to work, that should look like this:

class ServiceObjectWorker
  include Sidekiq::Worker
  sidekiq_options queue: :default, retry: true

  def perform(klass, *args)
    klass.constantize.call(args)
  end
end

Now you can create a service like this:

class SimpleService < SidekiqService
attr_reader :foo

  def initialize(foo, options = {})
    super(options)
    @foo = foo
  end

  private

  def perform
    puts foo
  end
end

And invoke it like this:

SimpleService.call('foo', background: true, perform_in: 15.minutes)
# Will set sidekiq worker to be performd in 15 minutes,
# that will print 'foo' in sidekiq console

Note that listeners won't work when service is being called as background job.

Form object

To the start

Form object is used to move validations from models. Usually used when similar model needs different validations on different forms. Can be used to build attributes for model to save.

Example:

class UserForm < Relevium::Form

  attribute :user_id, Integer
  attribute :user_name, String
  attribute :sibling_name, String, remove_from_hash: true

  include_in_hash :sibling_id

  validates :user_id, :user_name, presence: true
  validate :is_kyle

  private

  def is_kyle
    return true if user_name == 'Kyle'

    errors.add(:user_name, 'should be Kyle')
  end

  def sibling_id
    @sibling_id ||= User.find_by(name: sibling_name)
  end
end

Results:

user_params = {user_id: 1, user_name: 'Kyle', sibling_name: 'John'}
form = UserForm.new(user_params)
form.valid? # true
form = UserForm.new(user_params.merge(user_name: 'Steve'))
form.valid? # false
form.errors.full_messages # ["User name should be Kyle"]
form = UserForm.new(user_params.merge(user_id: 'test'))
form.valid? # false
form.errors.full_messages # ["User can't be blank"]
form = UserForm.new(user_params.merge(user_id: '1')) # Will convert user_id into Integer
form.to_h # { user_id: 1, user_name: 'Kyle', sibling_id: 12 }

form.set(user_name, 'Stan')
form.set_attributes(user_id: 2, sibling_name: 'Ken')
form.to_h # { user_id: 2, user_name: 'Stan', sibling_name: 'Ken' }

Serialization with forms

To the start

class UserForm < Relevium::Form
  attribute :available_cash, Float

  serialize_attributes :user_id, :sibling_id, :available_cash, :user_full_name

  def user_full_name
    first_name + ' ' + last_name
  end
end

Now you can use this form to serialize active records to hash:

ap User
# User < ActiveRecord::Base {
#  :user_id => :integer,
#  :available_cash => :string,
#  :sibling_id => :integer,
#  :first_name => :string,
#  :last_name => :string
# }
UserForm.from_model(User.last).serialize
# Output: 
# { user_id: 1, sibling_id: 2, available_cash: 123.45, user_full_name: 'Ken Stevenson' }

Also you can serialize active record collection or array of active records:

UserForm.serialize_relation(User.where(id: (1..15)))
UserForm.serialize_relation(User.last(3).to_a)