Skip to content

Commit

Permalink
refactor dynamic finder name matching into its own class
Browse files Browse the repository at this point in the history
Signed-off-by: Jeremy Kemper <jeremy@bitsweat.net>
  • Loading branch information
joshsusser authored and jeremy committed Aug 26, 2008
1 parent 3beed9c commit 143f5fb
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 72 deletions.
1 change: 1 addition & 0 deletions activerecord/lib/active_record.rb
Expand Up @@ -51,6 +51,7 @@
require 'active_record/serialization'
require 'active_record/attribute_methods'
require 'active_record/dirty'
require 'active_record/dynamic_finder_match'

ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
Expand Down
121 changes: 49 additions & 72 deletions activerecord/lib/active_record/base.rb
Expand Up @@ -1354,8 +1354,8 @@ def abstract_class?
end

def respond_to?(method_id, include_private = false)
if match = matches_dynamic_finder?(method_id) || matches_dynamic_finder_with_initialize_or_create?(method_id)
return true if all_attributes_exists?(extract_attribute_names_from_match(match))
if match = DynamicFinderMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
end
super
end
Expand Down Expand Up @@ -1674,88 +1674,65 @@ def undecorated_table_name(class_name = base_class.name)
# Each dynamic finder or initializer/creator 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)
if match = matches_dynamic_finder?(method_id)
finder = determine_finder(match)

attribute_names = extract_attribute_names_from_match(match)
if match = DynamicFinderMatch.match(method_id)
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)

self.class_eval %{
def self.#{method_id}(*args)
options = args.extract_options!
attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
finder_options = { :conditions => attributes }
validate_find_options(options)
set_readonly_option!(options)
if options[:conditions]
with_scope(:find => finder_options) do
ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
if match.finder?
finder = match.finder
self.class_eval %{
def self.#{method_id}(*args)
options = args.extract_options!
attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
finder_options = { :conditions => attributes }
validate_find_options(options)
set_readonly_option!(options)
if options[:conditions]
with_scope(:find => finder_options) do
ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
end
else
ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
end
else
ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
end
end
}, __FILE__, __LINE__
send(method_id, *arguments)
elsif match = matches_dynamic_finder_with_initialize_or_create?(method_id)
instantiator = determine_instantiator(match)
attribute_names = extract_attribute_names_from_match(match)
super unless all_attributes_exists?(attribute_names)

self.class_eval %{
def self.#{method_id}(*args)
guard_protected_attributes = false
if args[0].is_a?(Hash)
guard_protected_attributes = true
attributes = args[0].with_indifferent_access
find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
else
find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
end
}, __FILE__, __LINE__
send(method_id, *arguments)
elsif match.instantiator?
instantiator = match.instantiator
self.class_eval %{
def self.#{method_id}(*args)
guard_protected_attributes = false
if args[0].is_a?(Hash)
guard_protected_attributes = true
attributes = args[0].with_indifferent_access
find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
else
find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
end
options = { :conditions => find_attributes }
set_readonly_option!(options)
options = { :conditions => find_attributes }
set_readonly_option!(options)
record = find_initial(options)
record = find_initial(options)
if record.nil?
record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
#{'yield(record) if block_given?'}
#{'record.save' if instantiator == :create}
record
else
record
if record.nil?
record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
#{'yield(record) if block_given?'}
#{'record.save' if instantiator == :create}
record
else
record
end
end
end
}, __FILE__, __LINE__
send(method_id, *arguments)
}, __FILE__, __LINE__
send(method_id, *arguments)
end
else
super
end
end

def matches_dynamic_finder?(method_id)
/^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
end

def matches_dynamic_finder_with_initialize_or_create?(method_id)
/^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
end

def determine_finder(match)
match.captures.first == 'all_by' ? :find_every : :find_initial
end

def determine_instantiator(match)
match.captures.first == 'initialize' ? :new : :create
end

def extract_attribute_names_from_match(match)
match.captures.last.split('_and_')
end

def construct_attributes_from_arguments(attribute_names, arguments)
attributes = {}
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
Expand Down
33 changes: 33 additions & 0 deletions activerecord/lib/active_record/dynamic_finder_match.rb
@@ -0,0 +1,33 @@
module ActiveRecord
class DynamicFinderMatch
def self.match(method)
df_match = self.new(method)
df_match.finder ? df_match : nil
end

def initialize(method)
@finder = :find_initial
case method.to_s
when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
@finder = :find_every if $1 == 'all_by'
names = $2
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
@instantiator = $1 == 'initialize' ? :new : :create
names = $2
else
@finder = nil
end
@attribute_names = names && names.split('_and_')
end

attr_reader :finder, :attribute_names, :instantiator

def finder?
!@finder.nil? && @instantiator.nil?
end

def instantiator?
@finder == :find_initial && !@instantiator.nil?
end
end
end
42 changes: 42 additions & 0 deletions activerecord/test/cases/finder_test.rb
Expand Up @@ -12,6 +12,48 @@
require 'models/job'
require 'models/categorization'

class DynamicFinderMatchTest < ActiveRecord::TestCase
def test_find_no_match
assert_nil ActiveRecord::DynamicFinderMatch.match("not_a_finder")
end

def test_find_by
match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location")
assert_not_nil match
assert match.finder?
assert_equal :find_initial, match.finder
assert_equal %w(age sex location), match.attribute_names
end

def test_find_all_by
match = ActiveRecord::DynamicFinderMatch.match("find_all_by_age_and_sex_and_location")
assert_not_nil match
assert match.finder?
assert_equal :find_every, match.finder
assert_equal %w(age sex location), match.attribute_names
end

def test_find_or_initialize_by
match = ActiveRecord::DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location")
assert_not_nil match
assert !match.finder?
assert match.instantiator?
assert_equal :find_initial, match.finder
assert_equal :new, match.instantiator
assert_equal %w(age sex location), match.attribute_names
end

def test_find_or_create_by
match = ActiveRecord::DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location")
assert_not_nil match
assert !match.finder?
assert match.instantiator?
assert_equal :find_initial, match.finder
assert_equal :create, match.instantiator
assert_equal %w(age sex location), match.attribute_names
end
end

class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers

Expand Down

0 comments on commit 143f5fb

Please sign in to comment.