ruby-on-railsactiverecordactivesupport-concern

Rails ActiveSupport::Concern — scope can't call private class method


I'm trying to refactor a scope within a Concern so that the scope gets most of its logic by calling different class methods, depending on an arg.

With "public" class methods, that works fine.

But I want those class methods to be private, so they are not mistaken for scopes and called by other developers. (because this will lead to unexpected results in the app.)

I tried a suggestion here and every other way I could find to declare these class methods private, but when I do that, they are inaccessible from within the scope.

module OrderingScopes
  extend ActiveSupport::Concern

  included do
    # This scope can handle methods like :method and :reverse_method
    scope :order_by, lambda { |method|
      method = method.dup.to_s
      reverse = method.sub!(/^reverse_/, "")
      scope = :"order_by_#{method}";
      # This check fails
      return all unless private_methods(false).include?(scope)

      scope = send(scope)
      scope = scope.reverse_order if reverse
      # Disambiguate grouped result order by adding an order by :id
      scope = scope.order(arel_table[:id].desc)
      scope
    }
 
    private_class_method :order_by_method_name
  end

  class_methods do
    def order_by_method_name
    end
  end
end

Also tried:

  class_methods do
    private

    def order_by_method_name
    end
  end

In both cases the scope fails:

> Model.order_by(:method_name)

  included do
    scope :order_by, lambda { |method|
      method = method.dup.to_s
      reverse = method.sub!(/^reverse_/, "")
      scope = :"order_by_#{method}";
      debugger

> private_methods(false).include?(:order_by_method_name)
false
> respond_to?(:order_by_method_name)
false

Is there a way to do this?


Solution

  • Rails automatically defines scope methods on a relation object and delegates to your model's class method, but only if method is public.

    You can work around this by copying how rails defines the delegate method:

    scope :order_by, lambda { |method|
      scoping { model.send(:"order_by_#{method}") }
    }
    

    https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-scoping

    Note, that within a scope you're working with an ActiveRecord::Relation object. You can use model attribute reader to get your model class. Since private class methods are not copied into the relation class you have to run your check on the model itself:

    # private_methods(false).include?(scope)
    model.private_methods(false).include?(scope)