3

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.

pixelearth
  • 13,674
  • 10
  • 62
  • 110

2 Answers2

1

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)>
mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • I had/have hopes for this approach, but it didn't quite work for me. See updated edits. I also added some details for what I'm trying to do. – pixelearth Apr 25 '21 at 21:26
  • If this is a one-off then you could override monkey patch `scope` with your own that uses [your trick over here](https://stackoverflow.com/a/67250155/479863) to get the block source and then punts to the standard `scope` method. – mu is too short Apr 25 '21 at 21:43
  • Interesting idea. I'll have to consider whether the added complexity is worth the benefit. It's a big app. It's not a one-off. The Transaction class alone has 59 scopes. My strong preference is for something that doesn't affect the code at all. If I understood the body that is being returned (the one I don't recognize) I might be able to find the lambda in the bowels of the rails objects. Just not sure where to look. – pixelearth Apr 25 '21 at 21:52
0

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)