Skip to content

Commit

Permalink
extract deprecated dynamic methods
Browse files Browse the repository at this point in the history
  • Loading branch information
jonleighton committed May 4, 2012
1 parent ffad360 commit 510cf0a
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 142 deletions.
178 changes: 38 additions & 140 deletions activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,21 +1,17 @@
module ActiveRecord
module DynamicMatchers
module DynamicMatchers #:nodoc:
# This code in this file seems to have a lot of indirection, but the indirection
# is there to provide extension points for the active_record_deprecated_finders
# gem. When we stop supporting active_record_deprecated_finders (from Rails 5),
# then we can remove the indirection.

def respond_to?(name, include_private = false)
match = Method.match(self, name)
match && match.valid? || super
end

private

# Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
# <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
# section at the top of this file for more detailed information.
#
# It's even possible to use all the additional parameters to +find+. For example, the
# full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
#
# Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
# is first invoked, so that future attempts to use it do not run through method_missing.
def method_missing(name, *arguments, &block)
match = Method.match(self, name)

Expand All @@ -28,28 +24,27 @@ def method_missing(name, *arguments, &block)
end

class Method
def self.match(model, name)
klass = klasses.find { |k| name =~ k.pattern }
klass.new(model, name) if klass
end
@matchers = []

def self.klasses
[
FindBy, FindAllBy, FindLastBy, FindByBang, ScopedBy,
FindOrInitializeBy, FindOrCreateBy, FindOrCreateByBang
]
end
class << self
attr_reader :matchers

def self.pattern
/^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
end
def match(model, name)
klass = matchers.find { |k| name =~ k.pattern }
klass.new(model, name) if klass
end

def self.prefix
raise NotImplementedError
end
def pattern
/^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
end

def self.suffix
''
def prefix
raise NotImplementedError
end

def suffix
''
end
end

attr_reader :model, :name, :attribute_names
Expand Down Expand Up @@ -77,20 +72,20 @@ def body
end
end

class Finder < Method
module Finder
# Extended in active_record_deprecated_finders
def body
<<-CODE
result = #{result}
result && block_given? ? yield(result) : result
CODE
result
end

# Extended in active_record_deprecated_finders
def result
"scoped.apply_finder_options(options).#{finder}(#{attributes_hash})"
"#{finder}(#{attributes_hash})"
end

# Extended in active_record_deprecated_finders
def signature
attribute_names.join(', ') + ", options = {}"
attribute_names.join(', ')
end

def attributes_hash
Expand All @@ -102,7 +97,10 @@ def finder
end
end

class FindBy < Finder
class FindBy < Method
Method.matchers << self
include Finder

def self.prefix
"find_by"
end
Expand All @@ -112,7 +110,10 @@ def finder
end
end

class FindByBang < Finder
class FindByBang < Method
Method.matchers << self
include Finder

def self.prefix
"find_by"
end
Expand All @@ -125,108 +126,5 @@ def finder
"find_by!"
end
end

class FindAllBy < Finder
def self.prefix
"find_all_by"
end

def finder
"where"
end

def result
"#{super}.to_a"
end
end

class FindLastBy < Finder
def self.prefix
"find_last_by"
end

def finder
"where"
end

def result
"#{super}.last"
end
end

class ScopedBy < Finder
def self.prefix
"scoped_by"
end

def body
"where(#{attributes_hash})"
end
end

class Instantiator < Method
# This is nasty, but it doesn't matter because it will be deprecated.
def self.dispatch(klass, attribute_names, instantiator, args, block)
if args.length == 1 && args.first.is_a?(Hash)
attributes = args.first.stringify_keys
conditions = attributes.slice(*attribute_names)
rest = [attributes.except(*attribute_names)]
else
raise ArgumentError, "too few arguments" unless args.length >= attribute_names.length

conditions = Hash[attribute_names.map.with_index { |n, i| [n, args[i]] }]
rest = args.drop(attribute_names.length)
end

klass.where(conditions).first ||
klass.create_with(conditions).send(instantiator, *rest, &block)
end

def signature
"*args, &block"
end

def body
"#{self.class}.dispatch(self, #{attribute_names.inspect}, #{instantiator.inspect}, args, block)"
end

def instantiator
raise NotImplementedError
end
end

class FindOrInitializeBy < Instantiator
def self.prefix
"find_or_initialize_by"
end

def instantiator
"new"
end
end

class FindOrCreateBy < Instantiator
def self.prefix
"find_or_create_by"
end

def instantiator
"create"
end
end

class FindOrCreateByBang < Instantiator
def self.prefix
"find_or_create_by"
end

def self.suffix
"!"
end

def instantiator
"create!"
end
end
end
end
4 changes: 2 additions & 2 deletions activerecord/test/cases/named_scope_test.rb
Expand Up @@ -442,11 +442,11 @@ def test_eager_default_scope_relations_are_deprecated

class DynamicScopeMatchTest < ActiveRecord::TestCase
def test_scoped_by_no_match
assert_nil ActiveRecord::DynamicMatchers::ScopedBy.match(nil, "not_scoped_at_all")
assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all")
end

def test_scoped_by
match = ActiveRecord::DynamicMatchers::ScopedBy.match(nil, "scoped_by_age_and_sex_and_location")
match = ActiveRecord::DynamicMatchers::Method.match(nil, "scoped_by_age_and_sex_and_location")
assert_not_nil match
assert_equal %w(age sex location), match.attribute_names
end
Expand Down

0 comments on commit 510cf0a

Please sign in to comment.