jmongo 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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