mongo 1.6.2 → 1.6.4

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 (76) hide show
  1. data/README.md +44 -22
  2. data/Rakefile +17 -4
  3. data/docs/GridFS.md +2 -2
  4. data/docs/HISTORY.md +15 -1
  5. data/docs/RELEASES.md +4 -4
  6. data/docs/TUTORIAL.md +12 -0
  7. data/docs/WRITE_CONCERN.md +1 -1
  8. data/lib/mongo/collection.rb +1 -1
  9. data/lib/mongo/connection.rb +35 -47
  10. data/lib/mongo/cursor.rb +10 -9
  11. data/lib/mongo/db.rb +1 -1
  12. data/lib/mongo/gridfs/grid_ext.rb +4 -4
  13. data/lib/mongo/gridfs/grid_file_system.rb +3 -3
  14. data/lib/mongo/gridfs/grid_io.rb +1 -1
  15. data/lib/mongo/networking.rb +5 -0
  16. data/lib/mongo/repl_set_connection.rb +47 -21
  17. data/lib/mongo/util/conversions.rb +23 -0
  18. data/lib/mongo/util/logging.rb +13 -18
  19. data/lib/mongo/util/node.rb +1 -5
  20. data/lib/mongo/util/pool.rb +0 -1
  21. data/lib/mongo/util/ssl_socket.rb +3 -1
  22. data/lib/mongo/util/support.rb +1 -0
  23. data/lib/mongo/util/tcp_socket.rb +15 -32
  24. data/lib/mongo/util/uri_parser.rb +100 -35
  25. data/lib/mongo/version.rb +1 -1
  26. data/test/auxillary/1.4_features.rb +1 -1
  27. data/test/auxillary/authentication_test.rb +1 -1
  28. data/test/auxillary/autoreconnect_test.rb +1 -1
  29. data/test/auxillary/fork_test.rb +1 -1
  30. data/test/auxillary/repl_set_auth_test.rb +1 -1
  31. data/test/auxillary/slave_connection_test.rb +1 -1
  32. data/test/auxillary/threaded_authentication_test.rb +1 -1
  33. data/test/bson/binary_test.rb +1 -1
  34. data/test/bson/bson_test.rb +8 -1
  35. data/test/bson/byte_buffer_test.rb +1 -1
  36. data/test/bson/hash_with_indifferent_access_test.rb +1 -1
  37. data/test/bson/json_test.rb +1 -1
  38. data/test/bson/object_id_test.rb +11 -1
  39. data/test/bson/ordered_hash_test.rb +1 -1
  40. data/test/bson/timestamp_test.rb +1 -1
  41. data/test/collection_test.rb +1 -1
  42. data/test/connection_test.rb +25 -1
  43. data/test/conversions_test.rb +1 -1
  44. data/test/cursor_fail_test.rb +1 -1
  45. data/test/cursor_message_test.rb +1 -1
  46. data/test/cursor_test.rb +1 -1
  47. data/test/db_api_test.rb +57 -3
  48. data/test/db_connection_test.rb +1 -1
  49. data/test/db_test.rb +1 -1
  50. data/test/grid_file_system_test.rb +1 -1
  51. data/test/grid_io_test.rb +30 -1
  52. data/test/grid_test.rb +1 -1
  53. data/test/pool_test.rb +1 -1
  54. data/test/replica_sets/basic_test.rb +10 -0
  55. data/test/replica_sets/connect_test.rb +45 -0
  56. data/test/replica_sets/read_preference_test.rb +2 -1
  57. data/test/replica_sets/refresh_with_threads_test.rb +2 -0
  58. data/test/replica_sets/rs_test_helper.rb +1 -1
  59. data/test/safe_test.rb +1 -1
  60. data/test/support_test.rb +1 -1
  61. data/test/threading/threading_with_large_pool_test.rb +1 -1
  62. data/test/threading_test.rb +1 -1
  63. data/test/timeout_test.rb +1 -1
  64. data/test/tools/repl_set_manager.rb +1 -0
  65. data/test/unit/collection_test.rb +1 -1
  66. data/test/unit/connection_test.rb +89 -1
  67. data/test/unit/cursor_test.rb +1 -1
  68. data/test/unit/db_test.rb +1 -1
  69. data/test/unit/grid_test.rb +1 -1
  70. data/test/unit/node_test.rb +1 -1
  71. data/test/unit/pool_manager_test.rb +1 -1
  72. data/test/unit/pool_test.rb +1 -1
  73. data/test/unit/read_test.rb +1 -1
  74. data/test/unit/safe_test.rb +1 -1
  75. data/test/uri_test.rb +25 -5
  76. metadata +5 -5
