How can I access a Rails scope lambda without calling it?

Viewed 190

I would like to access the lamda defined in a rails scope as the lambda itself and assign it to a variable. Is this possible?

So if I have the following scope

scope :positive_amount, -> { where("amount > 0") }

I would like to be able to put this lambda into a variable, like "normal" lambda assignment:

positive_amount = -> { where("amount > 0") }

So something like this:

positive_amount = MyClass.get_scope_lambda(:positive_amount)

For clarification, I'm wanting the body of the method that I generally access with method_source gem via MyClass.instance_method(method).source.display. I'm wanting this for on-the-fly documentation of calculations that are taking place in our system.

Our invoicing calculations are combinations of smaller method and scopes. I'm trying to make a report that says how the calculations were reached, that uses the actual code. I've had luck with instance methods, but I'd like to show the scopes too:

enter image description here

Edit 1:

Following @mu's suggestion below, I tried:

Transaction.method(:positive_amount).source.display

But this returns:

singleton_class.send(:define_method, name) do |*args|
  scope = all
  scope = scope._exec_scope(*args, &body)
  scope = scope.extending(extension) if extension
  scope
end

And not the body of the method as I'd expect.

2 Answers

If you say:

class MyClass < ApplicationRecord
  scope :positive_amount, -> { where("amount > 0") }
end

then you're really adding a class method called positive_amount to MyClass. So if you want to access the scope, you can use the method method:

positive_amount = MyClass.method(:positive_amount)
#<Method: MyClass(...)

That will give you a Method instance but you can get a proc if you really need one:

positive_amount = MyClass.method(:positive_amount).to_proc
#<Proc:0x... (lambda)>

If I get your idea right. Here is one approach to do this

class SampleModel < ApplicationRecord
  class << self
    @@active = ->(klass) { klass.where(active: true) }
    @@by_names = ->(klass, name) { klass.where("name LIKE ?", "%#{name}%") }

    def get_scope_lambda(method_name, *args)
      method = class_variable_get("@@#{method_name}")

      return method.call(self, *args) if args
      method.call(self)
    end
  end
end

So after that you can access the scopes like this:

SampleModel.get_scope_lambda(:by_names, "harefx")
SampleModel.get_scope_lambda(:active)

Or you can define some more class methods above, the one extra klass argument might be not ideal. But I don't find a way to access the self from inside the lambda block yet, so this is my best shot now.

By the way, I don't think this is a good way to use scope. But I just express your idea and to point it out that it's possible :D


UPDATED:

Here I come with another approach, I think it could solve your problem :D

class SampleModel < ApplicationRecord
  scope :active, -> { where(active: true) }

  scope :more_complex, -> {
    where(active: true)
    .where("name LIKE ?", "%#{name}%")
  }

  class << self
    def get_scope_lambda(method_name)
      location, _ = self.method(:get_scope_lambda).source_location
      content = File.read(location)
      regex = /scope\s:#{method_name}, -> {[\\n\s\w\(\):\.\\",?%\#{}]+}/
      content.match(regex).to_s.display
    end
  end
end

So now you can try this to get the source

SampleModel.get_scope_lambda(:active)
SampleModel.get_scope_lambda(:more_complex)
Related