mongo 1.6.2 → 1.6.4

Sign up to get free protection for your applications and to get access to all the features.
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