@@ -33,19 +33,19 @@ module Mongo
33
33
  # @example
34
34
  #
35
35
  # # Check for the existence of a given filename
36
- # @grid = GridFileSystem.new(@db)
36
+ # @grid = Mongo::GridFileSystem.new(@db)
37
37
  # @grid.exist?(:filename => 'foo.txt')
38
38
  #
39
39
  # # Check for existence filename and content type
40
- # @grid = GridFileSystem.new(@db)
40
+ # @grid = Mongo::GridFileSystem.new(@db)
41
41
  # @grid.exist?(:filename => 'foo.txt', :content_type => 'image/jpg')
42
42
  #
43
43
  # # Check for existence by _id
44
- # @grid = Grid.new(@db)
44
+ # @grid = Mongo::Grid.new(@db)
45
45
  # @grid.exist?(:_id => BSON::ObjectId.from_string('4bddcd24beffd95a7db9b8c8'))
46
46
  #
47
47
  # # Check for existence by an arbitrary attribute.
48
- # @grid = Grid.new(@db)
48
+ # @grid = Mongo::Grid.new(@db)
49
49
  # @grid.exist?(:tags => {'$in' => ['nature', 'zen', 'photography']})
50
50
  #
51
51
  # @return [nil, Hash] either nil for the file's metadata as a hash.
@@ -78,20 +78,20 @@ module Mongo
78
78
  # @example
79
79
  #
80
80
  # # Store the text "Hello, world!" in the grid file system.
81
- # @grid = GridFileSystem.new(@db)
81
+ # @grid = Mongo::GridFileSystem.new(@db)
82
82
  # @grid.open('filename', 'w') do |f|
83
83
  # f.write "Hello, world!"
84
84
  # end
85
85
  #
86
86
  # # Output "Hello, world!"
87
- # @grid = GridFileSystem.new(@db)
87
+ # @grid = Mongo::GridFileSystem.new(@db)
88
88
  # @grid.open('filename', 'r') do |f|
89
89
  # puts f.read
90
90
  # end
91
91
  #
92
92
  # # Write a file on disk to the GridFileSystem
93
93
  # @file = File.open('image.jpg')
94
- # @grid = GridFileSystem.new(@db)
94
+ # @grid = Mongo::GridFileSystem.new(@db)
95
95
  # @grid.open('image.jpg, 'w') do |f|
96
96
  # f.write @file
97
97
  # end
@@ -275,7 +275,7 @@ module Mongo
275
275
  end
276
276
 
277
277
  def save_chunk(chunk)
278
- @chunks.insert(chunk)
278
+ @chunks.save(chunk)
279
279
  end
280
280
 
281
281
  def get_chunk(n)
@@ -140,6 +140,11 @@ module Mongo
140
140
  rescue SystemStackError, NoMemoryError, SystemCallError => ex
141
141
  close
142
142
  raise ex
143
+ rescue Exception => ex
144
+ if defined?(IRB)
145
+ close if ex.class == IRB::Abort
146
+ end
147
+ raise ex
143
148
  ensure
144
149
  if should_checkin
145
150
  if command || read == :primary
@@ -25,10 +25,12 @@ module Mongo
25
25
  :read_secondary, :rs_name, :name]
26
26
 
27
27
  attr_reader :replica_set_name, :seeds, :refresh_interval, :refresh_mode,
28
- :refresh_version
28
+ :refresh_version, :manager
29
29
 
