From eee32af45e7909c02370324833d7117fe9b6eb37 Mon Sep 17 00:00:00 2001
From: Andrew White <andyw@pixeltrix.co.uk>
Date: Mon, 12 Mar 2012 11:39:19 +0000
Subject: Add dynamic find_or_create_by_{attribute}! method.

---
 activerecord/CHANGELOG.md                          |  2 ++
 .../lib/active_record/dynamic_finder_match.rb      | 26 +++++++++++++++++++++-
 .../lib/active_record/relation/finder_methods.rb   |  2 +-
 .../test/cases/dynamic_finder_match_test.rb        |  8 +++++++
 activerecord/test/cases/finder_respond_to_test.rb  | 10 +++++++++
 activerecord/test/cases/finder_test.rb             | 22 ++++++++++++++++++
 activerecord/test/cases/relations_test.rb          | 12 ++++++++++
 7 files changed, 80 insertions(+), 2 deletions(-)

diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 9a036bdd66..6fd86b1f32 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -155,6 +155,8 @@
 
 ## Rails 3.2.3 (unreleased) ##
 
+*   Added find_or_create_by_{attribute}! dynamic method. *Andrew White*
+
 *   Whitelist all attribute assignment by default. Change the default for newly generated applications to whitelist all attribute assignment.  Also update the generated model classes so users are reminded of the importance of attr_accessible. *NZKoz*
 
 *   Update ActiveRecord::AttributeMethods#attribute_present? to return false for empty strings. *Jacobkg*
diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb
index 38dbbef5fc..0473d6aafc 100644
--- a/activerecord/lib/active_record/dynamic_finder_match.rb
+++ b/activerecord/lib/active_record/dynamic_finder_match.rb
@@ -7,7 +7,7 @@ module ActiveRecord
   class DynamicFinderMatch
     def self.match(method)
       method = method.to_s
-      klass = [FindBy, FindByBang, FindOrInitializeCreateBy].find do |_klass|
+      klass = klasses.find do |_klass|
         _klass.matches?(method)
       end
       klass.new(method) if klass
@@ -17,6 +17,10 @@ module ActiveRecord
       method =~ self::METHOD_PATTERN
     end
 
+    def self.klasses
+      [FindBy, FindByBang, FindOrInitializeCreateBy, FindOrCreateByBang]
+    end
+
     def initialize(method)
       @finder = :first
       @instantiator = nil
@@ -47,6 +51,14 @@ module ActiveRecord
       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)
@@ -81,4 +93,16 @@ module ActiveRecord
       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/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 4cd703e0a5..adfacf37ee 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -290,7 +290,7 @@ module ActiveRecord
           r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
         end
         yield(record) if block_given?
-        record.save if match.instantiator == :create
+        record.send(match.save_method) if match.save_record?
       end
 
       record
diff --git a/activerecord/test/cases/dynamic_finder_match_test.rb b/activerecord/test/cases/dynamic_finder_match_test.rb
index e576870317..db619faa83 100644
--- a/activerecord/test/cases/dynamic_finder_match_test.rb
+++ b/activerecord/test/cases/dynamic_finder_match_test.rb
@@ -83,6 +83,14 @@ module ActiveRecord
       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
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 235805a67c..810c1500cc 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -56,6 +56,16 @@ class FinderRespondToTest < ActiveRecord::TestCase
     assert_respond_to Topic, :find_or_create_by_title_and_author_name
   end
 
+  def test_should_respond_to_find_or_create_from_one_attribute_bang
+    ensure_topic_method_is_not_cached(:find_or_create_by_title!)
+    assert_respond_to Topic, :find_or_create_by_title!
+  end
+
+  def test_should_respond_to_find_or_create_from_two_attributes_bang
+    ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name!)
+    assert_respond_to Topic, :find_or_create_by_title_and_author_name!
+  end
+
   def test_should_not_respond_to_find_by_one_missing_attribute
     assert !Topic.respond_to?(:find_by_undertitle)
   end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 8a7d208524..96c8eb6417 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -862,6 +862,28 @@ class FinderTest < ActiveRecord::TestCase
     assert another.persisted?
   end
 
+  def test_find_or_create_from_one_attribute_bang
+    number_of_companies = Company.count
+    assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name!("") }
+    assert_equal number_of_companies, Company.count
+    sig38 = Company.find_or_create_by_name!("38signals")
+    assert_equal number_of_companies + 1, Company.count
+    assert_equal sig38, Company.find_or_create_by_name!("38signals")
+    assert sig38.persisted?
+  end
+
+  def test_find_or_create_from_two_attributes_bang
+    number_of_companies = Company.count
+    assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name_and_firm_id!("", 17) }
+    assert_equal number_of_companies, Company.count
+    sig38 = Company.find_or_create_by_name_and_firm_id!("38signals", 17)
+    assert_equal number_of_companies + 1, Company.count
+    assert_equal sig38, Company.find_or_create_by_name_and_firm_id!("38signals", 17)
+    assert sig38.persisted?
+    assert_equal "38signals", sig38.name
+    assert_equal 17, sig38.firm_id
+  end
+
   def test_find_or_create_from_two_attributes_with_one_being_an_aggregate
     number_of_customers = Customer.count
     created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth")
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 7b1d65c6db..acec230c72 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -462,6 +462,18 @@ class RelationTest < ActiveRecord::TestCase
     assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David')
   end
 
+  def test_dynamic_find_or_create_by_attributes_bang
+    authors = Author.scoped
+
+    assert_raises(ActiveRecord::RecordInvalid) { authors.find_or_create_by_name!('') }
+
+    lifo = authors.find_or_create_by_name!('Lifo')
+    assert_equal "Lifo", lifo.name
+    assert lifo.persisted?
+
+    assert_equal authors(:david), authors.find_or_create_by_name!(:name => 'David')
+  end
+
   def test_find_id
     authors = Author.scoped
 
-- 
cgit v1.2.3