Browse files

Merge branch 'master' of git@github.com:rails/rails

  • Loading branch information...
2 parents f2f5cdc + 17d5cc1 commit 37658f15bb88e054635a496327a4a82bb50fd5d5 @wycats wycats committed Jul 19, 2009
View
4 activerecord/CHANGELOG
@@ -1,5 +1,9 @@
*Edge*
+* Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha]
+ # employees.company_name references companies.name
+ Employee.belongs_to :company, :primary_key => 'name', :foreign_key => 'company_name'
+
* Implement #many? for NamedScope and AssociationCollection using #size. #1500 [Chris Kampmeier]
* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH]
View
18 activerecord/lib/active_record/associations.rb
@@ -963,6 +963,8 @@ def has_one(association_id, options = {})
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
# will use a foreign key of "favorite_person_id".
+ # [:primary_key]
+ # Specify the method that returns the primary key of associated object used for the association. By default this is id.
# [:dependent]
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
@@ -993,6 +995,7 @@ def has_one(association_id, options = {})
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
+ # belongs_to :person, :primary_key => "name", :foreign_key => "person_name"
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
# :conditions => 'discounts > #{payments_count}'
@@ -1273,9 +1276,16 @@ def collection_reader_method(reflection, association_proxy_class)
if send(reflection.name).loaded? || reflection.options[:finder_sql]
send(reflection.name).map(&:id)
else
- send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id)
+ if reflection.through_reflection && reflection.source_reflection.belongs_to?
+ through = reflection.through_reflection
+ primary_key = reflection.source_reflection.primary_key_name
+ send(through.name).all(:select => "DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}")
+ else
+ send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map!(&:id)
+ end
end
end
+
end
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
@@ -1321,14 +1331,14 @@ def add_counter_cache_callbacks(reflection)
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
- association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
+ association.class.increment_counter(cache_column, association.id) unless association.nil?
end
after_create(method_name)
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
- association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
+ association.class.decrement_counter(cache_column, association.id) unless association.nil?
end
before_destroy(method_name)
@@ -1520,7 +1530,7 @@ def create_has_one_through_reflection(association_id, options)
mattr_accessor :valid_keys_for_belongs_to_association
@@valid_keys_for_belongs_to_association = [
- :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions,
+ :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
:validate, :touch, :inverse_of
]
View
26 activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -14,7 +14,7 @@ def replace(record)
if record.nil?
if counter_cache_name && !@owner.new_record?
- @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
+ @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name]
end
@target = @owner[@reflection.primary_key_name] = nil
@@ -27,7 +27,7 @@ def replace(record)
end
@target = (AssociationProxy === record ? record.target : record)
- @owner[@reflection.primary_key_name] = record.id unless record.new_record?
+ @owner[@reflection.primary_key_name] = record_id(record) unless record.new_record?
@updated = true
end
@@ -43,13 +43,18 @@ def updated?
private
def find_target
- the_target = @reflection.klass.find(
+ find_method = if @reflection.options[:primary_key]
+ "find_by_#{@reflection.options[:primary_key]}"
+ else
+ "find"
+ end
+ the_target = @reflection.klass.send(find_method,
@owner[@reflection.primary_key_name],
:select => @reflection.options[:select],
:conditions => conditions,
:include => @reflection.options[:include],
:readonly => @reflection.options[:readonly]
- )
+ ) if @owner[@reflection.primary_key_name]
set_inverse_instance(the_target, @owner)
the_target
end
@@ -63,6 +68,19 @@ def foreign_key_present
def we_can_set_the_inverse_on_this?(record)
@reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
end
+
+ def record_id(record)
+ record.send(@reflection.options[:primary_key] || :id)
+ end
+
+ def previous_record_id
+ @previous_record_id ||= if @reflection.options[:primary_key]
+ previous_record = @owner.send(@reflection.name)
+ previous_record.nil? ? nil : previous_record.id
+ else
+ @owner[@reflection.primary_key_name]
+ end
+ end
end
end
end
View
6 activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -7,7 +7,7 @@ def replace(record)
else
@target = (AssociationProxy === record ? record.target : record)
- @owner[@reflection.primary_key_name] = record.id
+ @owner[@reflection.primary_key_name] = record_id(record)
@owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
@updated = true
@@ -41,6 +41,10 @@ def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
end
+ def record_id(record)
+ record.send(@reflection.options[:primary_key] || :id)
+ end
+
def association_class
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
end
View
3 activerecord/lib/active_record/autosave_association.rb
@@ -339,7 +339,8 @@ def save_belongs_to_association(reflection)
association.save(!autosave) if association.new_record? || autosave
if association.updated?
- self[reflection.primary_key_name] = association.id
+ association_id = association.send(reflection.options[:primary_key] || :id)
+ self[reflection.primary_key_name] = association_id
# TODO: Removing this code doesn't seem to matter…
if reflection.options[:polymorphic]
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
View
98 activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -14,6 +14,7 @@
require 'models/comment'
require 'models/sponsor'
require 'models/member'
+require 'models/essay'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -25,6 +26,11 @@ def test_belongs_to
assert !Client.find(3).firm.nil?, "Microsoft should have a firm"
end
+ def test_belongs_to_with_primary_key
+ client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
+ assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
+ end
+
def test_proxy_assignment
account = Account.find(1)
assert_nothing_raised { account.firm = account.firm }
@@ -47,6 +53,13 @@ def test_natural_assignment
assert_equal apple.id, citibank.firm_id
end
+ def test_natural_assignment_with_primary_key
+ apple = Firm.create("name" => "Apple")
+ citibank = Client.create("name" => "Primary key client")
+ citibank.firm_with_primary_key = apple
+ assert_equal apple.name, citibank.firm_name
+ end
+
def test_no_unexpected_aliasing
first_firm = companies(:first_firm)
another_firm = companies(:another_firm)
@@ -69,13 +82,29 @@ def test_creating_the_belonging_object
assert_equal apple, citibank.firm
end
+ def test_creating_the_belonging_object_with_primary_key
+ client = Client.create(:name => "Primary key client")
+ apple = client.create_firm_with_primary_key("name" => "Apple")
+ assert_equal apple, client.firm_with_primary_key
+ client.save
+ client.reload
+ assert_equal apple, client.firm_with_primary_key
+ end
+
def test_building_the_belonging_object
citibank = Account.create("credit_limit" => 10)
apple = citibank.build_firm("name" => "Apple")
citibank.save
assert_equal apple.id, citibank.firm_id
end
+ def test_building_the_belonging_object_with_primary_key
+ client = Client.create(:name => "Primary key client")
+ apple = client.build_firm_with_primary_key("name" => "Apple")
+ client.save
+ assert_equal apple.name, client.firm_name
+ end
+
def test_natural_assignment_to_nil
client = Client.find(3)
client.firm = nil
@@ -84,6 +113,14 @@ def test_natural_assignment_to_nil
assert_nil client.client_of
end
+ def test_natural_assignment_to_nil_with_primary_key
+ client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
+ client.firm_with_primary_key = nil
+ client.save
+ assert_nil client.firm_with_primary_key(true)
+ assert_nil client.client_of
+ end
+
def test_with_different_class_name
assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
@@ -110,6 +147,17 @@ def test_belongs_to_counter
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
end
+ def test_belongs_to_with_primary_key_counter
+ debate = Topic.create("title" => "debate")
+ assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
+
+ trash = debate.replies_with_primary_key.create("title" => "blah!", "content" => "world around!")
+ assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
+
+ trash.destroy
+ assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ end
+
def test_belongs_to_counter_with_assigning_nil
p = Post.find(1)
c = Comment.find(1)
@@ -122,6 +170,18 @@ def test_belongs_to_counter_with_assigning_nil
assert_equal 1, Post.find(p.id).comments.size
end
+ def test_belongs_to_with_primary_key_counter_with_assigning_nil
+ debate = Topic.create("title" => "debate")
+ reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
+
+ assert_equal debate.title, reply.parent_title
+ assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count")
+
+ reply.topic_with_primary_key = nil
+
+ assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count")
+ end
+
def test_belongs_to_counter_with_reassigning
t1 = Topic.create("title" => "t1")
t2 = Topic.create("title" => "t2")
@@ -219,6 +279,18 @@ def test_assignment_before_child_saved
assert_equal firm, final_cut.firm(true)
end
+ def test_assignment_before_child_saved_with_primary_key
+ final_cut = Client.new("name" => "Final Cut")
+ firm = Firm.find(1)
+ final_cut.firm_with_primary_key = firm
+ assert final_cut.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !firm.new_record?
+ assert_equal firm, final_cut.firm_with_primary_key
+ assert_equal firm, final_cut.firm_with_primary_key(true)
+ end
+
def test_new_record_with_foreign_key_but_no_object
c = Client.new("firm_id" => 1)
assert_equal Firm.find(:first), c.firm_with_basic_id
@@ -304,6 +376,20 @@ def test_polymorphic_assignment_foreign_type_field_updating
sponsor.sponsorable = member
assert_equal "Member", sponsor.sponsorable_type
end
+
+ def test_polymorphic_assignment_with_primary_key_foreign_type_field_updating
+ # should update when assigning a saved record
+ essay = Essay.new
+ writer = Author.create(:name => "David")
+ essay.writer = writer
+ assert_equal "Author", essay.writer_type
+
+ # should update when assigning a new record
+ essay = Essay.new
+ writer = Author.new
+ essay.writer = writer
+ assert_equal "Author", essay.writer_type
+ end
def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_records
sponsor = Sponsor.new
@@ -317,6 +403,18 @@ def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_recor
assert_equal nil, sponsor.sponsorable_id
end
+ def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
+ essay = Essay.new
+ saved_writer = Author.create(:name => "David")
+ new_writer = Author.new
+
+ essay.writer = saved_writer
+ assert_equal saved_writer.name, essay.writer_id
+
+ essay.writer = new_writer
+ assert_equal nil, essay.writer_id
+ end
+
def test_belongs_to_proxy_should_not_respond_to_private_methods
assert_raise(NoMethodError) { companies(:first_firm).private_method }
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
View
8 activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -243,8 +243,12 @@ def test_inner_join_with_quoted_table_name
assert_equal 2, people(:michael).jobs.size
end
- def test_get_ids
- assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
+ def test_get_ids_for_belongs_to_source
+ assert_sql(/DISTINCT/) { assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort }
+ end
+
+ def test_get_ids_for_has_many_source
+ assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids
end
def test_get_ids_for_loaded_associations
View
2 activerecord/test/cases/base_test.rb
@@ -2026,7 +2026,7 @@ def test_inspect_class
def test_inspect_instance
topic = topics(:first)
- assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, type: nil>), topic.inspect
+ assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil>), topic.inspect
end
def test_inspect_new_instance
View
10 activerecord/test/cases/reflection_test.rb
@@ -27,25 +27,25 @@ def test_column_null_not_null
def test_read_attribute_names
assert_equal(
- %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id type ).sort,
+ %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id parent_title type ).sort,
@first.attribute_names
)
end
def test_columns
- assert_equal 12, Topic.columns.length
+ assert_equal 13, Topic.columns.length
end
def test_columns_are_returned_in_the_order_they_were_declared
column_names = Topic.columns.map { |column| column.name }
- assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id type), column_names
+ assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type), column_names
end
def test_content_columns
content_columns = Topic.content_columns
content_column_names = content_columns.map {|column| column.name}
- assert_equal 8, content_columns.length
- assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved).sort, content_column_names.sort
+ assert_equal 9, content_columns.length
+ assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved parent_title).sort, content_column_names.sort
end
def test_column_string_type_and_limit
View
2 activerecord/test/models/author.rb
@@ -87,6 +87,8 @@ def testing_proxy_target
has_many :tags, :through => :posts # through has_many :through
has_many :post_categories, :through => :posts, :source => :categories
+ has_one :essay, :primary_key => :name, :as => :writer
+
belongs_to :author_address, :dependent => :destroy
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
View
1 activerecord/test/models/company.rb
@@ -88,6 +88,7 @@ class Client < Company
belongs_to :firm_with_select, :class_name => "Firm", :foreign_key => "firm_id", :select => "id"
belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1]
+ belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name"
belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true
# Record destruction so we can test whether firm.clients.clear has
View
3 activerecord/test/models/essay.rb
@@ -0,0 +1,3 @@
+class Essay < ActiveRecord::Base
+ belongs_to :writer, :primary_key => :name, :polymorphic => true
+end
View
3 activerecord/test/models/reply.rb
@@ -4,12 +4,13 @@ class Reply < Topic
named_scope :base
belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
+ belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count"
has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id"
validate :errors_on_empty_content
validate_on_create :title_is_wrong_create
- attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read
+ attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read, :parent_title
validate :check_empty_title
validate_on_create :check_content_mismatch
View
1 activerecord/test/models/topic.rb
@@ -39,6 +39,7 @@ def extension_two
named_scope :by_rejected_ids, lambda {{ :conditions => { :id => all(:conditions => {:approved => false}).map(&:id) } }}
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
+ has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title"
serialize :content
before_create :default_written_on
View
7 activerecord/test/schema/schema.rb
@@ -161,6 +161,12 @@ def create_table(*args, &block)
t.integer :course_id, :null => false
end
+ create_table :essays, :force => true do |t|
+ t.string :name
+ t.string :writer_id
+ t.string :writer_type
+ end
+
create_table :events, :force => true do |t|
t.string :title, :limit => 5
end
@@ -421,6 +427,7 @@ def create_table(*args, &block)
t.boolean :approved, :default => true
t.integer :replies_count, :default => 0
t.integer :parent_id
+ t.string :parent_title
t.string :type
end
View
2 activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -4,7 +4,7 @@ def deep_merge(other_hash)
merge(other_hash) do |key, oldval, newval|
oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
newval = newval.to_hash if newval.respond_to?(:to_hash)
- oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
+ oldval.is_a?( Hash ) && newval.is_a?( Hash ) ? oldval.deep_merge(newval) : newval
end
end
View
2 activesupport/lib/active_support/core_ext/hash/diff.rb
@@ -8,6 +8,6 @@ class Hash
# {}.diff(1 => 2) # => {1 => 2}
# {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
def diff(h2)
- dup.delete_if { |k, v| h2[k] == v }.merge(h2.dup.delete_if { |k, v| has_key?(k) })
+ dup.delete_if { |k, v| h2[k] == v }.merge!(h2.dup.delete_if { |k, v| has_key?(k) })
end
end
View
2 activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
@@ -21,7 +21,7 @@ def reverse_merge(other_hash)
# Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
# Modifies the receiver in place.
def reverse_merge!(other_hash)
- replace(reverse_merge(other_hash))
+ merge!( other_hash ){|k,o,n| o }
end
alias_method :reverse_update, :reverse_merge!
View
152 activesupport/lib/active_support/core_ext/string/interpolation.rb
@@ -1,87 +1,93 @@
-if RUBY_VERSION < '1.9'
-
=begin
- string.rb - Extension for String.
-
+ heavily based on Masao Mutoh's gettext String interpolation extension
+ http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
Copyright (C) 2005-2009 Masao Mutoh
-
- You may redistribute it and/or modify it under the same
- license terms as Ruby.
+ You may redistribute it and/or modify it under the same license terms as Ruby.
=end
-# This feature is included in Ruby 1.9 or later but not occur TypeError.
-#
-# String#% method which accepts named arguments. Particularly useful if the
-# string is to be used by a translator because named arguments mean more
-# than %s/%d style.
-class String
+if RUBY_VERSION < '1.9'
- unless instance_methods.find {|m| m.to_s == 'bytesize'}
- # For older ruby (such as ruby-1.8.5)
- alias :bytesize :size
- end
+ # KeyError is raised by String#% when the string contains a named placeholder
+ # that is not contained in the given arguments hash. Ruby 1.9 includes and
+ # raises this exception natively. We define it to mimic Ruby 1.9's behaviour
+ # in Ruby 1.8.x
- alias :_old_format_m :% # :nodoc:
+ class KeyError < IndexError
+ def initialize(message = nil)
+ super(message || "key not found")
+ end
+ end unless defined?(KeyError)
- PERCENT_MATCH_RE = Regexp.union(
+ # Extension for String class. This feature is included in Ruby 1.9 or later but not occur TypeError.
+ #
+ # String#% method which accept "named argument". The translator can know
+ # the meaning of the msgids using "named argument" instead of %s/%d style.
+
+ class String
+ # For older ruby versions, such as ruby-1.8.5
+ alias :bytesize :size unless instance_methods.find {|m| m.to_s == 'bytesize'}
+ alias :interpolate_without_ruby_19_syntax :% # :nodoc:
+
+ INTERPOLATION_PATTERN = Regexp.union(
/%%/,
- /%\{(\w+)\}/,
- /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/
- )
+ /%\{(\w+)\}/, # matches placeholders like "%{foo}"
+ /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
+ )
- # call-seq:
- # %(arg)
- # %(hash)
- #
- # Format - Uses str as a format specification, and returns the result of applying it to arg.
- # If the format specification contains more than one substitution, then arg must be
- # an Array containing the values to be substituted. See Kernel::sprintf for details of the
- # format string. This is the default behavior of the String class.
- # * arg: an Array or other class except Hash.
- # * Returns: formatted String
- # Example:
- # "%s, %s" % ["Masao", "Mutoh"]
- #
- # Also you can use a Hash as the "named argument". This is recommended way so translators
- # can understand the meanings of the msgids easily.
- # * hash: {:key1 => value1, :key2 => value2, ... }
- # * Returns: formatted String
- # Example:
- # For strings.
- # "%{firstname}, %{familyname}" % {:firstname => "Masao", :familyname => "Mutoh"}
- #
- # With field type to specify format such as d(decimal), f(float),...
- # "%<age>d, %<weight>.1f" % {:age => 10, :weight => 43.4}
- def %(args)
- if args.kind_of?(Hash)
- ret = dup
- ret.gsub!(PERCENT_MATCH_RE) {|match|
- if match == '%%'
- '%'
- elsif $1
- key = $1.to_sym
- args.has_key?(key) ? args[key] : match
- elsif $2
- key = $2.to_sym
- args.has_key?(key) ? sprintf("%#{$3}", args[key]) : match
- end
- }
- ret
- else
- ret = gsub(/%([{<])/, '%%\1')
- begin
- ret._old_format_m(args)
- rescue ArgumentError => e
- if $DEBUG
- $stderr.puts " The string:#{ret}"
- $stderr.puts " args:#{args.inspect}"
- puts e.backtrace
- else
- raise ArgumentError, e.message
+ # % uses self (i.e. the String) as a format specification and returns the
+ # result of applying it to the given arguments. In other words it interpolates
+ # the given arguments to the string according to the formats the string
+ # defines.
+ #
+ # There are three ways to use it:
+ #
+ # * Using a single argument or Array of arguments.
+ #
+ # This is the default behaviour of the String class. See Kernel#sprintf for
+ # more details about the format string.
+ #
+ # Example:
+ #
+ # "%d %s" % [1, "message"]
+ # # => "1 message"
+ #
+ # * Using a Hash as an argument and unformatted, named placeholders.
+ #
+ # When you pass a Hash as an argument and specify placeholders with %{foo}
+ # it will interpret the hash values as named arguments.
+ #
+ # Example:
+ #
+ # "%{firstname}, %{lastname}" % {:firstname => "Masao", :lastname => "Mutoh"}
+ # # => "Masao Mutoh"
+ #
+ # * Using a Hash as an argument and formatted, named placeholders.
+ #
+ # When you pass a Hash as an argument and specify placeholders with %<foo>d
+ # it will interpret the hash values as named arguments and format the value
+ # according to the formatting instruction appended to the closing >.
+ #
+ # Example:
+ #
+ # "%<integer>d, %<float>.1f" % { :integer => 10, :float => 43.4 }
+ # # => "10, 43.3"
+ def %(args)
+ if args.kind_of?(Hash)
+ dup.gsub(INTERPOLATION_PATTERN) do |match|
+ if match == '%%'
+ '%'
+ else
+ key = ($1 || $2).to_sym
+ raise KeyError unless args.has_key?(key)
+ $3 ? sprintf("%#{$3}", args[key]) : args[key]
+ end
end
+ elsif self =~ INTERPOLATION_PATTERN
+ raise ArgumentError.new('one hash required')
+ else
+ result = gsub(/%([{<])/, '%%\1')
+ result.send :'interpolate_without_ruby_19_syntax', args
end
end
end
-end
-
end
View
6 activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -98,6 +98,10 @@ def reverse_merge(other_hash)
super other_hash.with_indifferent_access
end
+ def reverse_merge!(other_hash)
+ replace(reverse_merge( other_hash ))
+ end
+
# Removes a specified key from the hash.
def delete(key)
super(convert_key(key))
@@ -109,7 +113,7 @@ def to_options!; self end
# Convert to a Hash with String keys.
def to_hash
- Hash.new(default).merge(self)
+ Hash.new(default).merge!(self)
end
protected
View
13 activesupport/test/core_ext/string_ext_test.rb
@@ -311,8 +311,8 @@ def test_sprintf_percent_in_replacement
end
def test_sprintf_lack_argument
- assert_equal("%{num}, test", "%{num}, %{record}" % {:record => "test"})
- assert_equal("%{record}", "%{record}" % {:num => 1})
+ assert_raises(KeyError) { "%{num}, %{record}" % {:record => "test"} }
+ assert_raises(KeyError) { "%{record}" % {:num => 1} }
end
def test_no_placeholder
@@ -336,9 +336,12 @@ def test_sprintf_old_style
assert_equal("foo 1.000000", "%s %f" % ["foo", 1.0])
end
- def test_sprintf_mix
+ def test_sprintf_mix_unformatted_and_formatted_named_placeholders
assert_equal("foo 1.000000", "%{name} %<num>f" % {:name => "foo", :num => 1.0})
- assert_equal("%{name} 1.000000", "%{name} %f" % [1.0])
- assert_equal("%{name} 1.000000", "%{name} %f" % [1.0, 2.0])
+ end
+
+ def test_string_interpolation_raises_an_argument_error_when_mixing_named_and_unnamed_placeholders
+ assert_raises(ArgumentError) { "%{name} %f" % [1.0] }
+ assert_raises(ArgumentError) { "%{name} %f" % [1.0, 2.0] }
end
end

0 comments on commit 37658f1

Please sign in to comment.