30
30
  # Create a connection to a MongoDB replica set.
31
31
  #
32
+ # If no args are provided, it will check <code>ENV["MONGODB_URI"]</code>.
33
+ #
32
34
  # Once connected to a replica set, you can find out which nodes are primary, secondary, and
33
35
  # arbiters with the corresponding accessors: Connection#primary, Connection#secondaries, and
34
36
  # Connection#arbiters. This is useful if your application needs to connect manually to nodes other
@@ -68,16 +70,18 @@ module Mongo
68
70
  # The purpose of seed nodes is to permit the driver to find at least one replica set member even if a member is down.
69
71
  #
70
72
  # @example Connect to a replica set and provide two seed nodes.
71
- # ReplSetConnection.new(['localhost:30000', 'localhost:30001'])
73
+ # Mongo::ReplSetConnection.new(['localhost:30000', 'localhost:30001'])
72
74
  #
73
75
  # @example Connect to a replica set providing two seed nodes and ensuring a connection to the replica set named 'prod':
74
- # ReplSetConnection.new(['localhost:30000', 'localhost:30001'], :name => 'prod')
76
+ # Mongo::ReplSetConnection.new(['localhost:30000', 'localhost:30001'], :name => 'prod')
75
77
  #
76
78
  # @example Connect to a replica set providing two seed nodes and allowing reads from a secondary node:
77
- # ReplSetConnection.new(['localhost:30000', 'localhost:30001'], :read => :secondary)
79
+ # Mongo::ReplSetConnection.new(['localhost:30000', 'localhost:30001'], :read => :secondary)
78
80
  #
79
81
  # @see http://api.mongodb.org/ruby/current/file.REPLICA_SETS.html Replica sets in Ruby
80
82
  #
83
+ # @raise [MongoArgumentError] If called with no arguments and <code>ENV["MONGODB_URI"]</code> implies a direct connection.
84
+ #
81
85
  # @raise [ReplicaSetConnectionError] This is raised if a replica set name is specified and the
82
86
  # driver fails to connect to a replica set with that name.
83
87
  def initialize(*args)
@@ -87,21 +91,30 @@ module Mongo
87
91
  opts = {}
88
92
  end
89
93
 
90
- unless args.length > 0
94
+ nodes = args
95
+
96
+ if nodes.empty? and ENV.has_key?('MONGODB_URI')
97
+ parser = URIParser.new ENV['MONGODB_URI'], opts
98
+ if parser.direct?
99
+ raise MongoArgumentError, "Mongo::ReplSetConnection.new called with no arguments, but ENV['MONGODB_URI'] implies a direct connection."
100
+ end
101
+ opts = parser.connection_options
102
+ nodes = parser.nodes
103
+ end
104
+
105
+ unless nodes.length > 0
91
106
  raise MongoArgumentError, "A ReplSetConnection requires at least one seed node."
92
107
  end
93
108
 
94
109
  # This is temporary until support for the old format is dropped
95
- @seeds = []
96
- if args.first.last.is_a?(Integer)
110
+ if nodes.first.last.is_a?(Integer)
97
111
  warn "Initiating a ReplSetConnection with seeds passed as individual [host, port] array arguments is deprecated."
98
112
  warn "Please specify hosts as an array of 'host:port' strings; the old format will be removed in v2.0"
99
- @seeds = args
113
+ @seeds = nodes
100
114
  else
101
- args.first.map do |host_port|
102
- seed = host_port.split(":")
103
- seed[1] = seed[1].to_i
104
- seeds << seed
115
+ @seeds = nodes.first.map do |host_port|
116
+ host, port = host_port.split(":")
117
+ [ host, port.to_i ]
105
118
  end
106
119
  end
107
120
 
@@ -149,8 +162,9 @@ module Mongo
149
162
 
150
163
  discovered_seeds = @manager ? @manager.seeds : []
151
164
  @manager = PoolManager.new(self, discovered_seeds)
