diff --git a/README.md b/README.md
index 874dafc..b0a6c09 100644
--- a/README.md
+++ b/README.md
@@ -173,6 +173,14 @@ Certain Ruby application servers work by forking, and it has long been necessary
re-establish the child process's connection to the database after fork. But with the release
of v1.3.0, the Ruby driver detects forking and reconnects automatically.
+## Environment variable `MONGODB_URI`
+
+`Mongo::Connection.new` and `Mongo::ReplSetConnection.new` will use ENV["MONGODB_URI"]
if no other args are provided.
+
+The URI must fit this specification:
+
+ mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
+
## String Encoding
The BSON ("Binary JSON") format used to communicate with Mongo requires that
diff --git a/lib/mongo/connection.rb b/lib/mongo/connection.rb
index e8eaf58..5f48833 100644
--- a/lib/mongo/connection.rb
+++ b/lib/mongo/connection.rb
@@ -32,6 +32,7 @@ module Mongo
Thread.abort_on_exception = true
+ DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 27017
GENERIC_OPTS = [:ssl, :auths, :pool_size, :pool_timeout, :timeout, :op_timeout, :connect_timeout, :safe, :logger, :connect]
CONNECTION_OPTS = [:slave_ok]
@@ -44,6 +45,8 @@ module Mongo
# Create a connection to single MongoDB instance.
#
+ # If no args are provided, it will check ENV["MONGODB_URI"]
.
+ #
# You may specify whether connection to slave is permitted.
# In all cases, the default host is "localhost" and the default port is 27017.
#
@@ -76,7 +79,7 @@ module Mongo
# connection attempt.
# @option opts [Boolean] :ssl (false) If true, create the connection to the server using SSL.
#
- # @example localhost, 27017
+ # @example localhost, 27017 (or ENV["MONGODB_URI"]
if available)
# Mongo::Connection.new
#
# @example localhost, 27017
@@ -93,9 +96,22 @@ module Mongo
# @raise [ReplicaSetConnectionError] This is raised if a replica set name is specified and the
# driver fails to connect to a replica set with that name.
#
+ # @raise [MongoArgumentError] If called with no arguments and ENV["MONGODB_URI"]
implies a replica set.
+ #
# @core self.connections
def initialize(host=nil, port=nil, opts={})
- @host_to_try = format_pair(host, port)
+ if host.nil? and ENV.has_key?('MONGODB_URI')
+ parser = URIParser.new ENV['MONGODB_URI'], opts
+ if parser.replicaset?
+ raise MongoArgumentError, "Mongo::Connection.new called with no arguments, but ENV['MONGODB_URI'] implies a replica set."
+ end
+ opts = parser.connection_options
+ @host_to_try = [parser.host, parser.port]
+ elsif host.is_a?(String)
+ @host_to_try = [host, (port || DEFAULT_PORT).to_i]
+ else
+ @host_to_try = [DEFAULT_HOST, DEFAULT_PORT]
+ end
# Host and port of current master.
@host = @port = nil
@@ -143,8 +159,7 @@ module Mongo
def self.multi(nodes, opts={})
warn "Connection.multi is now deprecated and will be removed in v2.0. Please use ReplSetConnection.new instead."
- nodes << opts
- ReplSetConnection.new(*nodes)
+ ReplSetConnection.new(*(nodes+[opts]))
end
# Initialize a connection to MongoDB using the MongoDB URI spec:
@@ -155,21 +170,9 @@ module Mongo
# @param opts Any of the options available for Connection.new
#
# @return [Mongo::Connection, Mongo::ReplSetConnection]
- def self.from_uri(string, extra_opts={})
- uri = URIParser.new(string)
- opts = uri.connection_options
- opts.merge!(extra_opts)
-
- if uri.nodes.length == 1
- opts.merge!({:auths => uri.auths})
- Connection.new(uri.nodes[0][0], uri.nodes[0][1], opts)
- elsif uri.nodes.length > 1
- nodes = uri.nodes.clone
- nodes_with_opts = nodes << opts
- ReplSetConnection.new(*nodes_with_opts)
- else
- raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
- end
+ def self.from_uri(uri, extra_opts={})
+ parser = URIParser.new uri, extra_opts
+ parser.connection
end
# The host name used for this connection.
@@ -337,7 +340,7 @@ module Mongo
# @param [String] from_host host of the 'from' database.
# @param [String] username username for authentication against from_db (>=1.3.x).
# @param [String] password password for authentication against from_db (>=1.3.x).
- def copy_database(from, to, from_host="localhost", username=nil, password=nil)
+ def copy_database(from, to, from_host=DEFAULT_HOST, username=nil, password=nil)
oh = BSON::OrderedHash.new
oh[:copydb] = 1
oh[:fromhost] = from_host
@@ -585,23 +588,8 @@ module Mongo
write_logging_startup_message
end
- should_connect = opts.fetch(:connect, true)
- connect if should_connect
- end
-
- ## Configuration helper methods
-
- # Returns a host-port pair.
- #
- # @return [Array]
- #
- # @private
- def format_pair(host, port)
- case host
- when String
- [host, port ? port.to_i : DEFAULT_PORT]
- when nil
- ['localhost', DEFAULT_PORT]
+ if opts.fetch(:connect, true)
+ connect
end
end
diff --git a/lib/mongo/repl_set_connection.rb b/lib/mongo/repl_set_connection.rb
index cc3552e..017525d 100644
--- a/lib/mongo/repl_set_connection.rb
+++ b/lib/mongo/repl_set_connection.rb
@@ -29,6 +29,8 @@ module Mongo
# Create a connection to a MongoDB replica set.
#
+ # If no args are provided, it will check ENV["MONGODB_URI"]
.
+ #
# Once connected to a replica set, you can find out which nodes are primary, secondary, and
# arbiters with the corresponding accessors: Connection#primary, Connection#secondaries, and
# Connection#arbiters. This is useful if your application needs to connect manually to nodes other
@@ -78,6 +80,8 @@ module Mongo
#
# @see http://api.mongodb.org/ruby/current/file.REPLICA_SETS.html Replica sets in Ruby
#
+ # @raise [MongoArgumentError] If called with no arguments and ENV["MONGODB_URI"]
implies a direct connection.
+ #
# @raise [ReplicaSetConnectionError] This is raised if a replica set name is specified and the
# driver fails to connect to a replica set with that name.
def initialize(*args)
@@ -87,21 +91,30 @@ module Mongo
opts = {}
end
- unless args.length > 0
+ nodes = args
+
+ if nodes.empty? and ENV.has_key?('MONGODB_URI')
+ parser = URIParser.new ENV['MONGODB_URI'], opts
+ if parser.direct?
+ raise MongoArgumentError, "Mongo::ReplSetConnection.new called with no arguments, but ENV['MONGODB_URI'] implies a direct connection."
+ end
+ opts = parser.connection_options
+ nodes = parser.nodes
+ end
+
+ unless nodes.length > 0
raise MongoArgumentError, "A ReplSetConnection requires at least one seed node."
end
# This is temporary until support for the old format is dropped
- @seeds = []
- if args.first.last.is_a?(Integer)
+ if nodes.first.last.is_a?(Integer)
warn "Initiating a ReplSetConnection with seeds passed as individual [host, port] array arguments is deprecated."
warn "Please specify hosts as an array of 'host:port' strings; the old format will be removed in v2.0"
- @seeds = args
+ @seeds = nodes
else
- args.first.map do |host_port|
- seed = host_port.split(":")
- seed[1] = seed[1].to_i
- seeds << seed
+ @seeds = nodes.first.map do |host_port|
+ host, port = host_port.split(":")
+ [ host, port.to_i ]
end
end
diff --git a/lib/mongo/util/uri_parser.rb b/lib/mongo/util/uri_parser.rb
index a0154f7..2c42993 100644
--- a/lib/mongo/util/uri_parser.rb
+++ b/lib/mongo/util/uri_parser.rb
@@ -16,11 +16,11 @@
# limitations under the License.
# ++
+require 'uri'
+
module Mongo
class URIParser
- DEFAULT_PORT = 27017
-
USER_REGEX = /([-.\w:]+)/
PASS_REGEX = /([^@,]+)/
AUTH_REGEX = /(#{USER_REGEX}:#{PASS_REGEX}@)?/
@@ -37,7 +37,7 @@ module Mongo
SPEC_ATTRS = [:nodes, :auths]
OPT_ATTRS = [:connect, :replicaset, :slaveok, :safe, :w, :wtimeout, :fsync, :journal, :connecttimeoutms, :sockettimeoutms, :wtimeoutms]
- OPT_VALID = {:connect => lambda {|arg| ['direct', 'replicaset'].include?(arg)},
+ OPT_VALID = {:connect => lambda {|arg| ['direct', 'replicaset', 'true', 'false', true, false].include?(arg)},
:replicaset => lambda {|arg| arg.length > 0},
:slaveok => lambda {|arg| ['true', 'false'].include?(arg)},
:safe => lambda {|arg| ['true', 'false'].include?(arg)},
@@ -50,7 +50,7 @@ module Mongo
:wtimeoutms => lambda {|arg| arg =~ /^\d+$/ }
}
- OPT_ERR = {:connect => "must be 'direct' or 'replicaset'",
+ OPT_ERR = {:connect => "must be 'direct', 'replicaset', 'true', or 'false'",
:replicaset => "must be a string containing the name of the replica set to connect to",
:slaveok => "must be 'true' or 'false'",
:safe => "must be 'true' or 'false'",
@@ -63,7 +63,7 @@ module Mongo
:wtimeoutms => "must be an integer specifying milliseconds"
}
- OPT_CONV = {:connect => lambda {|arg| arg},
+ OPT_CONV = {:connect => lambda {|arg| arg == 'false' ? false : arg}, # be sure to convert 'false' to FalseClass
:replicaset => lambda {|arg| arg},
:slaveok => lambda {|arg| arg == 'true' ? true : false},
:safe => lambda {|arg| arg == 'true' ? true : false},
@@ -81,22 +81,73 @@ module Mongo
# Parse a MongoDB URI. This method is used by Connection.from_uri.
# Returns an array of nodes and an array of db authorizations, if applicable.
#
- # Note: passwords can contain any character except for a ','.
+ # @note Passwords can contain any character except for ','
+ #
+ # @param [String] uri The MongoDB URI string.
+ # @param [Hash,nil] extra_opts Extra options. Will override anything already specified in the URI.
#
# @core connections
- def initialize(string)
- if string =~ /^mongodb:\/\//
- string = string[10..-1]
+ def initialize(uri, extra_opts={})
+ if uri.start_with?('mongodb://')
+ uri = uri[10..-1]
else
raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
end
- hosts, opts = string.split('?')
+ hosts, opts = uri.split('?')
parse_hosts(hosts)
- parse_options(opts)
- configure_connect
+ parse_options(opts, extra_opts)
+ validate_connect
end
+ # Create a Mongo::Connection or a Mongo::ReplSetConnection based on the URI.
+ #
+ # @note Don't confuse this with attribute getter method #connect.
+ #
+ # @return [Connection,ReplSetConnection]
+ def connection
+ if replicaset?
+ ReplSetConnection.new(*(nodes+[connection_options]))
+ else
+ Connection.new(host, port, connection_options)
+ end
+ end
+
+ # Whether this represents a replica set.
+ # @return [true,false]
+ def replicaset?
+ replicaset.is_a?(String) || nodes.length > 1
+ end
+
+ # Whether to immediately connect to the MongoDB node[s]. Defaults to true.
+ # @return [true, false]
+ def connect?
+ connect != false
+ end
+
+ # Whether this represents a direct connection.
+ #
+ # @note Specifying :connect => 'direct' has no effect... other than to raise an exception if other variables suggest a replicaset.
+ #
+ # @return [true,false]
+ def direct?
+ !replicaset?
+ end
+
+ # For direct connections, the host of the (only) node.
+ # @return [String]
+ def host
+ nodes[0][0]
+ end
+
+ # For direct connections, the port of the (only) node.
+ # @return [Integer]
+ def port
+ nodes[0][1].to_i
+ end
+
+ # Options that can be passed to Mongo::Connection.new or Mongo::ReplSetConnection.new
+ # @return [Hash]
def connection_options
opts = {}
@@ -136,14 +187,22 @@ module Mongo
end
if @slaveok
- if @connect == 'direct'
+ if direct?
opts[:slave_ok] = true
else
opts[:read] = :secondary
end
end
- opts[:name] = @replicaset if @replicaset
+ if direct?
+ opts[:auths] = auths
+ end
+
+ if replicaset.is_a?(String)
+ opts[:name] = replicaset
+ end
+
+ opts[:connect] = connect?
opts
end
@@ -167,7 +226,7 @@ module Mongo
hosturis.each do |hosturi|
# If port is present, use it, otherwise use default port
- host, port = hosturi.split(':') + [DEFAULT_PORT]
+ host, port = hosturi.split(':') + [Connection::DEFAULT_PORT]
if !(port.to_s =~ /^\d+$/)
raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
@@ -178,6 +237,10 @@ module Mongo
@nodes << [host, port]
end
+ if @nodes.empty?
+ raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
+ end
+
if uname && pwd && db
auths << {'db_name' => db, 'username' => uname, 'password' => pwd}
elsif uname || pwd
@@ -191,40 +254,37 @@ module Mongo
# This method uses the lambdas defined in OPT_VALID and OPT_CONV to validate
# and convert the given options.
- def parse_options(opts)
+ def parse_options(string_opts, extra_opts={})
# initialize instance variables for available options
OPT_VALID.keys.each { |k| instance_variable_set("@#{k}", nil) }
- return unless opts
+ string_opts ||= ''
- separator = opts.include?('&') ? '&' : ';'
- opts.split(separator).each do |attr|
- key, value = attr.split('=')
- key = key.downcase.to_sym
- value = value.strip.downcase
+ return if string_opts.empty? && extra_opts.empty?
+
+ opts = URI.decode_www_form(string_opts).inject({}) do |memo, (key, value)|
+ memo[key.downcase.to_sym] = value.strip.downcase
+ memo
+ end
+
+ opts.merge! extra_opts
+
+ opts.each do |key, value|
if !OPT_ATTRS.include?(key)
raise MongoArgumentError, "Invalid Mongo URI option #{key}"
end
-
if OPT_VALID[key].call(value)
instance_variable_set("@#{key}", OPT_CONV[key].call(value))
else
- raise MongoArgumentError, "Invalid value for #{key}: #{OPT_ERR[key]}"
+ raise MongoArgumentError, "Invalid value #{value.inspect} for #{key}: #{OPT_ERR[key]}"
end
end
end
- def configure_connect
- if !@connect
- if @nodes.length > 1
- @connect = 'replicaset'
- else
- @connect = 'direct'
- end
- end
-
- if @connect == 'direct' && @replicaset
- raise MongoArgumentError, "If specifying a replica set name, please also specify that connect=replicaset"
+ def validate_connect
+ if replicaset? and @connect == 'direct'
+ # Make sure the user doesn't specify something contradictory
+ raise MongoArgumentError, "connect=direct conflicts with setting a replicaset name"
end
end
end
diff --git a/test/connection_test.rb b/test/connection_test.rb
index 6e9ab7a..bcfe773 100644
--- a/test/connection_test.rb
+++ b/test/connection_test.rb
@@ -58,6 +58,18 @@ class TestConnection < Test::Unit::TestCase
assert_equal mongo_port, con.primary_pool.port
end
+ def test_env_mongodb_uri
+ begin
+ old_mongodb_uri = ENV['MONGODB_URI']
+ ENV['MONGODB_URI'] = "mongodb://#{host_port}"
+ con = Connection.new
+ assert_equal mongo_host, con.primary_pool.host
+ assert_equal mongo_port, con.primary_pool.port
+ ensure
+ ENV['MONGODB_URI'] = old_mongodb_uri
+ end
+ end
+
def test_server_version
assert_match(/\d\.\d+(\.\d+)?/, @conn.server_version.to_s)
end
diff --git a/test/replica_sets/connect_test.rb b/test/replica_sets/connect_test.rb
index a608873..11f66af 100644
--- a/test/replica_sets/connect_test.rb
+++ b/test/replica_sets/connect_test.rb
@@ -105,6 +105,20 @@ class ConnectTest < Test::Unit::TestCase
assert @conn.is_a?(ReplSetConnection)
assert @conn.connected?
end
+
+ def test_connect_with_connection_string_in_env_var
+ begin
+ old_mongodb_uri = ENV['MONGODB_URI']
+ ENV['MONGODB_URI'] = "mongodb://#{@rs.host}:#{@rs.ports[0]},#{@rs.host}:#{@rs.ports[1]}?replicaset=#{@rs.name}"
+ silently do
+ @conn = ReplSetConnection.new
+ end
+ assert @conn.is_a?(ReplSetConnection)
+ assert @conn.connected?
+ ensure
+ ENV['MONGODB_URI'] = old_mongodb_uri
+ end
+ end
def test_connect_with_new_seed_format
@conn = ReplSetConnection.new build_seeds(3)
@@ -128,4 +142,21 @@ class ConnectTest < Test::Unit::TestCase
assert @conn.safe[:fsync]
assert @conn.read_pool
end
+
+ def test_connect_with_full_connection_string_in_env_var
+ begin
+ old_mongodb_uri = ENV['MONGODB_URI']
+ ENV['MONGODB_URI'] = "mongodb://#{@rs.host}:#{@rs.ports[0]},#{@rs.host}:#{@rs.ports[1]}?replicaset=#{@rs.name};safe=true;w=2;fsync=true;slaveok=true"
+ silently do
+ @conn = ReplSetConnection.new
+ end
+ assert @conn.is_a?(ReplSetConnection)
+ assert @conn.connected?
+ assert_equal 2, @conn.safe[:w]
+ assert @conn.safe[:fsync]
+ assert @conn.read_pool
+ ensure
+ ENV['MONGODB_URI'] = old_mongodb_uri
+ end
+ end
end
diff --git a/test/unit/connection_test.rb b/test/unit/connection_test.rb
index a4ab0d3..2f54a40 100644
--- a/test/unit/connection_test.rb
+++ b/test/unit/connection_test.rb
@@ -130,5 +130,93 @@ class ConnectionTest < Test::Unit::TestCase
end
end
end
+
+ context "initializing with ENV['MONGODB_URI']" do
+ setup do
+ @old_mongodb_uri = ENV['MONGODB_URI']
+ end
+
+ teardown do
+ ENV['MONGODB_URI'] = @old_mongodb_uri
+ end
+
+ should "parse a simple uri" do
+ ENV['MONGODB_URI'] = "mongodb://localhost?connect=false"
+ @conn = Connection.new
+ assert_equal ['localhost', 27017], @conn.host_to_try
+ end
+
+ should "allow a complex host names" do
+ host_name = "foo.bar-12345.org"
+ ENV['MONGODB_URI'] = "mongodb://#{host_name}?connect=false"
+ @conn = Connection.new
+ assert_equal [host_name, 27017], @conn.host_to_try
+ end
+
+ should "allow db without username and password" do
+ host_name = "foo.bar-12345.org"
+ ENV['MONGODB_URI'] = "mongodb://#{host_name}/foo?connect=false"
+ @conn = Connection.new
+ assert_equal [host_name, 27017], @conn.host_to_try
+ end
+
+ should "set safe options on connection" do
+ host_name = "localhost"
+ opts = "safe=true&w=2&wtimeoutMS=1000&fsync=true&journal=true&connect=false"
+ ENV['MONGODB_URI'] = "mongodb://#{host_name}/foo?#{opts}"
+ @conn = Connection.new
+ assert_equal({:w => 2, :wtimeout => 1000, :fsync => true, :j => true}, @conn.safe)
+ end
+
+ should "set timeout options on connection" do
+ host_name = "localhost"
+ opts = "connectTimeoutMS=1000&socketTimeoutMS=5000&connect=false"
+ ENV['MONGODB_URI'] = "mongodb://#{host_name}/foo?#{opts}"
+ @conn = Connection.new
+ assert_equal 1, @conn.connect_timeout
+ assert_equal 5, @conn.op_timeout
+ end
+
+ should "parse a uri with a hyphen & underscore in the username or password" do
+ ENV['MONGODB_URI'] = "mongodb://hyphen-user_name:p-s_s@localhost:27017/db?connect=false"
+ @conn = Connection.new
+ assert_equal ['localhost', 27017], @conn.host_to_try
+ auth_hash = { 'db_name' => 'db', 'username' => 'hyphen-user_name', "password" => 'p-s_s' }
+ assert_equal auth_hash, @conn.auths[0]
+ end
+
+ should "attempt to connect" do
+ TCPSocket.stubs(:new).returns(new_mock_socket)
+ ENV['MONGODB_URI'] = "mongodb://localhost?connect=false" # connect=false ??
+ @conn = Connection.new
+
+ admin_db = new_mock_db
+ admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1})
+ @conn.expects(:[]).with('admin').returns(admin_db)
+ @conn.connect
+ end
+
+ should "raise an error on invalid uris" do
+ ENV['MONGODB_URI'] = "mongo://localhost"
+ assert_raise MongoArgumentError do
+ Connection.new
+ end
+
+ ENV['MONGODB_URI'] = "mongodb://localhost:abc"
+ assert_raise MongoArgumentError do
+ Connection.new
+ end
+ end
+
+ should "require all of username, if password and db are specified" do
+ ENV['MONGODB_URI'] = "mongodb://kyle:jones@localhost/db?connect=false"
+ assert Connection.new
+
+ ENV['MONGODB_URI'] = "mongodb://kyle:password@localhost"
+ assert_raise MongoArgumentError do
+ Connection.new
+ end
+ end
+ end
end
end
diff --git a/test/uri_test.rb b/test/uri_test.rb
index e833cfb..806c251 100644
--- a/test/uri_test.rb
+++ b/test/uri_test.rb
@@ -63,9 +63,10 @@ class URITest < Test::Unit::TestCase
assert_equal "test", parser.auths[1]["db_name"]
end
- def test_opts_basic
+ def test_opts_with_semincolon_separator
parser = Mongo::URIParser.new('mongodb://localhost:27018?connect=direct;slaveok=true;safe=true')
assert_equal 'direct', parser.connect
+ assert parser.direct?
assert parser.slaveok
assert parser.safe
end
@@ -73,10 +74,17 @@ class URITest < Test::Unit::TestCase
def test_opts_with_amp_separator
parser = Mongo::URIParser.new('mongodb://localhost:27018?connect=direct&slaveok=true&safe=true')
assert_equal 'direct', parser.connect
+ assert parser.direct?
assert parser.slaveok
assert parser.safe
end
+ def test_opts_made_invalid_by_mixed_separators
+ assert_raise_error ArgumentError, "invalid data of application/x-www-form-urlencoded (replicaset=foo;bar&slaveok=true&safe=true)" do
+ Mongo::URIParser.new('mongodb://localhost:27018?replicaset=foo;bar&slaveok=true&safe=true')
+ end
+ end
+
def test_opts_safe
parser = Mongo::URIParser.new('mongodb://localhost:27018?safe=true;w=2;journal=true;fsync=true;wtimeoutMS=200')
assert parser.safe
@@ -93,12 +101,16 @@ class URITest < Test::Unit::TestCase
end
def test_opts_replica_set
- assert_raise_error MongoArgumentError, "specify that connect=replicaset" do
- Mongo::URIParser.new('mongodb://localhost:27018?replicaset=foo')
- end
parser = Mongo::URIParser.new('mongodb://localhost:27018?connect=replicaset;replicaset=foo')
assert_equal 'foo', parser.replicaset
assert_equal 'replicaset', parser.connect
+ assert parser.replicaset?
+ end
+
+ def test_opts_conflicting_replica_set
+ assert_raise_error MongoArgumentError, "connect=direct conflicts with setting a replicaset name" do
+ Mongo::URIParser.new('mongodb://localhost:27018?connect=direct;replicaset=foo')
+ end
end
def test_case_insensitivity