8
8
"net"
9
9
"net/url"
10
10
"runtime"
11
+ "sort"
11
12
"strconv"
12
13
"strings"
13
14
"time"
@@ -192,9 +193,32 @@ func (opt *Options) clone() *Options {
192
193
// Scheme is required.
193
194
// There are two connection types: by tcp socket and by unix socket.
194
195
// Tcp connection:
195
- // redis://<user>:<password>@<host>:<port>/<db_number>
196
+ // redis://<user>:<password>@<host>:<port>/<db_number>
196
197
// Unix connection:
197
198
// unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
199
+ // Most Option fields can be set using query parameters, with the following restrictions:
200
+ // - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
201
+ // - only scalar type fields are supported (bool, int, time.Duration)
202
+ // - for time.Duration fields, values must be a valid input for time.ParseDuration();
203
+ // additionally a plain integer as value (i.e. without unit) is intepreted as seconds
204
+ // - to disable a duration field, use value less than or equal to 0; to use the default
205
+ // value, leave the value blank or remove the parameter
206
+ // - only the last value is interpreted if a parameter is given multiple times
207
+ // - fields "network", "addr", "username" and "password" can only be set using other
208
+ // URL attributes (scheme, host, userinfo, resp.), query paremeters using these
209
+ // names will be treated as unknown parameters
210
+ // - unknown parameter names will result in an error
211
+ // Examples:
212
+ // redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
213
+ // is equivalent to:
214
+ // &Options{
215
+ // Network: "tcp",
216
+ // Addr: "localhost:6789",
217
+ // DB: 1, // path "/3" was overridden by "&db=1"
218
+ // DialTimeout: 3 * time.Second, // no time unit = seconds
219
+ // ReadTimeout: 6 * time.Second,
220
+ // MaxRetries: 2,
221
+ // }
198
222
func ParseURL (redisURL string ) (* Options , error ) {
199
223
u , err := url .Parse (redisURL )
200
224
if err != nil {
@@ -216,10 +240,6 @@ func setupTCPConn(u *url.URL) (*Options, error) {
216
240
217
241
o .Username , o .Password = getUserPassword (u )
218
242
219
- if len (u .Query ()) > 0 {
220
- return nil , errors .New ("redis: no options supported" )
221
- }
222
-
223
243
h , p , err := net .SplitHostPort (u .Host )
224
244
if err != nil {
225
245
h = u .Host
@@ -250,7 +270,7 @@ func setupTCPConn(u *url.URL) (*Options, error) {
250
270
o .TLSConfig = & tls.Config {ServerName : h }
251
271
}
252
272
253
- return o , nil
273
+ return setupConnParams ( u , o )
254
274
}
255
275
256
276
func setupUnixConn (u * url.URL ) (* Options , error ) {
@@ -262,19 +282,122 @@ func setupUnixConn(u *url.URL) (*Options, error) {
262
282
return nil , errors .New ("redis: empty unix socket path" )
263
283
}
264
284
o .Addr = u .Path
265
-
266
285
o .Username , o .Password = getUserPassword (u )
286
+ return setupConnParams (u , o )
287
+ }
267
288
268
- dbStr := u .Query ().Get ("db" )
269
- if dbStr == "" {
270
- return o , nil // if database is not set, connect to 0 db.
289
+ type queryOptions struct {
290
+ q url.Values
291
+ err error
292
+ }
293
+
294
+ func (o * queryOptions ) string (name string ) string {
295
+ vs := o .q [name ]
296
+ if len (vs ) == 0 {
297
+ return ""
271
298
}
299
+ delete (o .q , name ) // enable detection of unknown parameters
300
+ return vs [len (vs )- 1 ]
301
+ }
272
302
273
- db , err := strconv .Atoi (dbStr )
274
- if err != nil {
275
- return nil , fmt .Errorf ("redis: invalid database number: %w" , err )
303
+ func (o * queryOptions ) int (name string ) int {
304
+ s := o .string (name )
305
+ if s == "" {
306
+ return 0
307
+ }
308
+ i , err := strconv .Atoi (s )
309
+ if err == nil {
310
+ return i
311
+ }
312
+ if o .err == nil {
313
+ o .err = fmt .Errorf ("redis: invalid %s number: %s" , name , err )
314
+ }
315
+ return 0
316
+ }
317
+
318
+ func (o * queryOptions ) duration (name string ) time.Duration {
319
+ s := o .string (name )
320
+ if s == "" {
321
+ return 0
322
+ }
323
+ // try plain number first
324
+ if i , err := strconv .Atoi (s ); err == nil {
325
+ if i <= 0 {
326
+ // disable timeouts
327
+ return - 1
328
+ }
329
+ return time .Duration (i ) * time .Second
330
+ }
331
+ dur , err := time .ParseDuration (s )
332
+ if err == nil {
333
+ return dur
334
+ }
335
+ if o .err == nil {
336
+ o .err = fmt .Errorf ("redis: invalid %s duration: %w" , name , err )
337
+ }
338
+ return 0
339
+ }
340
+
341
+ func (o * queryOptions ) bool (name string ) bool {
342
+ switch s := o .string (name ); s {
343
+ case "true" , "1" :
344
+ return true
345
+ case "false" , "0" , "" :
346
+ return false
347
+ default :
348
+ if o .err == nil {
349
+ o .err = fmt .Errorf ("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q" , name , s )
350
+ }
351
+ return false
352
+ }
353
+ }
354
+
355
+ func (o * queryOptions ) remaining () []string {
356
+ if len (o .q ) == 0 {
357
+ return nil
358
+ }
359
+ keys := make ([]string , 0 , len (o .q ))
360
+ for k := range o .q {
361
+ keys = append (keys , k )
362
+ }
363
+ sort .Strings (keys )
364
+ return keys
365
+ }
366
+
367
+ // setupConnParams converts query parameters in u to option value in o.
368
+ func setupConnParams (u * url.URL , o * Options ) (* Options , error ) {
369
+ q := queryOptions {q : u .Query ()}
370
+
371
+ // compat: a future major release may use q.int("db")
372
+ if tmp := q .string ("db" ); tmp != "" {
373
+ db , err := strconv .Atoi (tmp )
374
+ if err != nil {
375
+ return nil , fmt .Errorf ("redis: invalid database number: %w" , err )
376
+ }
377
+ o .DB = db
378
+ }
379
+
380
+ o .MaxRetries = q .int ("max_retries" )
381
+ o .MinRetryBackoff = q .duration ("min_retry_backoff" )
382
+ o .MaxRetryBackoff = q .duration ("max_retry_backoff" )
383
+ o .DialTimeout = q .duration ("dial_timeout" )
384
+ o .ReadTimeout = q .duration ("read_timeout" )
385
+ o .WriteTimeout = q .duration ("write_timeout" )
386
+ o .PoolFIFO = q .bool ("pool_fifo" )
387
+ o .PoolSize = q .int ("pool_size" )
388
+ o .MinIdleConns = q .int ("min_idle_conns" )
389
+ o .MaxConnAge = q .duration ("max_conn_age" )
390
+ o .PoolTimeout = q .duration ("pool_timeout" )
391
+ o .IdleTimeout = q .duration ("idle_timeout" )
392
+ o .IdleCheckFrequency = q .duration ("idle_check_frequency" )
393
+ if q .err != nil {
394
+ return nil , q .err
395
+ }
396
+
397
+ // any parameters left?
398
+ if r := q .remaining (); len (r ) > 0 {
399
+ return nil , fmt .Errorf ("redis: unexpected option: %s" , strings .Join (r , ", " ))
276
400
}
277
- o .DB = db
278
401
279
402
return o , nil
280
403
}
0 commit comments