152
-
153
- Thread.current[:manager] = @manager
165
+
166
+ Thread.current[:managers] ||= Hash.new
167
+ Thread.current[:managers][self] = @manager
154
168
 
155
169
  @manager.connect
156
170
  @refresh_version += 1
@@ -203,7 +217,7 @@ module Mongo
203
217
  new_manager = PoolManager.new(self, discovered_seeds | @seeds)
204
218
  new_manager.connect
205
219
 
206
- Thread.current[:manager] = new_manager
220
+ Thread.current[:managers][self] = new_manager
207
221
 
208
222
  # TODO: make sure that connect has succeeded
209
223
  @old_managers << @manager
@@ -263,6 +277,12 @@ module Mongo
263
277
  else
264
278
  @manager.close if @manager
265
279
  end
280
+
281
+ # Clear the reference to this object.
282
+ if Thread.current[:managers]
283
+ Thread.current[:managers].delete(self)
284
+ end
285
+
266
286
  @connected = false
267
287
  end
268
288
 
@@ -394,11 +414,17 @@ module Mongo
394
414
  end
395
415
  end
396
416
 
397
- def get_socket_from_pool(pool_type)
398
- if Thread.current[:manager] != @manager
399
- Thread.current[:manager] = @manager
417
+ def ensure_manager
418
+ Thread.current[:managers] ||= Hash.new
419
+
420
+ if Thread.current[:managers][self] != @manager
421
+ Thread.current[:managers][self] = @manager
400
422
  end
401
-
423
+ end
424
+
425
+ def get_socket_from_pool(pool_type)
426
+ ensure_manager
427
+
402
428
  pool = case pool_type
403
429
  when :primary
404
430
  primary_pool
@@ -417,9 +443,9 @@ module Mongo
417
443
  return nil
418
444
  end
419
445
  end
420
-
446
+
421
447
  def local_manager
422
- Thread.current[:manager]
448
+ Thread.current[:managers][self] if Thread.current[:managers]
423
449
  end
424
450
 
425
451
  def arbiters
@@ -24,6 +24,29 @@ module Mongo #:nodoc:
24
24
  ASCENDING_CONVERSION = ["ascending", "asc", "1"]
25
25
  DESCENDING_CONVERSION = ["descending", "desc", "-1"]
26
26
 
27
+ # Allows sort parameters to be defined as a Hash.
28
+ # Does not allow usage of un-ordered hashes, therefore
29
+ # Ruby 1.8.x users must use BSON::OrderedHash.
30
+ #
31
+ # Example:
32
+ #
33
+ # <tt>hash_as_sort_parameters({:field1 => :asc, "field2" => :desc})</tt> =>
34
+ # <tt>{ "field1" => 1, "field2" => -1}</tt>
35
+ def hash_as_sort_parameters(value)
36
+ if RUBY_VERSION < '1.9' && !value.is_a?(BSON::OrderedHash)
37
+ raise InvalidSortValueError.new(
38
+ "Hashes used to supply sort order must maintain ordering." +
39
+ "Use BSON::OrderedHash."
40
+ )
41
+ else
42
+ order_by = value.inject({}) do |memo, (key, direction)|
43
+ memo[key.to_s] = sort_value(direction.to_s.downcase)
44
+ memo
45
+ end
46
+ end
47
+ order_by
48
+ end
49
+
27
50
  # Converts the supplied +Array+ to a +Hash+ to pass to mongo as
28
51
  # sorting parameters. The returned +Hash+ will vary depending
29
52
  # on whether the passed +Array+ is one or two dimensional.
@@ -1,14 +1,10 @@
1
1
  module Mongo
2
2
  module Logging
3
3
 
4
- DEBUG_LEVEL = defined?(Logger) ? Logger::DEBUG : 0
5
-
6
4
  def write_logging_startup_message
