jmongo 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. data/Gemfile +8 -0
  2. data/Gemfile.lock +43 -0
  3. data/Rakefile +72 -0
  4. data/jmongo.gemspec +84 -6
  5. data/lib/jmongo.rb +6 -14
  6. data/lib/jmongo/collection.rb +196 -114
  7. data/lib/jmongo/connection.rb +39 -13
  8. data/lib/jmongo/cursor.rb +161 -63
  9. data/lib/jmongo/db.rb +119 -30
  10. data/lib/jmongo/exceptions.rb +39 -0
  11. data/lib/jmongo/mongo-2.6.5.gb1.jar +0 -0
  12. data/lib/jmongo/mongo/bson.rb +130 -0
  13. data/lib/jmongo/mongo/collection.rb +185 -0
  14. data/lib/jmongo/mongo/connection.rb +45 -0
  15. data/lib/jmongo/mongo/db.rb +31 -0
  16. data/lib/jmongo/mongo/jmongo.rb +44 -0
  17. data/lib/jmongo/mongo/mongo.rb +98 -0
  18. data/lib/jmongo/mongo/ruby_ext.rb +38 -0
  19. data/lib/jmongo/mongo/utils.rb +136 -0
  20. data/lib/jmongo/version.rb +1 -1
  21. data/test-results.txt +98 -0
  22. data/test/auxillary/1.4_features.rb +166 -0
  23. data/test/auxillary/authentication_test.rb +68 -0
  24. data/test/auxillary/autoreconnect_test.rb +41 -0
  25. data/test/auxillary/fork_test.rb +30 -0
  26. data/test/auxillary/repl_set_auth_test.rb +58 -0
  27. data/test/auxillary/slave_connection_test.rb +36 -0
  28. data/test/auxillary/threaded_authentication_test.rb +101 -0
  29. data/test/bson/binary_test.rb +15 -0
  30. data/test/bson/bson_test.rb +657 -0
  31. data/test/bson/byte_buffer_test.rb +208 -0
  32. data/test/bson/hash_with_indifferent_access_test.rb +38 -0
  33. data/test/bson/json_test.rb +17 -0
  34. data/test/bson/object_id_test.rb +138 -0
  35. data/test/bson/ordered_hash_test.rb +245 -0
  36. data/test/bson/test_helper.rb +46 -0
  37. data/test/bson/timestamp_test.rb +46 -0
  38. data/test/collection_test.rb +933 -0
  39. data/test/connection_test.rb +325 -0
  40. data/test/conversions_test.rb +121 -0
  41. data/test/cursor_fail_test.rb +75 -0
  42. data/test/cursor_message_test.rb +43 -0
  43. data/test/cursor_test.rb +547 -0
  44. data/test/data/empty_data +0 -0
  45. data/test/data/sample_data +0 -0
  46. data/test/data/sample_file.pdf +0 -0
  47. data/test/data/small_data.txt +1 -0
  48. data/test/db_api_test.rb +739 -0
  49. data/test/db_connection_test.rb +15 -0
  50. data/test/db_test.rb +325 -0
  51. data/test/grid_file_system_test.rb +260 -0
  52. data/test/grid_io_test.rb +210 -0
  53. data/test/grid_test.rb +259 -0
  54. data/test/load/thin/config.ru +6 -0
  55. data/test/load/thin/config.yml.template +6 -0
  56. data/test/load/thin/load.rb +24 -0
  57. data/test/load/unicorn/config.ru +6 -0
  58. data/test/load/unicorn/load.rb +23 -0
  59. data/test/load/unicorn/unicorn.rb.template +29 -0
  60. data/test/replica_sets/connect_test.rb +111 -0
  61. data/test/replica_sets/connection_string_test.rb +29 -0
  62. data/test/replica_sets/count_test.rb +36 -0
  63. data/test/replica_sets/insert_test.rb +54 -0
  64. data/test/replica_sets/pooled_insert_test.rb +58 -0
  65. data/test/replica_sets/query_secondaries.rb +109 -0
  66. data/test/replica_sets/query_test.rb +52 -0
  67. data/test/replica_sets/read_preference_test.rb +43 -0
  68. data/test/replica_sets/refresh_test.rb +123 -0
  69. data/test/replica_sets/replication_ack_test.rb +71 -0
  70. data/test/replica_sets/rs_test_helper.rb +27 -0
  71. data/test/safe_test.rb +68 -0
  72. data/test/support/hash_with_indifferent_access.rb +186 -0
  73. data/test/support/keys.rb +45 -0
  74. data/test/support_test.rb +19 -0
  75. data/test/test_helper.rb +111 -0
  76. data/test/threading/threading_with_large_pool_test.rb +90 -0
  77. data/test/threading_test.rb +88 -0
  78. data/test/tools/auth_repl_set_manager.rb +14 -0
  79. data/test/tools/keyfile.txt +1 -0
  80. data/test/tools/repl_set_manager.rb +377 -0
  81. data/test/unit/collection_test.rb +128 -0
  82. data/test/unit/connection_test.rb +85 -0
  83. data/test/unit/cursor_test.rb +127 -0
  84. data/test/unit/db_test.rb +96 -0
  85. data/test/unit/grid_test.rb +51 -0
  86. data/test/unit/node_test.rb +73 -0
  87. data/test/unit/pool_manager_test.rb +47 -0
  88. data/test/unit/pool_test.rb +9 -0
  89. data/test/unit/read_test.rb +101 -0
  90. data/test/unit/safe_test.rb +125 -0
  91. data/test/uri_test.rb +92 -0
  92. metadata +170 -99
  93. data/lib/jmongo/ajrb.rb +0 -189
  94. data/lib/jmongo/jmongo_jext.rb +0 -302
  95. data/lib/jmongo/mongo-2.6.3.jar +0 -0
  96. data/lib/jmongo/utils.rb +0 -61
