diff options
author | Jon Leighton <j@jonathanleighton.com> | 2012-05-02 23:57:52 +0100 |
---|---|---|
committer | Jon Leighton <j@jonathanleighton.com> | 2012-05-04 12:50:03 +0100 |
commit | 0c76a52c472546083a199f685f96170031b36fdd (patch) | |
tree | 6a2ef0077eb6d2904a420456db0025fc7639327c /activerecord | |
parent | 5f62c86b50f21ef14ffda1112a8cd002e87590ca (diff) | |
download | rails-0c76a52c472546083a199f685f96170031b36fdd.tar.gz rails-0c76a52c472546083a199f685f96170031b36fdd.tar.bz2 rails-0c76a52c472546083a199f685f96170031b36fdd.zip |
clean up implementation of dynamic methods. use method compilation etc.
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/lib/active_record.rb | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/collection_proxy.rb | 6 | ||||
-rw-r--r-- | activerecord/lib/active_record/dynamic_finder_match.rb | 108 | ||||
-rw-r--r-- | activerecord/lib/active_record/dynamic_matchers.rb | 228 | ||||
-rw-r--r-- | activerecord/lib/active_record/dynamic_scope_match.rb | 30 | ||||
-rw-r--r-- | activerecord/lib/active_record/relation/finder_methods.rb | 41 | ||||
-rw-r--r-- | activerecord/test/cases/dynamic_finder_match_test.rb | 106 | ||||
-rw-r--r-- | activerecord/test/cases/named_scope_test.rb | 5 |
8 files changed, 204 insertions, 322 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index c4b10a8dae..ed26b4899f 100644 --- a/activerecord/lib/active_record.rb +++ b/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 diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 50d16b16a9..8bbab75de6 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -79,9 +79,9 @@ module ActiveRecord 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? diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb deleted file mode 100644 index 0473d6aafc..0000000000 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ /dev/null @@ -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 diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e35b1c91a0..01efaee8fc 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/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 @@ module ActiveRecord # # 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+. diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb deleted file mode 100644 index 6c043d29c4..0000000000 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ /dev/null @@ -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 diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3c9c9c4e84..a78e3d08e4 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -243,47 +243,6 @@ module ActiveRecord 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? diff --git a/activerecord/test/cases/dynamic_finder_match_test.rb b/activerecord/test/cases/dynamic_finder_match_test.rb deleted file mode 100644 index db619faa83..0000000000 --- a/activerecord/test/cases/dynamic_finder_match_test.rb +++ /dev/null @@ -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 diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index f8557259fb..479b375360 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -442,13 +442,12 @@ end 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 |