Module: Chat::Service::Base

Extended by:
ActiveSupport::Concern
Included in:
TrashChannel, UpdateChannel, UpdateChannelStatus, UpdateUserLastRead
Defined in:
plugins/chat/app/services/base.rb

Overview

Module to be included to provide steps DSL to any class. This allows to create easy to understand services as the whole service cycle is visible simply by reading the beginning of its class.

Steps are executed in the order they’re defined. They will use their name to execute the corresponding method defined in the service class.

Currently, there are 5 types of steps:

  • model(name = :model): used to instantiate a model (either by building it or fetching it from the DB). If a falsy value is returned, then the step will fail. Otherwise the resulting object will be assigned in context[name] (context[:model] by default).

  • policy(name = :default): used to perform a check on the state of the system. Typically used to run guardians. If a falsy value is returned, the step will fail.

  • contract(name = :default): used to validate the input parameters, typically provided by a user calling an endpoint. A special embedded Contract class has to be defined to holds the validations. If the validations fail, the step will fail. Otherwise, the resulting contract will be available in context.

  • step(name): used to run small snippets of arbitrary code. The step doesn’t care about its return value, so to mark the service as failed, #fail! has to be called explicitly.

  • transaction: used to wrap other steps inside a DB transaction.

The methods defined on the service are automatically provided with the whole context passed as keyword arguments. This allows to define in a very explicit way what dependencies are used by the method. If for whatever reason a key isn’t found in the current context, then Ruby will raise an exception when the method is called.

Regarding contract classes, they have automatically ActiveModel modules included so all the ActiveModel API is available.

Examples:

An example from the TrashChannel service

class TrashChannel
  include Base

  model :channel, :fetch_channel
  policy :invalid_access
  transaction do
    step :prevents_slug_collision
    step :soft_delete_channel
    step :log_channel_deletion
  end
  step :enqueue_delete_channel_relations_job

  private

  def fetch_channel(channel_id:, **)
    ChatChannel.find_by(id: channel_id)
  end

  def invalid_access(guardian:, channel:, **)
    guardian.can_preview_chat_channel?(channel) && guardian.can_delete_chat_channel?
  end

  def prevents_slug_collision(channel:, **)
    
  end

  def soft_delete_channel(guardian:, channel:, **)
    
  end

  def log_channel_deletion(guardian:, channel:, **)
    
  end

  def enqueue_delete_channel_relations_job(channel:, **)
    
  end
end

An example from the UpdateChannelStatus service which uses a contract

class UpdateChannelStatus
  include Base

  model :channel, :fetch_channel
  contract
  policy :check_channel_permission
  step :change_status

  class Contract
    attribute :status
    validates :status, inclusion: { in: ChatChannel.editable_statuses.keys }
  end

  
end

Defined Under Namespace

Classes: Context, Failure

Class Method Summary collapse

Class Method Details

.contract(name = :default, class_name: self::Contract, use_default_values_from: nil) ⇒ Object

Checks the validity of the input parameters. Implements ActiveModel::Validations and ActiveModel::Attributes.

It stores the resulting contract in context [“contract.default”] by default (can be customized by providing the name argument).

Examples:

contract

class Contract
  attribute :name
  validates :name, presence: true
end

Parameters:

  • name (Symbol) (defaults to: :default)

    name for this contract

  • class_name (Class) (defaults to: self::Contract)

    a class defining the contract

  • use_default_values_from (Symbol) (defaults to: nil)

    name of the model to get default values from

.model(name = :model, step_name = :"fetch_#{name}") ⇒ Object

Evaluates arbitrary code to build or fetch a model (typically from the DB). If the step returns a falsy value, then the step will fail.

It stores the resulting model in context[:model] by default (can be customized by providing the name argument).

Examples:

model :channel, :fetch_channel

private

def fetch_channel(channel_id:, **)
  ChatChannel.find_by(id: channel_id)
end

Parameters:

  • name (Symbol) (defaults to: :model)

    name of the model

  • step_name (Symbol) (defaults to: :"fetch_#{name}")

    name of the method to call for this step

.policy(name = :default) ⇒ Object

Performs checks related to the state of the system. If the step doesn’t return a truthy value, then the policy will fail.

Examples:

policy :no_direct_message_channel

private

def no_direct_message_channel(channel:, **)
  !channel.direct_message_channel?
end

Parameters:

  • name (Symbol) (defaults to: :default)

    name for this policy

.step(name) ⇒ Object

Runs arbitrary code. To mark a step as failed, a call to #fail! needs to be made explicitly.

Examples:

step :update_channel

private

def update_channel(channel:, params_to_edit:, **)
  channel.update!(params_to_edit)
end

using #fail! in a step

step :save_channel

private

def save_channel(channel:, **)
  fail!("something went wrong") unless channel.save
end

Parameters:

  • name (Symbol)

    the name of this step

.transaction(&block) ⇒ Object

Runs steps inside a DB transaction.

Examples:

transaction do
  step :prevents_slug_collision
  step :soft_delete_channel
  step :log_channel_deletion
end

Parameters:

  • block (Proc)

    a block containing steps to be run inside a transaction