@@ -0,0 +1,52 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require './test/replica_sets/rs_test_helper'
3
+
4
+ # NOTE: This test expects a replica set of three nodes to be running
5
+ # on the local host.
6
+ class ReplicaSetQueryTest < Test::Unit::TestCase
7
+ include Mongo
8
+
9
+ def setup
10
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0], RS.ports[1]])
11
+ @db = @conn.db(MONGO_TEST_DB)
12
+ @db.drop_collection("test-sets")
13
+ @coll = @db.collection("test-sets")
14
+ end
15
+
16
+ def teardown
17
+ RS.restart_killed_nodes
18
+ @conn.close if @conn
19
+ end
20
+
21
+ def test_query
22
+ @coll.save({:a => 20}, :safe => {:w => 3})
23
+ @coll.save({:a => 30}, :safe => {:w => 3})
24
+ @coll.save({:a => 40}, :safe => {:w => 3})
25
+ results = []
26
+ @coll.find.each {|r| results << r}
27
+ [20, 30, 40].each do |a|
28
+ assert results.any? {|r| r['a'] == a}, "Could not find record for a => #{a}"
29
+ end
30
+
31
+ puts "Benchmark before failover: #{benchmark_queries}"
32
+
33
+ RS.kill_primary
34
+
35
+ results = []
36
+ rescue_connection_failure do
37
+ @coll.find.each {|r| results << r}
38
+ [20, 30, 40].each do |a|
39
+ assert results.any? {|r| r['a'] == a}, "Could not find record for a => #{a}"
40
+ end
41
+
42
+ puts "Benchmark after failover: #{benchmark_queries}"
43
+ end
44
+ end
45
+
46
+ def benchmark_queries
47
+ t1 = Time.now
48
+ 10000.times { @coll.find_one }
49
+ Time.now - t1
50
+ end
51
+
52
+ end
@@ -0,0 +1,43 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require './test/replica_sets/rs_test_helper'
3
+
4
+ # TODO: enable this once we enable reads from tags.
5
+ class ReadPreferenceTest < Test::Unit::TestCase
6
+ include Mongo
7
+
8
+ #def setup
9
+ # @conn = ReplSetConnection.new([RS.host, RS.ports[0], RS.host, RS.ports[1]], :read => :secondary, :pool_size => 50)
10
+ # @db = @conn.db(MONGO_TEST_DB)
11
+ # @db.drop_collection("test-sets")
12
+ #end
13
+
14
+ # TODO: enable this once we enable reads from tags.
15
+ # def test_query_tagged
16
+ # col = @db['mongo-test']
17
+
18
+ # col.insert({:a => 1}, :safe => {:w => 3})
19
+ # col.find_one({}, :read => {:db => "main"})
20
+ # col.find_one({}, :read => {:dc => "ny"})
21
+ # col.find_one({}, :read => {:dc => "sf"})
22
+
23
+ # assert_raise Mongo::NodeWithTagsNotFound do
24
+ # col.find_one({}, :read => {:foo => "bar"})
25
+ # end
26
+
27
+ # threads = []
28
+ # 100.times do
29
+ # threads << Thread.new do
30
+ # col.find_one({}, :read => {:dc => "sf"})
31
+ # end
32
+ # end
33
+
34
+ # threads.each {|t| t.join }
35
+
36
+ # col.remove
37
+ # end
38
+
39
+ #def teardown
40
+ # RS.restart_killed_nodes
41
+ #end
42
+
43
+ end
@@ -0,0 +1,123 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require './test/replica_sets/rs_test_helper'
3
+ require 'benchmark'
4
+
5
+ # on ports TEST_PORT, RS.ports[1], and TEST + 2.
6
+ class ReplicaSetRefreshTest < Test::Unit::TestCase
7
+ include Mongo
8
+
9
+ def setup
10
+ @conn = nil
11
+ end
12
+
13
+ def teardown
14
+ RS.restart_killed_nodes
15
+ @conn.close if @conn
16
+ end
17
+
18
+ def test_connect_speed
19
+ Benchmark.bm do |x|
20
+ x.report("Connect") do
21
+ 10.times do
22
+ ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
23
+ [RS.host, RS.ports[2]], :background_refresh => false)
24
+ end
25
+ end
26
+
27
+ @con = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
28
+ [RS.host, RS.ports[2]], :background_refresh => false)
29
+
30
+ x.report("manager") do
31
+ man = Mongo::PoolManager.new(@con, @con.seeds)
32
+ 10.times do
33
+ man.connect
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def test_connect_and_manual_refresh_with_secondaries_down
40
+ RS.kill_all_secondaries
41
+
42
+ rescue_connection_failure do
43
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
44
+ [RS.host, RS.ports[2]], :background_refresh => false)
45
+ end
46
+
47
+ assert_equal [], @conn.secondaries
48
+ assert @conn.connected?
49
+ assert_equal @conn.read_pool, @conn.primary_pool
50
+
51
+ # Refresh with no change to set
52
+ @conn.refresh
53
+ assert_equal [], @conn.secondaries
54
+ assert @conn.connected?
55
+ assert_equal @conn.read_pool, @conn.primary_pool
56
+
57
+ RS.restart_killed_nodes
58
+ assert_equal [], @conn.secondaries
59
+ assert @conn.connected?
60
+ assert_equal @conn.read_pool, @conn.primary_pool
61
+
62
+ # Refresh with everything up
63
+ @conn.refresh
64
+ assert @conn.read_pool
65
+ assert @conn.secondaries.length > 0
66
+ end
67
+
68
+ def test_automated_refresh_with_secondaries_down
69
+ RS.kill_all_secondaries
70
+
71
+ rescue_connection_failure do
72
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
73
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
74
+ end
75
+
76
+ assert_equal [], @conn.secondaries
77
+ assert @conn.connected?
78
+ assert_equal @conn.read_pool, @conn.primary_pool
79
+
80
+ RS.restart_killed_nodes
81
+
82
+ sleep(3)
83
+
84
+ assert @conn.read_pool != @conn.primary_pool, "Read pool and primary pool are identical."
85
+ assert @conn.secondaries.length > 0, "No secondaries have been added."
86
+ end
87
+
88
+ def test_automated_refresh_with_removed_node
89
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
90
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
91
+
92
+ assert_equal 2, @conn.secondary_pools.length
93
+ assert_equal 2, @conn.secondaries.length
94
+
95
+ n = RS.remove_secondary_node
96
+ sleep(4)
97
+
98
+ assert_equal 1, @conn.secondaries.length
99
+ assert_equal 1, @conn.secondary_pools.length
100
+
101
+ RS.add_node(n)
102
+ end
103
+
104
+ def test_adding_and_removing_nodes
105
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
106
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
107
+
108
+ RS.add_node
109
+ sleep(5)
110
+
111
+ @conn2 = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
112
+ [RS.host, RS.ports[2]], :refresh_interval => 2, :background_refresh => true)
113
+
114
+ assert @conn2.secondaries == @conn.secondaries
115
+ assert_equal 3, @conn.secondary_pools.length
116
+ assert_equal 3, @conn.secondaries.length
117
+
118
+ RS.remove_secondary_node
119
+ sleep(4)
120
+ assert_equal 2, @conn.secondary_pools.length
121
+ assert_equal 2, @conn.secondaries.length
122
+ end
123
+ end
@@ -0,0 +1,71 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require './test/replica_sets/rs_test_helper'
3
+
4
+ # NOTE: This test expects a replica set of three nodes to be running on local host.
5
+ class ReplicaSetAckTest < Test::Unit::TestCase
6
+ include Mongo
7
+
8
+ def setup
9
+ RS.ensure_up
10
+
11
+ @conn = ReplSetConnection.new([RS.host, RS.ports[0]])
12
+
13
+ @slave1 = Connection.new(@conn.secondary_pools[0].host,
14
+ @conn.secondary_pools[0].port, :slave_ok => true)
15
+
16
+ assert !@slave1.read_primary?
17
+
18
+ @db = @conn.db(MONGO_TEST_DB)
19
+ @db.drop_collection("test-sets")
20
+ @col = @db.collection("test-sets")
21
+ end
22
+
23
+ def teardown
24
+ RS.restart_killed_nodes
25
+ @conn.close if @conn
26
+ end
27
+
28
+ def test_safe_mode_with_w_failure
29
+ assert_raise_error OperationFailure, "timeout" do
30
+ @col.insert({:foo => 1}, :safe => {:w => 4, :wtimeout => 1, :fsync => true})
31
+ end
32
+ assert_raise_error OperationFailure, "timeout" do
33
+ @col.update({:foo => 1}, {:foo => 2}, :safe => {:w => 4, :wtimeout => 1, :fsync => true})
34
+ end
35
+ assert_raise_error OperationFailure, "timeout" do
36
+ @col.remove({:foo => 2}, :safe => {:w => 4, :wtimeout => 1, :fsync => true})
37
+ end
38
+ end
39
+
40
+ def test_safe_mode_replication_ack
41
+ @col.insert({:baz => "bar"}, :safe => {:w => 3, :wtimeout => 5000})
42
+
43
+ assert @col.insert({:foo => "0" * 5000}, :safe => {:w => 3, :wtimeout => 5000})
44
+ assert_equal 2, @slave1[MONGO_TEST_DB]["test-sets"].count
45
+
46
+ assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 3, :wtimeout => 5000})
47
+ assert @slave1[MONGO_TEST_DB]["test-sets"].find_one({:baz => "foo"})
48
+
49
+ assert @col.remove({}, :safe => {:w => 3, :wtimeout => 5000})
50
+ assert_equal 0, @slave1[MONGO_TEST_DB]["test-sets"].count
51
+ end
52
+
53
+ def test_last_error_responses
54
+ 20.times { @col.insert({:baz => "bar"}) }
55
+ response = @db.get_last_error(:w => 2, :wtimeout => 5000)
56
+ assert response['ok'] == 1
57
+ assert response['lastOp']
58
+
59
+ @col.update({}, {:baz => "foo"}, :multi => true)
60
+ response = @db.get_last_error(:w => 2, :wtimeout => 5000)
61
+ assert response['ok'] == 1
62
+ assert response['lastOp']
63
+
64
+ @col.remove({})
65
+ response = @db.get_last_error(:w => 2, :wtimeout => 5000)
66
+ assert response['ok'] == 1
67
+ assert response['n'] == 20
68
+ assert response['lastOp']
69
+ end
70
+
71
+ end
@@ -0,0 +1,27 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require './test/test_helper'
3
+ require './test/tools/repl_set_manager'
4
+
5
+ unless defined? RS
6
+ RS = ReplSetManager.new
7
+ RS.start_set
8
+ end
9
+
10
+ class Test::Unit::TestCase
11
+
12
+ # Generic code for rescuing connection failures and retrying operations.
13
+ # This could be combined with some timeout functionality.
14
+ def rescue_connection_failure(max_retries=30)
15
+ retries = 0
16
+ begin
17
+ yield
18
+ rescue Mongo::ConnectionFailure => ex
19
+ puts "Rescue attempt #{retries}: from #{ex}"
20
+ retries += 1
21
+ raise ex if retries > max_retries
22
+ sleep(2)
23
+ retry
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,68 @@
1
+ require './test/test_helper'
2
+ include Mongo
3
+
4
+ class SafeTest < Test::Unit::TestCase
5
+ context "Safe mode propogation: " do
6
+ setup do
7
+ @con = standard_connection(:safe => {:w => 1})
8
+ @db = @con[MONGO_TEST_DB]
9
+ @col = @db['test-safe']
10
+ @col.create_index([[:a, 1]], :unique => true)
11
+ @col.remove
12
+ end
13
+
14
+ should "propogate safe option on insert" do
15
+ @col.insert({:a => 1})
16
+
17
+ assert_raise_error(OperationFailure, "duplicate key") do
18
+ @col.insert({:a => 1})
19
+ end
20
+ end
21
+
22
+ should "allow safe override on insert" do
23
+ @col.insert({:a => 1})
24
+ @col.insert({:a => 1}, :safe => false)
25
+ end
26
+
27
+ should "propogate safe option on update" do
28
+ @col.insert({:a => 1})
29
+ @col.insert({:a => 2})
30
+
31
+ assert_raise_error(OperationFailure, "duplicate key") do
32
+ @col.update({:a => 2}, {:a => 1})
33
+ end
34
+ end
35
+
36
+ should "allow safe override on update" do
37
+ @col.insert({:a => 1})
38
+ @col.insert({:a => 2})
39
+ @col.update({:a => 2}, {:a => 1}, :safe => false)
40
+ end
41
+ end
42
+
43
+ context "Safe error objects" do
44
+ setup do
45
+ @con = standard_connection
46
+ @db = @con[MONGO_TEST_DB]
47
+ @col = @db['test']
48
+ @col.remove
49
+ @col.insert({:a => 1})
50
+ @col.insert({:a => 1})
51
+ @col.insert({:a => 1})
52
+ end
53
+
54
+ should "return object on update" do
55
+ response = @col.update({:a => 1}, {"$set" => {:a => 2}},
56
+ :multi => true, :safe => true)
57
+
58
+ assert response['updatedExisting']
59
+ assert_equal 3, response['n']
60
+ end
61
+
62
+ should "return object on remove" do
63
+ response = @col.remove({}, :safe => true)
64
+ assert_equal 3, response['n']
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,186 @@
1
+ # Note: HashWithIndifferentAccess is so commonly used
2
+ # that we always need to make sure that the driver works
3
+ # with it.
4
+ #require File.join(File.dirname(__FILE__), 'keys.rb')
5
+
6
+ # This class has dubious semantics and we only have it so that
7
+ # people can write params[:key] instead of params['key']
8
+ # and they get the same value for both keys.
9
+
10
+ class Hash
11
+ # Return a new hash with all keys converted to strings.
12
+ def stringify_keys
13
+ dup.stringify_keys!
14
+ end
15
+
16
+ # Destructively convert all keys to strings.
17
+ def stringify_keys!
18
+ keys.each do |key|
19
+ self[key.to_s] = delete(key)
20
+ end
21
+ self
22
+ end
23
+
24
+ # Return a new hash with all keys converted to symbols, as long as
25
+ # they respond to +to_sym+.
26
+ def symbolize_keys
27
+ dup.symbolize_keys!
28
+ end
29
+
30
+ # Destructively convert all keys to symbols, as long as they respond
31
+ # to +to_sym+.
32
+ def symbolize_keys!
33
+ keys.each do |key|
34
+ self[(key.to_sym rescue key) || key] = delete(key)
35
+ end
36
+ self
37
+ end
38
+
39
+ alias_method :to_options, :symbolize_keys
40
+ #alias_method :to_options!, :symbolize_keys!
41
+ end
42
+
43
+ module ActiveSupport
44
+ class HashWithIndifferentAccess < Hash
45
+ def extractable_options?
46
+ true
47
+ end
48
+
49
+ def initialize(constructor = {})
50
+ if constructor.is_a?(Hash)
51
+ super()
52
+ update(constructor)
53
+ else
54
+ super(constructor)
55
+ end
56
+ end
57
+
58
+ def default(key = nil)
59
+ if key.is_a?(Symbol) && include?(key = key.to_s)
60
+ self[key]
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def self.new_from_hash_copying_default(hash)
67
+ ActiveSupport::HashWithIndifferentAccess.new(hash).tap do |new_hash|
68
+ new_hash.default = hash.default
69
+ end
70
+ end
71
+
72
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
73
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
74
+
75
+ # Assigns a new value to the hash:
76
+ #
77
+ # hash = HashWithIndifferentAccess.new
78
+ # hash[:key] = "value"
79
+ #
80
+ def []=(key, value)
81
+ regular_writer(convert_key(key), convert_value(value))
82
+ end
83
+
84
+ # Updates the instantized hash with values from the second:
85
+ #
86
+ # hash_1 = HashWithIndifferentAccess.new
87
+ # hash_1[:key] = "value"
88
+ #
89
+ # hash_2 = HashWithIndifferentAccess.new
90
+ # hash_2[:key] = "New Value!"
91
+ #
92
+ # hash_1.update(hash_2) # => {"key"=>"New Value!"}
93
+ #
94
+ def update(other_hash)
95
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
96
+ self
97
+ end
98
+
99
+ alias_method :merge!, :update
100
+
101
+ # Checks the hash for a key matching the argument passed in:
102
+ #
103
+ # hash = HashWithIndifferentAccess.new
104
+ # hash["key"] = "value"
105
+ # hash.key? :key # => true
106
+ # hash.key? "key" # => true
107
+ #
108
+ def key?(key)
109
+ super(convert_key(key))
110
+ end
111
+
112
+ alias_method :include?, :key?
113
+ alias_method :has_key?, :key?
114
+ alias_method :member?, :key?
115
+
116
+ # Fetches the value for the specified key, same as doing hash[key]
117
+ def fetch(key, *extras)
118
+ super(convert_key(key), *extras)
119
+ end
120
+
121
+ # Returns an array of the values at the specified indices:
122
+ #
123
+ # hash = HashWithIndifferentAccess.new
124
+ # hash[:a] = "x"
125
+ # hash[:b] = "y"
126
+ # hash.values_at("a", "b") # => ["x", "y"]
127
+ #
128
+ def values_at(*indices)
129
+ indices.collect {|key| self[convert_key(key)]}
130
+ end
131
+
132
+ # Returns an exact copy of the hash.
133
+ def dup
134
+ HashWithIndifferentAccess.new(self)
135
+ end
136
+
137
+ # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
138
+ # Does not overwrite the existing hash.
139
+ def merge(hash)
140
+ self.dup.update(hash)
141
+ end
142
+
143
+ # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
144
+ # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
145
+ def reverse_merge(other_hash)
146
+ super self.class.new_from_hash_copying_default(other_hash)
147
+ end
148
+
149
+ def reverse_merge!(other_hash)
150
+ replace(reverse_merge( other_hash ))
151
+ end
152
+
153
+ # Removes a specified key from the hash.
154
+ def delete(key)
155
+ super(convert_key(key))
156
+ end
157
+
158
+ def stringify_keys!; self end
159
+ def stringify_keys; dup end
160
+ def symbolize_keys; to_hash.symbolize_keys end
161
+ def to_options!; self end
162
+
163
+ # Convert to a Hash with String keys.
164
+ def to_hash
165
+ Hash.new(default).merge!(self)
166
+ end
167
+
168
+ protected
169
+ def convert_key(key)
170
+ key.kind_of?(Symbol) ? key.to_s : key
171
+ end
172
+
173
+ def convert_value(value)
174
+ case value
175
+ when Hash
176
+ self.class.new_from_hash_copying_default(value)
177
+ when Array
178
+ value.collect { |e| e.is_a?(Hash) ? self.class.new_from_hash_copying_default(e) : e }
179
+ else
180
+ value
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess