** Parses a Mongo Connection URL into known options.
**
** If user credentials are supplied, they are used as default authentication for each connection.
**
** The following URL options are supported:
**
** - 'ssl'
** - 'tls'
**
** - 'connectTimeoutMS'
** - 'socketTimeoutMS'
**
** - 'compressors'
** - 'zlibCompressionLevel'
**
** - 'minPoolSize'
** - 'maxPoolSize'
** - 'waitQueueTimeoutMS'
** - 'maxIdleTimeMS'
**
** - 'w'
** - 'wtimeoutMS'
** - 'journal'
**
** - 'authSource'
** - 'authMechanism'
** - 'authMechanismProperties'
**
** - 'appName'
** - 'retryWrites'
** - 'retryReads'
**
** - 'disableTxns' *(unofficial)*
**
** URL examples:
** - 'mongodb://username:password@example1.com/database?maxPoolSize=50'
** - 'mongodb://example2.com?minPoolSize=10&maxPoolSize=50&ssl=true'
**
** See `https://www.mongodb.com/docs/manual/reference/connection-string/`.
const class MongoConnUrl {
private const Log log := MongoConnUrl#.pod.log
** The original URL this class was initialised with.
** May contain authentication details.
**
** mongodb://username:password@example1.com/puppies?maxPoolSize=50
const Uri connectionUrl
** The default database name - taken from the path.
**
** mongodb://example1.com/<database>
const Str? dbName
** An "unofficial" option that can be useful for development.
**
** Transactions CANNOT be run against standalone MongoDB servers (MongoDB raises errors).
**
** This option switches transactions off, and instead executes the txnFn outside of a transaction.
** This means the same app code may be used in both standalone and clustered environments.
**
** mongodb://example.com/puppies?disableTxns=true
**
** This option exists because MongoDB does NOT provide a sure-fire way of identifying standalone instances.
** Plus, automatically disabling txns could be dangerous and give the user a false sense of security.
const Bool disableTxns
// ---- TLS Options ----
** Specifies a TLS / SSL connection. Set to 'true' for Mongo Atlas databases.
**
** Defaults to 'false'.
**
** mongodb://example.com/puppies?tls=true
** mongodb://example.com/puppies?ssl=true
const Bool tls := false
// ---- Timeout Options ----
** The amount of time to attempt a socket connection before timing out.
** If 'null' (the default) then a system timeout is used.
**
** mongodb://example.com/puppies?connectTimeoutMS=25000
**
** Equates to `inet::SocketOptions.connectTimeout`.
const Duration? connectTimeout
** The amount of time to attempt a send or receive on a socket before timing out.
** 'null' (the default) indicates an infinite timeout.
**
** mongodb://example.com/puppies?socketTimeoutMS=25000
**
** Equates to `inet::SocketOptions.receiveTimeout`.
const Duration? socketTimeout
// ---- Compression Options ----
** A list of compressors, as understood by this driver and presented to the MongoDB server.
** Any options supplied to the MongoURL and not understood by this driver will **not** be present in this list.
**
** mongodb://example.com/puppies?compressors=snappy,zlib
**
** Mongo understands 'snappy, 'zlib', 'zstd', but currently this driver ONLY understands 'zlib'.
**
** This option may be used to disable wire compression, by suppling an empty list.
**
** mongodb://example.com/puppies?compressors=
**
** If not defined, this defaults to '["zlib"]'.
const Str[] compressors
** The compression level (0 - 9) to use with zlib (0 = No compression, 1 = Best speed, 9 = Best compression).
**
** 'null' indicates a default value will be used.
**
** mongodb://example.com/puppies?zlibCompressionLevel=8
const Int? zlibCompressionLevel
// ---- Connection Pool Options ----
** The minimum number of database connections the pool should keep open.
**
** Defaults to 1.
**
** mongodb://example.com/puppies?minPoolSize=50
const Int minPoolSize := 1
** The maximum number of database connections the pool is allowed to open.
** This is the maximum number of concurrent users you expect your application to have.
**
** Defaults to 10.
**
** mongodb://example.com/puppies?maxPoolSize=10
const Int maxPoolSize := 10
** The maximum time a thread can wait for a connection to become available.
**
** Defaults to 12 seconds.
**
** mongodb://example.com/puppies?waitQueueTimeoutMS=12000
const Duration waitQueueTimeout := 12sec
** The maximum time a connection can remain idle in the pool before being removed and
** closed. (This does NOT override minPoolSize.)
**
** This helps ease connection throttling during bursts of activity.
**
** Defaults to 10sec.
**
** mongodb://example.com/puppies?maxIdleTimeMS=15000
const Duration maxIdleTime := 10sec
// ---- Write Concern Options ----
** The default write concern for all write operations.
** Set by specifying the 'w', 'wtimeoutMS' and 'journal' connection string options.
**
** mongodb://username:password@example1.com/puppies?w=1&wtimeout=0&j=false
const [Str:Obj?]? writeConcern
// ---- Authentication Options ----
** The credentials (if any) used to authenticate connections against MongoDB.
const MongoCreds? mongoCreds
** The auth mechanisms used for authenticating connections.
const Str:MongoAuthMech authMechs := [
"SCRAM-SHA-1" : MongoAuthScramSha1(),
]
// ---- Miscellaneous Configuration ----
** The application name this client identifies itself to the MongoDB server as.
** Used by MongoDB when logging.
**
** mongodb://example.com/puppies?appName=WattsApp
const Str? appName
** An option to **turn off** retryable reads.
** (Defaults to 'true').
**
** mongodb://example.com/puppies?retryReads=false
const Bool retryReads
** An option to **turn off** retryable writes.
** (Defaults to 'true').
**
** mongodb://example.com/puppies?retryWrites=false
const Bool retryWrites
** Parses a Mongo Connection URL.
new fromUrl(Uri connectionUrl) {
if (connectionUrl.scheme != "mongodb")
throw ArgErr("Mongo connection URIs must start with the scheme 'mongodb://' - ${connectionUrl}")
mongoUrl := connectionUrl
this.connectionUrl = connectionUrl
this.minPoolSize = mongoUrl.query["minPoolSize"]?.toInt ?: minPoolSize
this.maxPoolSize = mongoUrl.query["maxPoolSize"]?.toInt ?: maxPoolSize
waitQueueTimeoutMs := mongoUrl.query["waitQueueTimeoutMS"]?.toInt
connectTimeoutMs := mongoUrl.query["connectTimeoutMS"]?.toInt
socketTimeoutMs := mongoUrl.query["socketTimeoutMS"]?.toInt
maxIdleTimeMs := mongoUrl.query["maxIdleTimeMS"]?.toInt
w := mongoUrl.query["w"]
wtimeoutMs := mongoUrl.query["wtimeoutMS"]?.toInt
journal := mongoUrl.query["journal"]?.toBool
this.tls =(mongoUrl.query["tls"]?.toBool ?: mongoUrl.query["ssl"]?.toBool) ?: false
authSource := mongoUrl.query["authSource"]?.trimToNull
authMech := mongoUrl.query["authMechanism"]?.trimToNull
authMechProps := mongoUrl.query["authMechanismProperties"]?.trimToNull
appName := mongoUrl.query["appName"]?.trimToNull
compressors := mongoUrl.query["compressors"]?.split(',')?.exclude { it.isEmpty || it.size > 64 } as Str[]
zlibCompressionLevel := mongoUrl.query["zlibCompressionLevel"]?.toInt(10, false)
this.retryWrites = mongoUrl.query["retryWrites"] != "false"
this.retryReads = mongoUrl.query["retryReads"] != "false"
this.disableTxns = mongoUrl.query["disableTxns"] == "true"
if (minPoolSize < 0)
throw ArgErr(errMsg_intTooSmall("minPoolSize", "0", minPoolSize, mongoUrl))
if (maxPoolSize < 1)
throw ArgErr(errMsg_intTooSmall("maxPoolSize", "1", maxPoolSize, mongoUrl))
if (minPoolSize > maxPoolSize)
throw ArgErr(errMsg_badMinMaxConnectionSize(minPoolSize, maxPoolSize, mongoUrl))
if (waitQueueTimeoutMs != null && waitQueueTimeoutMs < 0)
throw ArgErr(errMsg_intTooSmall("waitQueueTimeoutMS", "0", waitQueueTimeoutMs, mongoUrl))
if (connectTimeoutMs != null && connectTimeoutMs < 0)
throw ArgErr(errMsg_intTooSmall("connectTimeoutMS", "0", connectTimeoutMs, mongoUrl))
if (socketTimeoutMs != null && socketTimeoutMs < 0)
throw ArgErr(errMsg_intTooSmall("socketTimeoutMS", "0", socketTimeoutMs, mongoUrl))
if (maxIdleTimeMs != null && maxIdleTimeMs < 0)
throw ArgErr(errMsg_intTooSmall("maxIdleTimeMS", "0", maxIdleTimeMs, mongoUrl))
if (wtimeoutMs != null && wtimeoutMs < 0)
throw ArgErr(errMsg_intTooSmall("wtimeoutMS", "0", wtimeoutMs, mongoUrl))
if (zlibCompressionLevel != null && zlibCompressionLevel < -1)
throw ArgErr(errMsg_intTooSmall("zlibCompressionLevel", "-1", zlibCompressionLevel, mongoUrl))
if (zlibCompressionLevel != null && zlibCompressionLevel > 9)
throw ArgErr(errMsg_intTooLarge("zlibCompressionLevel", "9", zlibCompressionLevel, mongoUrl))
if (waitQueueTimeoutMs != null) waitQueueTimeout = 1ms * waitQueueTimeoutMs
if (connectTimeoutMs != null) connectTimeout = 1ms * connectTimeoutMs
if (socketTimeoutMs != null) socketTimeout = 1ms * socketTimeoutMs
if (maxIdleTimeMs != null) maxIdleTime = 1ms * maxIdleTimeMs
// authSource trumps defaultauthdb
database := authSource ?: mongoUrl.pathStr.trimToNull
username := mongoUrl.userInfo?.split(':')?.getSafe(0)?.trimToNull
password := mongoUrl.userInfo?.split(':')?.getSafe(1)?.trimToNull
if ((username == null).xor(password == null))
throw ArgErr(errMsg_badUsernamePasswordCombo(username, password, mongoUrl))
if (database != null && database.startsWith("/"))
database = database[1..-1].trimToNull
if (username != null && password != null && database == null)
database = "admin"
if (username == null && password == null) // a default database has no meaning without credentials
database = null
this.dbName = mongoUrl.pathOnly.relTo(`/`).encode.trimToNull
if (authMech != null) {
props := Str:Obj?[:]
props.ordered = true
authMechProps?.split(',')?.each |pair| {
if (pair.size > 0) {
key := pair
val := null
idx := pair.index(":")
if (idx != null) {
key = pair[0..<idx]
val = pair[idx+1..-1]
}
props[key] = val
}
}
this.mongoCreds = MongoCreds {
it.mechanism = authMech
it.source = database
it.username = username
it.password = password
it.props = props
}
}
// set some default creds
if (this.mongoCreds == null && username != null && password != null)
this.mongoCreds = MongoCreds {
it.mechanism = "SCRAM-SHA-1"
it.source = database
it.username = username
it.password = password
}
writeConcern := Str:Obj?[:]
writeConcern.ordered = true
if (w != null)
writeConcern["w"] = Int.fromStr(w, 10, false) != null ? w.toInt : w
if (wtimeoutMs != null)
writeConcern["wtimeout"] = wtimeoutMs
if (journal != null)
writeConcern["j"] = journal
if (writeConcern.size > 0)
this.writeConcern = writeConcern
if (appName != null) {
// appName cannot exceed 128 bytes
// https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#limitations
// I know this check is for chars but I'm guessing the reasoning is to just prevent inappropriate hacking attempts
if (appName.size > 128)
appName = appName[0..<128]
this.appName = appName
}
validCompressors := Str["zlib"]
this.compressors = compressors?.findAll { validCompressors.contains(it) } ?: validCompressors
if (zlibCompressionLevel == -1)
zlibCompressionLevel = null
this.zlibCompressionLevel = zlibCompressionLevel
query := mongoUrl.query.rw
query.remove("minPoolSize")
query.remove("maxPoolSize")
query.remove("waitQueueTimeoutMS")
query.remove("connectTimeoutMS")
query.remove("socketTimeoutMS")
query.remove("maxIdleTimeMS")
query.remove("w")
query.remove("wtimeoutMS")
query.remove("journal")
query.remove("ssl")
query.remove("tls")
query.remove("authSource")
query.remove("authMechanism")
query.remove("authMechanismProperties")
query.remove("appName")
query.remove("compressors")
query.remove("zlibCompressionLevel")
query.remove("retryWrites")
query.remove("retryReads")
query.remove("disableTxns")
query.each |val, key| {
log.warn("Unknown option in Mongo connection URL: ${key}=${val}")
}
}
private static Str errMsg_intTooSmall(Str what, Str min, Int val, Uri mongoUrl) {
"$what must be >= $min, val=$val, uri=$mongoUrl"
}
private static Str errMsg_intTooLarge(Str what, Str min, Int val, Uri mongoUrl) {
"$what must be <= $min, val=$val, uri=$mongoUrl"
}
private static Str errMsg_badMinMaxConnectionSize(Int min, Int max, Uri mongoUrl) {
"Minimum number of connections must not be greater than the maximum! min=$min, max=$max, url=$mongoUrl"
}
private static Str errMsg_unknownAuthMechanism(Str mechanism, Str[] supportedMechanisms) {
"Unknown authentication mechanism '${mechanism}', only the following are currently supported: " + supportedMechanisms.join(", ")
}
private static Str errMsg_badUsernamePasswordCombo(Str? username, Str? password, Uri mongoUrl) {
"Either both the username and password should be provided, or neither. username=$username, password=$password, url=$mongoUrl"
}
}