Browse files

clean up implementation of dynamic methods. use method compilation etc.

  • Loading branch information...
1 parent 5f62c86 commit 0c76a52c472546083a199f685f96170031b36fdd @jonleighton jonleighton committed May 2, 2012
View
2 activerecord/lib/active_record.rb
@@ -63,8 +63,6 @@ module ActiveRecord
autoload :CounterCache
autoload :ConnectionHandling
autoload :DynamicMatchers
- autoload :DynamicFinderMatch
- autoload :DynamicScopeMatch
autoload :Explain
autoload :Inheritance
autoload :Integration
View
6 activerecord/lib/active_record/associations/collection_proxy.rb
@@ -79,9 +79,9 @@ def respond_to?(name, include_private = false)
end
def method_missing(method, *args, &block)
- match = DynamicFinderMatch.match(method)
- if match && match.instantiator?
- scoped.send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
+ match = DynamicMatchers::Method.match(method)
+ if match && match.is_a?(DynamicMatchers::Instantiator)
+ scoped.send(method, *args) do |r|
proxy_association.send :set_owner_attributes, r
proxy_association.send :add_to_target, r
yield(r) if block_given?
View
108 activerecord/lib/active_record/dynamic_finder_match.rb
@@ -1,108 +0,0 @@
-module ActiveRecord
-
- # = Active Record Dynamic Finder Match
- #
- # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info
- #
- class DynamicFinderMatch
- def self.match(method)
- method = method.to_s
- klass = klasses.find do |_klass|
- _klass.matches?(method)
- end
- klass.new(method) if klass
- end
-
- def self.matches?(method)
- method =~ self::METHOD_PATTERN
- end
-
- def self.klasses
- [FindBy, FindByBang, FindOrInitializeCreateBy, FindOrCreateByBang]
- end
-
- def initialize(method)
- @finder = :first
- @instantiator = nil
- match_data = method.match(self.class::METHOD_PATTERN)
- @attribute_names = match_data[-1].split("_and_")
- initialize_from_match_data(match_data)
- end
-
- attr_reader :finder, :attribute_names, :instantiator
-
- def finder?
- @finder && !@instantiator
- end
-
- def creator?
- @finder == :first && @instantiator == :create
- end
-
- def instantiator?
- @instantiator
- end
-
- def bang?
- false
- end
-
- def valid_arguments?(arguments)
- arguments.size >= @attribute_names.size
- end
-
- def save_record?
- @instantiator == :create
- end
-
- def save_method
- bang? ? :save! : :save
- end
-
- private
-
- def initialize_from_match_data(match_data)
- end
- end
-
- class FindBy < DynamicFinderMatch
- METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/
-
- def initialize_from_match_data(match_data)
- @finder = :last if match_data[1] == 'last_'
- @finder = :all if match_data[1] == 'all_'
- end
- end
-
- class FindByBang < DynamicFinderMatch
- METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/
-
- def bang?
- true
- end
- end
-
- class FindOrInitializeCreateBy < DynamicFinderMatch
- METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
-
- def initialize_from_match_data(match_data)
- @instantiator = match_data[1] == 'initialize' ? :new : :create
- end
-
- def valid_arguments?(arguments)
- arguments.size == 1 && arguments.first.is_a?(Hash) || super
- end
- end
-
- class FindOrCreateByBang < DynamicFinderMatch
- METHOD_PATTERN = /^find_or_create_by_([_a-zA-Z]\w*)\!$/
-
- def initialize_from_match_data(match_data)
- @instantiator = :create
- end
-
- def bang?
- true
- end
- end
-end
View
228 activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module DynamicMatchers
- def respond_to?(method_id, include_private = false)
- match = find_dynamic_match(method_id)
+ def respond_to?(name, include_private = false)
+ match = Method.match(name)
valid_match = match && all_attributes_exists?(match.attribute_names)
valid_match || super
@@ -18,43 +18,213 @@ def respond_to?(method_id, include_private = false)
#
# 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(method_id, *arguments, &block)
- if match = find_dynamic_match(method_id)
+ def method_missing(name, *arguments, &block)
+ if match = Method.match(name)
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
- unless match.valid_arguments?(arguments)
- method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
- backtrace = [method_trace] + caller
- raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
- end
-
- if match.respond_to?(:scope?) && match.scope?
- define_scope_method(method_id, attribute_names)
- send(method_id, *arguments)
- elsif match.finder?
- options = arguments.extract_options!
- relation = options.any? ? scoped(options) : scoped
- relation.send :find_by_attributes, match, attribute_names, *arguments, &block
- elsif match.instantiator?
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
- end
+ match.define(self)
+ send(name, *arguments, &block)
else
super
end
end
- def define_scope_method(method_id, attribute_names) #:nodoc
- self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
- def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
- conditions = Hash[[:#{attribute_names.join(',:')}].zip(args)] # conditions = Hash[[:user_name, :password].zip(args)]
- where(conditions) # where(conditions)
- end # end
- METHOD
+ class Method
+ def self.match(name)
+ klass = klasses.find { |k| name =~ k.pattern }
+ klass.new(name) if klass
+ end
+
+ def self.klasses
+ [
+ FindBy, FindAllBy, FindLastBy, FindByBang, ScopedBy,
+ FindOrInitializeBy, FindOrCreateBy, FindOrCreateByBang
+ ]
+ end
+
+ def self.pattern
+ /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ end
+
+ def self.prefix
+ raise NotImplementedError
+ end
+
+ def self.suffix
+ ''
+ end
+
+ attr_reader :name, :attribute_names
+
+ def initialize(name)
+ @name = name.to_s
+ @attribute_names = @name.match(self.class.pattern)[1].split('_and_')
+ end
+
+ def define(klass)
+ klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def self.#{name}(#{signature})
+ #{body}
+ end
+ CODE
+ end
+
+ def body
+ raise NotImplementedError
+ end
+ end
+
+ class Finder < Method
+ def body
+ <<-CODE
+ result = #{result}
+ result && block_given? ? yield(result) : result
+ CODE
+ end
+
+ def result
+ "scoped.apply_finder_options(options).#{finder}(#{attributes_hash})"
+ end
+
+ def signature
+ attribute_names.join(', ') + ", options = {}"
+ end
+
+ def attributes_hash
+ "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ end
+
+ def finder
+ raise NotImplementedError
+ end
+ end
+
+ class FindBy < Finder
+ def self.prefix
+ "find_by"
+ end
+
+ def finder
+ "find_by"
+ end
+ end
+
+ class FindByBang < Finder
+ def self.prefix
+ "find_by"
+ end
+
+ def self.suffix
+ "!"
+ end
+
+ 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
- def find_dynamic_match(method_id) #:nodoc:
- DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)
+ 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
# Similar in purpose to +expand_hash_conditions_for_aggregates+.
View
30 activerecord/lib/active_record/dynamic_scope_match.rb
@@ -1,30 +0,0 @@
-module ActiveRecord
-
- # = Active Record Dynamic Scope Match
- #
- # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt>
- # if, for example, the <tt>Product</tt> has an attribute with that name. You can
- # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named
- # scope except that it's dynamic.
- class DynamicScopeMatch
- METHOD_PATTERN = /^scoped_by_([_a-zA-Z]\w*)$/
-
- def self.match(method)
- if method.to_s =~ METHOD_PATTERN
- new(true, $1 && $1.split('_and_'))
- end
- end
-
- def initialize(scope, attribute_names)
- @scope = scope
- @attribute_names = attribute_names
- end
-
- attr_reader :scope, :attribute_names
- alias :scope? :scope
-
- def valid_arguments?(arguments)
- arguments.size >= @attribute_names.size
- end
- end
-end
View
41 activerecord/lib/active_record/relation/finder_methods.rb
@@ -243,47 +243,6 @@ def construct_limited_ids_condition(relation)
ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
end
- def find_by_attributes(match, attributes, *args)
- conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
- result = where(conditions).send(match.finder)
-
- if match.bang? && result.blank?
- raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
- else
- if block_given? && result
- yield(result)
- else
- result
- end
- end
- end
-
- def find_or_instantiator_by_attributes(match, attributes, *args)
- options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {}
- protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
- args.each_with_index do |arg, i|
- if arg.is_a?(Hash)
- protected_attributes_for_create = args[i].with_indifferent_access
- else
- unprotected_attributes_for_create[attributes[i]] = args[i]
- end
- end
-
- conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys
-
- record = where(conditions).first
-
- unless record
- record = @klass.new(protected_attributes_for_create, options) do |r|
- r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
- end
- yield(record) if block_given?
- record.send(match.save_method) if match.save_record?
- end
-
- record
- end
-
def find_with_ids(*ids)
return to_a.find { |*block_args| yield(*block_args) } if block_given?
View
106 activerecord/test/cases/dynamic_finder_match_test.rb
@@ -1,106 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- class DynamicFinderMatchTest < ActiveRecord::TestCase
- def test_find_or_create_by
- match = DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location")
- assert_not_nil match
- assert !match.finder?
- assert match.instantiator?
- assert_equal :first, match.finder
- assert_equal :create, match.instantiator
- assert_equal %w(age sex location), match.attribute_names
- end
-
- def test_find_or_initialize_by
- match = DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location")
- assert_not_nil match
- assert !match.finder?
- assert match.instantiator?
- assert_equal :first, match.finder
- assert_equal :new, match.instantiator
- assert_equal %w(age sex location), match.attribute_names
- end
-
- def test_find_no_match
- assert_nil DynamicFinderMatch.match("not_a_finder")
- end
-
- def find_by_bang
- match = DynamicFinderMatch.match("find_by_age_and_sex_and_location!")
- assert_not_nil match
- assert match.finder?
- assert match.bang?
- assert_equal :first, match.finder
- assert_equal %w(age sex location), match.attribute_names
- end
-
- def test_find_by
- match = DynamicFinderMatch.match("find_by_age_and_sex_and_location")
- assert_not_nil match
- assert match.finder?
- assert_equal :first, match.finder
- assert_equal %w(age sex location), match.attribute_names
- end
-
- def test_find_by_with_symbol
- m = DynamicFinderMatch.match(:find_by_foo)
- assert_equal :first, m.finder
- assert_equal %w{ foo }, m.attribute_names
- end
-
- def test_find_all_by_with_symbol
- m = DynamicFinderMatch.match(:find_all_by_foo)
- assert_equal :all, m.finder
- assert_equal %w{ foo }, m.attribute_names
- end
-
- def test_find_all_by
- match = DynamicFinderMatch.match("find_all_by_age_and_sex_and_location")
- assert_not_nil match
- assert match.finder?
- assert_equal :all, match.finder
- assert_equal %w(age sex location), match.attribute_names
- end
-
- def test_find_last_by
- m = DynamicFinderMatch.match(:find_last_by_foo)
- assert_equal :last, m.finder
- assert_equal %w{ foo }, m.attribute_names
- end
-
- def test_find_by!
- m = DynamicFinderMatch.match(:find_by_foo!)
- assert_equal :first, m.finder
- assert m.bang?, 'should be banging'
- assert_equal %w{ foo }, m.attribute_names
- end
-
- def test_find_or_create
- m = DynamicFinderMatch.match(:find_or_create_by_foo)
- assert_equal :first, m.finder
- assert_equal %w{ foo }, m.attribute_names
- assert_equal :create, m.instantiator
- end
-
- def test_find_or_create!
- m = DynamicFinderMatch.match(:find_or_create_by_foo!)
- assert_equal :first, m.finder
- assert m.bang?, 'should be banging'
- assert_equal %w{ foo }, m.attribute_names
- assert_equal :create, m.instantiator
- end
-
- def test_find_or_initialize
- m = DynamicFinderMatch.match(:find_or_initialize_by_foo)
- assert_equal :first, m.finder
- assert_equal %w{ foo }, m.attribute_names
- assert_equal :new, m.instantiator
- end
-
- def test_garbage
- assert !DynamicFinderMatch.match(:fooo), 'should be false'
- assert !DynamicFinderMatch.match(:find_by), 'should be false'
- end
- end
-end
View
5 activerecord/test/cases/named_scope_test.rb
@@ -442,13 +442,12 @@ def test_eager_default_scope_relations_are_deprecated
class DynamicScopeMatchTest < ActiveRecord::TestCase
def test_scoped_by_no_match
- assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all")
+ assert_nil ActiveRecord::DynamicMatchers::ScopedBy.match("not_scoped_at_all")
end
def test_scoped_by
- match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location")
+ match = ActiveRecord::DynamicMatchers::ScopedBy.match("scoped_by_age_and_sex_and_location")
assert_not_nil match
- assert match.scope?
assert_equal %w(age sex location), match.attribute_names
end
end

0 comments on commit 0c76a52

Please sign in to comment.