7
- if @logger && (@logger.level == DEBUG_LEVEL)
8
- log(:debug, "Logging level is currently :debug which could negatively impact " +
5
+ log(:debug, "Logging level is currently :debug which could negatively impact " +
9
6
  "client-side performance. You should set your logging level no lower than " +
10
7
  ":info in production.")
11
- end
12
8
  end
13
9
 
14
10
  # Log a message with the given level.
@@ -31,27 +27,26 @@ module Mongo
31
27
  end
32
28
 
33
29
  # Execute the block and log the operation described by name and payload.
34
- def instrument(name, payload = {}, &blk)
30
+ def instrument(name, payload = {})
35
31
  start_time = Time.now
36
32
  res = yield
37
- if @logger && (@logger.level == DEBUG_LEVEL)
38
- log_operation(name, payload, start_time)
39
- end
33
+ log_operation(name, payload, start_time)
40
34
  res
41
35
  end
42
36
 
43
37
  protected
44
38
 
45
39
  def log_operation(name, payload, start_time)
46
- msg = "MONGODB "
47
- msg << "(#{((Time.now - start_time) * 1000).to_i}ms) "
48
- msg << "#{payload[:database]}['#{payload[:collection]}'].#{name}("
49
- msg << payload.values_at(:selector, :document, :documents, :fields ).compact.map(&:inspect).join(', ') + ")"
50
- msg << ".skip(#{payload[:skip]})" if payload[:skip]
51
- msg << ".limit(#{payload[:limit]})" if payload[:limit]
52
- msg << ".sort(#{payload[:order]})" if payload[:order]
53
-
54
- @logger.debug(msg)
40
+ @logger && @logger.debug do
41
+ msg = "MONGODB "
42
+ msg << "(#{((Time.now - start_time) * 1000).to_i}ms) "
43
+ msg << "#{payload[:database]}['#{payload[:collection]}'].#{name}("
44
+ msg << payload.values_at(:selector, :document, :documents, :fields ).compact.map(&:inspect).join(', ') + ")"
45
+ msg << ".skip(#{payload[:skip]})" if payload[:skip]
46
+ msg << ".limit(#{payload[:limit]})" if payload[:limit]
47
+ msg << ".sort(#{payload[:order]})" if payload[:order]
48
+ msg
49
+ end
55
50
  end
56
51
 
57
52
  end
@@ -40,11 +40,7 @@ module Mongo
40
40
  @connection.op_timeout, @connection.connect_timeout
41
41
  )
42
42
 
43
- if socket.nil?
44
- return nil
45
- else
46
- socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
47
- end
43
+ return nil if socket.nil?
48
44
  rescue OperationTimeout, ConnectionFailure, OperationFailure, SocketError, SystemCallError, IOError => ex
49
45
  @connection.log(:debug, "Failed connection to #{host_string} with #{ex.class}, #{ex.message}.")
50
46
  socket.close if socket
@@ -157,7 +157,6 @@ module Mongo
157
157
  def checkout_new_socket
158
158
  begin
159
159
  socket = @connection.socket_class.new(@host, @port, @connection.op_timeout)
160
- socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
161
160
  socket.pool = self
162
161
  rescue => ex
163
162
  socket.close if socket
@@ -15,7 +15,9 @@ module Mongo
15
15
  @op_timeout = op_timeout
16
16
  @connect_timeout = connect_timeout
17
17
 
18
- @socket = ::TCPSocket.new(host, port)
18
+ @socket = ::TCPSocket.new(host, port)
19
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
20
+
19
21
  @ssl = OpenSSL::SSL::SSLSocket.new(@socket)
20
22
  @ssl.sync_close = true
21
23
 
@@ -69,6 +69,7 @@ module Mongo
69
69
 
70
70
  def format_order_clause(order)
71
71
  case order
72
+ when Hash, BSON::OrderedHash then hash_as_sort_parameters(order)
72
73
  when String, Symbol then string_as_sort_parameters(order)
73
74
  when Array then array_as_sort_parameters(order)
74
75
  else
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'timeout'
2
3
 
3
4
  module Mongo
4
5
  # Wrapper class for Socket
@@ -16,41 +17,21 @@ module Mongo
16
17
  # TODO: Prefer ipv6 if server is ipv6 enabled
17
18
  @host = Socket.getaddrinfo(host, nil, Socket::AF_INET).first[3]
