Patch for #1812 -- allow default_scope to take a proc

Can I get someone to review this very small patch (only 5 lines, plus
tests of course) to allow default_scope to accept a proc?

It applies to 3-0-stable and to master, and passes tests on both
branches.

-Tim

Sorry I forgot to link to the ticket:
https://rails.lighthouseapp.com/projects/8994/tickets/1812-default_scope-cant-take-procs

And the patch is here:
https://rails.lighthouseapp.com/projects/8994/tickets/1812/a/721344/allow-default_scope-to-accept-a-proc.patch

Or you can pull from my Rails fork here: http://github.com/seven1m/rails

-Tim

Two things:

1. Can you change it so that it uses "&&" rather than "and"

2. Change from m.is_a?(Proc) to m.respond_to?(:call)

#1 is for style. #2 is because we don't care that this is a Proc
object, we only care that it responds to "call".

Sure!

Here is the updated patch: https://rails.lighthouseapp.com/projects/8994/tickets/1812/a/727956/allow-default_scope-to-accept-a-proc2.patch

Also, the updated commit on GitHub:
http://github.com/seven1m/rails/commit/e0b73b64c30e81e5b4279ce55dd2b7d65cfae00a

-Tim

Thanks! I've applied it and extended it a little bit so that
default_scope will take any object that responds to "call".

I'll show some code, then try to explain the benefits. This is now possible:

  class AuthorFilter < Struct.new(:klass, :author_id)
    def call
      klass.where(:author_id => author_id)
    end
  end

  class AmazingPost < ActiveRecord::Base
    self.table_name = 'posts'
    default_scope AuthorFilter.new(self, 2)
  end

The Object form does not need to save binding like the lambda form does. This
can lead to less memory usage and help curb "leaking objects".

Because we can use objects, it allows our default scopes to contain more
logic: we can actually break up functionality to other methods. For
example:

  class AuthorFilter < Struct.new(:klass, :some_value)
    def calculate_id
      ... do som complex logic to figure out the id ...
    end

    def call
      klass.where(:author_id => calculate_id)
    end
  end

We can even use modules or subclasses to extend our logic. For example:

  module BodyFilter
    def call
      super.where(:body => 'hello')
    end
  end

  class AuthorFilter < Struct.new(:klass, :author_id)
    def call
      klass.where(:author_id => author_id)
    end
  end

  class AmazingPost < ActiveRecord::Base
    self.table_name = 'posts'
    # Produces: WHERE author_id = 2 AND body = 'hello'
    default_scope AuthorFilter.new(self, 2).extend(BodyFilter)
  end

Finally, we can more easily test any complex logic we need in this
filter:

  class FilterTest < Test::Unit::TestCase
    class DreamCatcher
      attr_reader :wheres
      def initialize; @wheres = []; end
      def where(arg); @wheres << arg; end
    end

    def test_appropriate_where_clause
      dc = DreamCatcher.new
      AuthorFilter.new(dc, 2).call
      assert_equal([{:author_id => 2}], dc.wheres)
    end
  end

I'd like to see the rest of our scope methods to allow arbitrary objects
that respond to `call` for the above reasons. :slight_smile:

This is great, and I wouldn’t have seen the possibilities if you hadn’t explained it in code!

Thanks!

-Tim

No problem. I've updated the `scope` method to accept objects that
respond to `call` as well:

  http://github.com/rails/rails/blob/master/activerecord/test/models/topic.rb#L22-26
  http://github.com/rails/rails/blob/master/activerecord/test/cases/named_scope_test.rb#L128-132

Now we can more easily reuse our scope code!

Wheeeeeeeeee :smiley:

// sorry for the spammy message, but really, this is fantastic in many levels :smiley: