mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge branch 'master' of git@github.com:rails/rails
This commit is contained in:
commit
37658f15bb
21 changed files with 268 additions and 101 deletions
|
@ -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]
|
||||
|
|
|
@ -963,6 +963,8 @@ module ActiveRecord
|
|||
# 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 @@ module ActiveRecord
|
|||
#
|
||||
# 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 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
|
||||
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
|
||||
]
|
||||
|
|
|
@ -14,7 +14,7 @@ module ActiveRecord
|
|||
|
||||
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 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
|
||||
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 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -7,7 +7,7 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
!@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
|
||||
|
|
|
@ -339,7 +339,8 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -14,6 +14,7 @@ require 'models/tagging'
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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,6 +82,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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")
|
||||
|
@ -76,6 +98,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
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 }
|
||||
|
|
|
@ -243,8 +243,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
|||
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
|
||||
|
|
|
@ -2026,7 +2026,7 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
|
||||
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
|
||||
|
|
|
@ -27,25 +27,25 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
|
||||
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
|
||||
|
|
|
@ -87,6 +87,8 @@ class Author < ActiveRecord::Base
|
|||
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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
3
activerecord/test/models/essay.rb
Normal file
3
activerecord/test/models/essay.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class Essay < ActiveRecord::Base
|
||||
belongs_to :writer, :primary_key => :name, :polymorphic => true
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -39,6 +39,7 @@ class Topic < ActiveRecord::Base
|
|||
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
|
||||
|
|
|
@ -161,6 +161,12 @@ ActiveRecord::Schema.define do
|
|||
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 @@ ActiveRecord::Schema.define do
|
|||
t.boolean :approved, :default => true
|
||||
t.integer :replies_count, :default => 0
|
||||
t.integer :parent_id
|
||||
t.string :parent_title
|
||||
t.string :type
|
||||
end
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ class 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -21,7 +21,7 @@ class 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!
|
||||
|
|
|
@ -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
|
|
@ -98,6 +98,10 @@ module ActiveSupport
|
|||
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 @@ module ActiveSupport
|
|||
|
||||
# Convert to a Hash with String keys.
|
||||
def to_hash
|
||||
Hash.new(default).merge(self)
|
||||
Hash.new(default).merge!(self)
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
@ -311,8 +311,8 @@ class TestGetTextString < Test::Unit::TestCase
|
|||
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 @@ class TestGetTextString < Test::Unit::TestCase
|
|||
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
|
||||
|
|
Loading…
Reference in a new issue