18
19
  @port = port
20
+
19
21
  @socket_address = Socket.pack_sockaddr_in(@port, @host)
20
22
  @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
23
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
21
24
 
22
25
  connect
23
26
  end
24
27
 
25
28
  def connect
26
- # Connect nonblock is broken in current versions of JRuby
27
- if RUBY_PLATFORM == 'java'
28
- require 'timeout'
29
- if @connect_timeout
30
- Timeout::timeout(@connect_timeout, OperationTimeout) do
31
- @socket.connect(@socket_address)
32
- end
33
- else
29
+ if @connect_timeout
30
+ Timeout::timeout(@connect_timeout, OperationTimeout) do
34
31
  @socket.connect(@socket_address)
35
32
  end
36
33
  else
37
- # Try to connect for @connect_timeout seconds
38
- begin
39
- @socket.connect_nonblock(@socket_address)
40
- rescue Errno::EINPROGRESS
41
- # Block until there is a response or error
42
- resp = IO.select([@socket], [@socket], [@socket], @connect_timeout)
43
- if resp.nil?
44
- raise ConnectionTimeoutError
45
- end
46
- end
47
-
48
- # If there was a failure this will raise an Error
49
- begin
50
- @socket.connect_nonblock(@socket_address)
51
- rescue Errno::EISCONN
52
- # Successfully connected
53
- end
34
+ @socket.connect(@socket_address)
54
35
  end
55
36
  end
56
37
 
@@ -67,13 +48,15 @@ module Mongo
67
48
  end
68
49
  if ready
69
50
  begin
70
- @socket.readpartial(maxlen, buffer)
71
- rescue EOFError
72
- return ConnectionError
73
- rescue Errno::ENOTCONN, Errno::EBADF, Errno::ECONNRESET, Errno::EPIPE
74
- raise ConnectionFailure
75
- rescue Errno::EINTR, Errno::EIO, IOError
76
- raise OperationFailure
51
+ @socket.sysread(maxlen, buffer)
52
+ rescue SystemCallError => ex
53
+ # Needed because sometimes JRUBY doesn't throw Errno::ECONNRESET as it should
54
+ # http://jira.codehaus.org/browse/JRUBY-6180
55
+ raise ConnectionFailure, ex
56
+ rescue Errno::ENOTCONN, Errno::EBADF, Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT, EOFError => ex
57
+ raise ConnectionFailure, ex
58
+ rescue Errno::EINTR, Errno::EIO, IOError => ex
59
+ raise OperationFailure, ex
77
60
  end
78
61
  else
79
62
  raise OperationTimeout
@@ -16,11 +16,11 @@
16
16
  # limitations under the License.
17
17
  # ++
18
18
 
19
+ require 'cgi'
20
+
19
21
  module Mongo
20
22
  class URIParser
21
23
 
22
- DEFAULT_PORT = 27017
23
-
24
24
  USER_REGEX = /([-.\w:]+)/
25
25
  PASS_REGEX = /([^@,]+)/
