Tag: Rails

Rails + Responders + Trailblazer – Flash messages with proper model names from Trailblazer operations

Responders gem is really awesome. It sets flash message based on the controller action and resource status allowing you to dry up your Rails controllers. Trailblazer is a thin layer on top of Rails. It gently enforces encapsulation, an intuitive code structure and gives you an object-oriented architecture. When combining both of them together, you can achieve really slim and functional controllers:

class ContractsController < BaseController
  def index
    respond_with(collection Contracts::Index)
  end

  def show
    respond_with(present Contracts::Update)
  end

  def new
    respond_with(form Contracts::Create)
  end

  def create
    respond_with(run Contracts::Create)
  end

  def edit
    respond_with(form Contracts::Update)
  end

  def update
    respond_with(run Contracts::Update)
  end

  def destroy
    respond_with(run Contracts::Destroy)
  end
end

Unfortunately there's one downside: Since Responders #respond_with method expects a model as a first argument, it will think that your operation is a model (if you return it the way I did) and try to figure out it's human name. Probably it won't end up as you expect and you will see a message similar to this one:

Users/campaigns/update was successfully updated.

It happens because of this piece of code from Responders:

resource_name = if resource.class.respond_to?(:model_name)
  resource.class.model_name.human
else
  resource.class.name.underscore.humanize
end

Trailblazer operations don't respond to #model_name, that's why all the operations will be translated using the else statement. To make it work as expected, we just need to implement the #model_name class method:

class ApplicationOperation < Trailblazer::Operation
  def self.model_name
    ::ActiveModel::Name.new(model_class)
  end
end

I follow the ApplicationController, ApplicationRecord, ApplicationHelper "pattern" also with operations. That way I don't need to add this code into Trailblazer::Operation or into each of the operations separately. #model_class method is a method that will return the model class that we've set using Trailblazer::Operation #method:

class Operation < ApplicationOperation
 include Model
 model Contract
end

Operation.model_class #=> Contract
Operation.model_name #=> ActiveModel::Name:0x0000000575b778 @name="Contract"...

From now on, all the Trailblazer operations will introduce themselves to Responders with the name of the model on which we operate.

ActiveRecord isolated, namespaced scopes and methods using instance_exec and ActiveSupport::Concern

Rails concerns are a really good way to provide shared functionalities into your models. For example you can add scopes and then apply them in multiple classes:

module Statistics
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end
end

class Host
  include Statistics
end

One of the downsides of this solution is a "models pollution". Every model in which we want to include our module, will get all the methods and scopes from that module. With multiple concerns and extensions, it can create issues with methods and scopes overwriting each other. But as always with Ruby, there's a quite nice solution that will allow you to define custom scopes that will be encapsulated in a namespace method. This approach allows to have multiple scopes with the same name that will vary depending on an invocation context:

module Statistics
  extend ActiveSupport::Concern

  included do
    # Some magic here...
    scope :last_week, -> { where('created_at >= ?', 1.week.ago) }
  end
end

module Calculable
  extend ActiveSupport::Concern

  included do
    # Some magic here...
    scope :last_week, -> { unscoped.where('created_at >= ?', 1.week.ago) }
  end
end

class Host
  include Statistics
  include Calculable
end

# last_week accessible via statistics namespace
Host.statistics.last_week #=> all the hosts created last week
# last_week not available directly on a host class
Host.last_week #=> This will raise NoMethodError: undefined method last_week
# last_week should be also chainable with other scopes
Host.enabled.statistics.last_week #=> all enabled hosts from last week
# And the last_week method but from the calculable namespace
Host.calculable.last_week

Proxy object and instance_exec as a solution

To obtain such a behavior we can use a proxy object in our concern module:

module Statisticable
  extend ActiveSupport::Concern

  # Class that acts as a simple proxy for all statistical scopes
  # We have it, because we don't want to "pollute" original klass
  # that we extend with this. This way we can define scopes here
  # and then access them via .statistics class method of a class
  class Proxy
    # @param [Symbol] name of a scope or a method
    # @param [Block] block with a scope that will be binded to the class
    def self.scope(name, block)
      define_method(name) do |*args|
        @scope.instance_exec(*args, &block)
      end
    end

    # @param scope [ActiveRecord::Base] active record class for which we want to
    #   create statistics proxy
    def initialize(scope)
      @scope = scope
      # Here we could also use any scope that we want, like:
      # @scope = scope.unscoped
    end

    %i( week month year ).each do |time|
      # @return [Integer] number of elements that were created in a certain time
      scope :"last_#{time}", -> { where('created_at >= ?', 1.send(time).ago) }
    end

    # We can add any standard method as well - but it can't be chained further
    scope :summed, -> { sum(:id) }
  end

  included do
    # @return [Statisticable::Proxy] proxy that provides us with scopes for statistical info
    #   about a given class
    # @example Get statistics proxy for Like class
    #   Like.statistics #=> Statisticable::Proxy instance
    def self.statistics
      @statistics ||= Statisticable::Proxy.new(self)
    end
  end
end

It uses the instance_exec method to execute all the scopes in our base class (@scope) context.

Now in every AR in which we include this Statisticable concern, all its scopes and methods will be available via statistics namespace.

Copyright © 2025 Closer to Code

Theme by Anders NorenUp ↑