26
26
  AUTH_REGEX = /(#{USER_REGEX}:#{PASS_REGEX}@)?/
@@ -37,7 +37,7 @@ module Mongo
37
37
  SPEC_ATTRS = [:nodes, :auths]
38
38
  OPT_ATTRS = [:connect, :replicaset, :slaveok, :safe, :w, :wtimeout, :fsync, :journal, :connecttimeoutms, :sockettimeoutms, :wtimeoutms]
39
39
 
40
- OPT_VALID = {:connect => lambda {|arg| ['direct', 'replicaset'].include?(arg)},
40
+ OPT_VALID = {:connect => lambda {|arg| ['direct', 'replicaset', 'true', 'false', true, false].include?(arg)},
41
41
  :replicaset => lambda {|arg| arg.length > 0},
42
42
  :slaveok => lambda {|arg| ['true', 'false'].include?(arg)},
43
43
  :safe => lambda {|arg| ['true', 'false'].include?(arg)},
@@ -50,7 +50,7 @@ module Mongo
50
50
  :wtimeoutms => lambda {|arg| arg =~ /^\d+$/ }
51
51
  }
52
52
 
53
- OPT_ERR = {:connect => "must be 'direct' or 'replicaset'",
53
+ OPT_ERR = {:connect => "must be 'direct', 'replicaset', 'true', or 'false'",
54
54
  :replicaset => "must be a string containing the name of the replica set to connect to",
55
55
  :slaveok => "must be 'true' or 'false'",
56
56
  :safe => "must be 'true' or 'false'",
@@ -63,7 +63,7 @@ module Mongo
63
63
  :wtimeoutms => "must be an integer specifying milliseconds"
64
64
  }
65
65
 
66
- OPT_CONV = {:connect => lambda {|arg| arg},
66
+ OPT_CONV = {:connect => lambda {|arg| arg == 'false' ? false : arg}, # be sure to convert 'false' to FalseClass
67
67
  :replicaset => lambda {|arg| arg},
68
68
  :slaveok => lambda {|arg| arg == 'true' ? true : false},
69
69
  :safe => lambda {|arg| arg == 'true' ? true : false},
@@ -81,22 +81,73 @@ module Mongo
81
81
  # Parse a MongoDB URI. This method is used by Connection.from_uri.
82
82
  # Returns an array of nodes and an array of db authorizations, if applicable.
83
83
  #
84
- # Note: passwords can contain any character except for a ','.
84
+ # @note Passwords can contain any character except for ','
85
+ #
86
+ # @param [String] uri The MongoDB URI string.
87
+ # @param [Hash,nil] extra_opts Extra options. Will override anything already specified in the URI.
85
88
  #
86
89
  # @core connections
87
- def initialize(string)
88
- if string =~ /^mongodb:\/\//
89
- string = string[10..-1]
90
+ def initialize(uri, extra_opts={})
91
+ if uri.start_with?('mongodb://')
92
+ uri = uri[10..-1]
90
93
  else
91
94
  raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
92
95
  end
93
96
 
94
- hosts, opts = string.split('?')
97
+ hosts, opts = uri.split('?')
95
98
  parse_hosts(hosts)
96
- parse_options(opts)
97
- configure_connect
99
+ parse_options(opts, extra_opts)
100
+ validate_connect
101
+ end
102
+
103
+ # Create a Mongo::Connection or a Mongo::ReplSetConnection based on the URI.
104
+ #
105
+ # @note Don't confuse this with attribute getter method #connect.
106
+ #
107
+ # @return [Connection,ReplSetConnection]
108
+ def connection
109
+ if replicaset?
110
+ ReplSetConnection.new(*(nodes+[connection_options]))
111
+ else
112
+ Connection.new(host, port, connection_options)
113
+ end
114
+ end
115
+
116
+ # Whether this represents a replica set.
117
+ # @return [true,false]
118
+ def replicaset?
119
+ replicaset.is_a?(String) || nodes.length > 1
120
+ end
121
+
122
+ # Whether to immediately connect to the MongoDB node[s]. Defaults to true.
123
+ # @return [true, false]
124
+ def connect?
125
+ connect != false
126
+ end
127
+
128
+ # Whether this represents a direct connection.
129
+ #
130
+ # @note Specifying :connect => 'direct' has no effect... other than to raise an exception if other variables suggest a replicaset.
131
+ #
132
+ # @return [true,false]
133
+ def direct?
134
+ !replicaset?
135
+ end
136
+
137
+ # For direct connections, the host of the (only) node.
138
+ # @return [String]
139
+ def host
140
+ nodes[0][0]
98
141
  end
99
142
 
143
+ # For direct connections, the port of the (only) node.
144
+ # @return [Integer]
145
+ def port
146
+ nodes[0][1].to_i
147
+ end
148
+
149
+ # Options that can be passed to Mongo::Connection.new or Mongo::ReplSetConnection.new
150
+ # @return [Hash]
100
151
  def connection_options
101
152
  opts = {}
102
153
 
@@ -136,14 +187,22 @@ module Mongo
136
187
  end
137
188
 
138
189
  if @slaveok
139
- if @connect == 'direct'
190
+ if direct?
140
191
  opts[:slave_ok] = true
141
192
  else
142
193
  opts[:read] = :secondary
143
194
  end
144
195
  end
145
196
 
146
- opts[:name] = @replicaset if @replicaset
197
+ if direct?
198
+ opts[:auths] = auths
199
+ end
200
+
201
+ if replicaset.is_a?(String)
202
+ opts[:name] = replicaset
203
+ end
204
+
205
+ opts[:connect] = connect?
147
206
 
148
207
  opts
149
208
  end
@@ -167,7 +226,7 @@ module Mongo
167
226
 
168
227
  hosturis.each do |hosturi|
169
228
  # If port is present, use it, otherwise use default port
170
- host, port = hosturi.split(':') + [DEFAULT_PORT]
229
+ host, port = hosturi.split(':') + [Connection::DEFAULT_PORT]
171
230
 
172
231
  if !(port.to_s =~ /^\d+$/)
173
232
  raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
@@ -178,6 +237,10 @@ module Mongo
178
237
  @nodes << [host, port]
179
238
  end
180
239
 
240
+ if @nodes.empty?
241
+ raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
242
+ end
243
+
181
244
  if uname && pwd && db
182
245
  auths << {'db_name' => db, 'username' => uname, 'password' => pwd}
183
246
  elsif uname || pwd
@@ -191,40 +254,42 @@ module Mongo
191
254
 
192
255
  # This method uses the lambdas defined in OPT_VALID and OPT_CONV to validate
193
256
  # and convert the given options.
194
- def parse_options(opts)
257
+ def parse_options(string_opts, extra_opts={})
195
258
  # initialize instance variables for available options
196
259
  OPT_VALID.keys.each { |k| instance_variable_set("@#{k}", nil) }
197
260
 
198
- return unless opts
261
+ string_opts ||= ''
199
262
 
200
- separator = opts.include?('&') ? '&' : ';'
201
- opts.split(separator).each do |attr|
202
- key, value = attr.split('=')
203
- key = key.downcase.to_sym
204
- value = value.strip.downcase
263
+ return if string_opts.empty? && extra_opts.empty?
264
+
265
+ if string_opts.include?(';') and string_opts.include?('&')
266
+ raise MongoArgumentError, "must not mix URL separators ; and &"
267
+ end
268
+
269
+ opts = CGI.parse(string_opts).inject({}) do |memo, (key, value)|
270
+ value = value.first
271
+ memo[key.downcase.to_sym] = value.strip.downcase
272
+ memo
273
+ end
274
+
275
+ opts.merge! extra_opts
276
+
277
+ opts.each do |key, value|
205
278
  if !OPT_ATTRS.include?(key)
206
279
  raise MongoArgumentError, "Invalid Mongo URI option #{key}"
207
280
  end
208
-
209
281
  if OPT_VALID[key].call(value)
210
282
  instance_variable_set("@#{key}", OPT_CONV[key].call(value))
211
283
  else
212
- raise MongoArgumentError, "Invalid value for #{key}: #{OPT_ERR[key]}"
284
+ raise MongoArgumentError, "Invalid value #{value.inspect} for #{key}: #{OPT_ERR[key]}"
213
285
  end
214
286
  end
215
287
  end
216
288
 
217
- def configure_connect
218
- if !@connect
219
- if @nodes.length > 1
220
- @connect = 'replicaset'
221
- else
222
- @connect = 'direct'
223
- end
224
- end
225
-
226
- if @connect == 'direct' && @replicaset
227
- raise MongoArgumentError, "If specifying a replica set name, please also specify that connect=replicaset"
289
+ def validate_connect
290
+ if replicaset? and @connect == 'direct'
291
+ # Make sure the user doesn't specify something contradictory
292
+ raise MongoArgumentError, "connect=direct conflicts with setting a replicaset name"
228
293
  end
229
294
  end
230
295
  end