File: options_mapper.rb

package info (click to toggle)
ruby-mongo 2.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 14,764 kB
  • sloc: ruby: 108,806; makefile: 5; sh: 2
file content (868 lines) | stat: -rw-r--r-- 28,177 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
# frozen_string_literal: true
# rubocop:todo all

# Copyright (C) 2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Mongo
  class URI

    # Performs mapping between URI options and Ruby options.
    #
    # This class contains:
    #
    # - The mapping defining how URI options are converted to Ruby options.
    # - The mapping from downcased URI option names to canonical-cased URI
    #   option names.
    # - Methods to perform conversion of URI option values to Ruby option
    #   values (the convert_* methods). These generally warn and return nil
    #   when input given is invalid.
    # - Methods to perform conversion of Ruby option values to standardized
    #   MongoClient options (revert_* methods). These assume the input is valid
    #   and generally do not perform validation.
    #
    # URI option names are case insensitive. Ruby options are specified as
    # symbols (though in Client options use indifferent access).
    #
    # @api private
    class OptionsMapper

      include Loggable

      # Instantates the options mapper.
      #
      # @option opts [ Logger ] :logger A custom logger to use.
      def initialize(**opts)
        @options = opts
      end

      # @return [ Hash ] The options.
      attr_reader :options

      # Adds an option to the uri options hash.
      #
      #   Acquires a target for the option based on group.
      #   Transforms the value.
      #   Merges the option into the target.
      #
      # @param [ String ] key URI option name.
      # @param [ String ] value The value of the option.
      # @param [ Hash ] uri_options The base option target.
      def add_uri_option(key, value, uri_options)
        strategy = URI_OPTION_MAP[key.downcase]
        if strategy.nil?
          log_warn("Unsupported URI option '#{key}' on URI '#{@string}'. It will be ignored.")
          return
        end

        group = strategy[:group]
        target = if group
          uri_options[group] || {}
        else
          uri_options
        end
        value = apply_transform(key, value, strategy[:type])
        # Sometimes the value here would be nil, for example if we are processing
        # read preference tags or auth mechanism properties and all of the
        # data within is invalid. Ignore such options.
        unless value.nil?
          merge_uri_option(target, value, strategy[:name])
        end

        if group && !target.empty? && !uri_options.key?(group)
          uri_options[group] = target
        end
      end

      def smc_to_ruby(opts)
        uri_options = {}

        opts.each do |key, value|
          strategy = URI_OPTION_MAP[key.downcase]
          if strategy.nil?
            log_warn("Unsupported URI option '#{key}' on URI '#{@string}'. It will be ignored.")
            return
          end

          group = strategy[:group]
          target = if group
            uri_options[group] || {}
          else
            uri_options
          end

          value = apply_transform(key, value, strategy[:type])
          # Sometimes the value here would be nil, for example if we are processing
          # read preference tags or auth mechanism properties and all of the
          # data within is invalid. Ignore such options.
          unless value.nil?
            merge_uri_option(target, value, strategy[:name])
          end

          if group && !target.empty? && !uri_options.key?(group)
            uri_options[group] = target
          end
        end

        uri_options
      end

      # Converts Ruby options provided to "standardized MongoClient options".
      #
      # @param [ Hash ] opts Ruby options to convert.
      #
      # @return [ Hash ] Standardized MongoClient options.
      def ruby_to_smc(opts)
        rv = {}
        URI_OPTION_MAP.each do |uri_key, spec|
          if spec[:group]
            v = opts[spec[:group]]
            v = v && v[spec[:name]]
          else
            v = opts[spec[:name]]
          end
          unless v.nil?
            if type = spec[:type]
              v = send("revert_#{type}", v)
            end
            canonical_key = URI_OPTION_CANONICAL_NAMES[uri_key]
            unless canonical_key
              raise ArgumentError, "Option #{uri_key} is not known"
            end
            rv[canonical_key] = v
          end
        end
        # For options that default to true, remove the value if it is true.
        %w(retryReads retryWrites).each do |k|
          if rv[k]
            rv.delete(k)
          end
        end
        # Remove auth source when it is $external for mechanisms that default
        # (or require) that auth source.
        if %w(MONGODB-AWS).include?(rv['authMechanism']) && rv['authSource'] == '$external'
          rv.delete('authSource')
        end
        # ssl and tls are aliases, remove ssl ones
        rv.delete('ssl')
        # TODO remove authSource if it is the same as the database,
        # requires this method to know the database specified in the client.
        rv
      end

      # Converts Ruby options provided to their representation in a URI string.
      #
      # @param [ Hash ] opts Ruby options to convert.
      #
      # @return [ Hash ] URI string hash.
      def ruby_to_string(opts)
        rv = {}
        URI_OPTION_MAP.each do |uri_key, spec|
          if spec[:group]
            v = opts[spec[:group]]
            v = v && v[spec[:name]]
          else
            v = opts[spec[:name]]
          end
          unless v.nil?
            if type = spec[:type]
              v = send("stringify_#{type}", v)
            end
            canonical_key = URI_OPTION_CANONICAL_NAMES[uri_key]
            unless canonical_key
              raise ArgumentError, "Option #{uri_key} is not known"
            end
            rv[canonical_key] = v
          end
        end
        # For options that default to true, remove the value if it is true.
        %w(retryReads retryWrites).each do |k|
          if rv[k]
            rv.delete(k)
          end
        end
        # Remove auth source when it is $external for mechanisms that default
        # (or require) that auth source.
        if %w(MONGODB-AWS).include?(rv['authMechanism']) && rv['authSource'] == '$external'
          rv.delete('authSource')
        end
        # ssl and tls are aliases, remove ssl ones
        rv.delete('ssl')
        # TODO remove authSource if it is the same as the database,
        # requires this method to know the database specified in the client.
        rv
      end

      private

      # Applies URI value transformation by either using the default cast
      # or a transformation appropriate for the given type.
      #
      # @param [ String ] key URI option name.
      # @param [ String ] value The value to be transformed.
      # @param [ Symbol ] type The transform method.
      def apply_transform(key, value, type)
        if type
          send("convert_#{type}", key, value)
        else
          value
        end
      end

      # Merges a new option into the target.
      #
      # If the option exists at the target destination the merge will
      # be an addition.
      #
      # Specifically required to append an additional tag set
      # to the array of tag sets without overwriting the original.
      #
      # @param [ Hash ] target The destination.
      # @param [ Object ] value The value to be merged.
      # @param [ Symbol ] name The name of the option.
      def merge_uri_option(target, value, name)
        if target.key?(name)
          if REPEATABLE_OPTIONS.include?(name)
            target[name] += value
          else
            log_warn("Repeated option key: #{name}.")
          end
        else
          target.merge!(name => value)
        end
      end

      # Hash for storing map of URI option parameters to conversion strategies
      URI_OPTION_MAP = {}

      # @return [ Hash<String, String> ] Map from lowercased to canonical URI
      #   option names.
      URI_OPTION_CANONICAL_NAMES = {}

      # Simple internal dsl to register a MongoDB URI option in the URI_OPTION_MAP.
      #
      # @param [ String ] uri_key The MongoDB URI option to register.
      # @param [ Symbol ] name The name of the option in the driver.
      # @param [ Hash ] extra Extra options.
      #   * :group [ Symbol ] Nested hash where option will go.
      #   * :type [ Symbol ] Name of function to transform value.
      def self.uri_option(uri_key, name, **extra)
        URI_OPTION_MAP[uri_key.downcase] = { name: name }.update(extra)
        URI_OPTION_CANONICAL_NAMES[uri_key.downcase] = uri_key
      end

      # Replica Set Options
      uri_option 'replicaSet', :replica_set

      # Timeout Options
      uri_option 'connectTimeoutMS', :connect_timeout, type: :ms
      uri_option 'socketTimeoutMS', :socket_timeout, type: :ms
      uri_option 'serverSelectionTimeoutMS', :server_selection_timeout, type: :ms
      uri_option 'localThresholdMS', :local_threshold, type: :ms
      uri_option 'heartbeatFrequencyMS', :heartbeat_frequency, type: :ms
      uri_option 'maxIdleTimeMS', :max_idle_time, type: :ms
      uri_option 'timeoutMS', :timeout_ms, type: :integer

      # Write Options
      uri_option 'w', :w, group: :write_concern, type: :w
      uri_option 'journal', :j, group: :write_concern, type: :bool
      uri_option 'fsync', :fsync, group: :write_concern, type: :bool
      uri_option 'wTimeoutMS', :wtimeout, group: :write_concern, type: :integer

      # Read Options
      uri_option 'readPreference', :mode, group: :read, type: :read_mode
      uri_option 'readPreferenceTags', :tag_sets, group: :read, type: :read_tags
      uri_option 'maxStalenessSeconds', :max_staleness, group: :read, type: :max_staleness

      # Pool options
      uri_option 'maxConnecting', :max_connecting, type: :integer
      uri_option 'minPoolSize', :min_pool_size, type: :integer
      uri_option 'maxPoolSize', :max_pool_size, type: :integer
      uri_option 'waitQueueTimeoutMS', :wait_queue_timeout, type: :ms

      # Security Options
      uri_option 'ssl', :ssl, type: :repeated_bool
      uri_option 'tls', :ssl, type: :repeated_bool
      uri_option 'tlsAllowInvalidCertificates', :ssl_verify_certificate,
                 type: :inverse_bool
      uri_option 'tlsAllowInvalidHostnames', :ssl_verify_hostname,
                 type: :inverse_bool
      uri_option 'tlsCAFile', :ssl_ca_cert
      uri_option 'tlsCertificateKeyFile', :ssl_cert
      uri_option 'tlsCertificateKeyFilePassword', :ssl_key_pass_phrase
      uri_option 'tlsInsecure', :ssl_verify, type: :inverse_bool
      uri_option 'tlsDisableOCSPEndpointCheck', :ssl_verify_ocsp_endpoint,
        type: :inverse_bool

      # Topology options
      uri_option 'directConnection', :direct_connection, type: :bool
      uri_option 'connect', :connect, type: :symbol
      uri_option 'loadBalanced', :load_balanced, type: :bool
      uri_option 'srvMaxHosts', :srv_max_hosts, type: :integer
      uri_option 'srvServiceName', :srv_service_name

      # Auth Options
      uri_option 'authSource', :auth_source
      uri_option 'authMechanism', :auth_mech, type: :auth_mech
      uri_option 'authMechanismProperties', :auth_mech_properties, type: :auth_mech_props

      # Client Options
      uri_option 'appName', :app_name
      uri_option 'compressors', :compressors, type: :array
      uri_option 'readConcernLevel', :level, group: :read_concern, type: :symbol
      uri_option 'retryReads', :retry_reads, type: :bool
      uri_option 'retryWrites', :retry_writes, type: :bool
      uri_option 'zlibCompressionLevel', :zlib_compression_level, type: :zlib_compression_level

      # Converts +value+ to a boolean.
      #
      # Returns true for 'true', false for 'false', otherwise nil.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | true | false ] value URI option value.
      #
      # @return [ true | false | nil ] Converted value.
      def convert_bool(name, value)
        case value
        when true, "true", 'TRUE'
          true
        when false, "false", 'FALSE'
          false
        else
          log_warn("invalid boolean option for #{name}: #{value}")
          nil
        end
      end

      # Reverts a boolean type.
      #
      # @param [ true | false | nil ] value The boolean to revert.
      #
      # @return [ true | false | nil ] The passed value.
      def revert_bool(value)
        value
      end

      # Stringifies a boolean type.
      #
      # @param [ true | false | nil ] value The boolean.
      #
      # @return [ String | nil ] The string.
      def stringify_bool(value)
        revert_bool(value)&.to_s
      end

      # Converts the value into a boolean and returns it wrapped in an array.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value URI option value.
      #
      # @return [ Array<true | false> | nil ] The boolean value parsed and wraped
      #   in an array.
      def convert_repeated_bool(name, value)
        [convert_bool(name, value)]
      end

      # Reverts a repeated boolean type.
      #
      # @param [ Array<true | false> | true | false | nil ] value The repeated boolean to revert.
      #
      # @return [ Array<true | false> | true | false | nil ] The passed value.
      def revert_repeated_bool(value)
        value
      end

      # Stringifies a repeated boolean type.
      #
      # @param [ Array<true | false> | nil ] value The repeated boolean.
      #
      # @return [ Array<true | false> | nil ] The string.
      def stringify_repeated_bool(value)
        rep = revert_repeated_bool(value)
        if rep&.is_a?(Array)
          rep.join(",")
        else
          rep
        end
      end

      # Parses a boolean value and returns its inverse.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | true | false ] value The URI option value.
      #
      # @return [ true | false | nil ] The inverse of the boolean value parsed out, otherwise nil
      #   (and a warning will be logged).
      def convert_inverse_bool(name, value)
        b = convert_bool(name, value)

        if b.nil?
          nil
        else
          !b
        end
      end

      # Reverts and inverts a boolean type.
      #
      # @param [ true | false | nil ] value The boolean to revert and invert.
      #
      # @return [ true | false | nil ] The inverted boolean.
      def revert_inverse_bool(value)
        value.nil? ? nil : !value
      end

      # Inverts and stringifies a boolean.
      #
      # @param [ true | false | nil ] value The boolean.
      #
      # @return [ String | nil ] The string.
      def stringify_inverse_bool(value)
        revert_inverse_bool(value)&.to_s
      end

      # Converts +value+ into an integer. Only converts positive integers.
      #
      # If the value is not a valid integer, warns and returns nil.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | Integer ] value URI option value.
      #
      # @return [ nil | Integer ] Converted value.
      def convert_integer(name, value)
        if value.is_a?(String) && /\A\d+\z/ !~ value
          log_warn("#{value} is not a valid integer for #{name}")
          return nil
        end

        value.to_i
      end

      # Reverts an integer.
      #
      # @param [ Integer | nil ] value The integer.
      #
      # @return [ Integer | nil ] The passed value.
      def revert_integer(value)
        value
      end

      # Stringifies an integer.
      #
      # @param [ Integer | nil ] value The integer.
      #
      # @return [ String | nil ] The string.
      def stringify_integer(value)
        revert_integer(value)&.to_s
      end

      # Ruby's convention is to provide timeouts in seconds, not milliseconds and
      # to use fractions where more precision is necessary. The connection string
      # options are always in MS so we provide an easy conversion type.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | Integer | Float ] value The millisecond value.
      #
      # @return [ Float ] The seconds value.
      #
      # @since 2.0.0
      def convert_ms(name, value)
        case value
        when String
          if /\A-?\d+(\.\d+)?\z/ !~ value
            log_warn("Invalid ms value for #{name}: #{value}")
            return nil
          end
          if value.to_s[0] == '-'
            log_warn("#{name} cannot be a negative number")
            return nil
          end
        when Integer, Float
          if value < 0
            log_warn("#{name} cannot be a negative number")
            return nil
          end
        else
          raise ArgumentError, "Can only convert Strings, Integers, or Floats to ms. Given: #{value.class}"
        end

        value.to_f / 1000
      end

      # Reverts an ms.
      #
      # @param [ Float ] value The float.
      #
      # @return [ Integer ] The number multiplied by 1000 as an integer.
      def revert_ms(value)
        (value * 1000).round
      end

      # Stringifies an ms.
      #
      # @param [ Float ] value The float.
      #
      # @return [ String ] The string.
      def stringify_ms(value)
        revert_ms(value).to_s
      end

      # Converts +value+ into a symbol.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | Symbol ] value URI option value.
      #
      # @return [ Symbol ] Converted value.
      def convert_symbol(name, value)
        value.to_sym
      end

      # Reverts a symbol.
      #
      # @param [ Symbol ] value The symbol.
      #
      # @return [ String ] The passed value as a string.
      def revert_symbol(value)
        value.to_s
      end
      alias :stringify_symbol :revert_symbol

      # Extract values from the string and put them into an array.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value The string to build an array from.
      #
      # @return [ Array<String> ] The array built from the string.
      def convert_array(name, value)
        value.split(',')
      end

      # Reverts an array.
      #
      # @param [ Array<String> ] value An array of strings.
      #
      # @return [ Array<String> ] The passed value.
      def revert_array(value)
        value
      end

      # Stringifies an array.
      #
      # @param [ Array<String> ] value An array of strings.
      #
      # @return [ String ] The array joined by commas.
      def stringify_array(value)
        value.join(',')
      end

      # Authentication mechanism transformation.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value The authentication mechanism.
      #
      # @return [ Symbol ] The transformed authentication mechanism.
      def convert_auth_mech(name, value)
        auth_mech = AUTH_MECH_MAP[value.upcase]
        (auth_mech || value).tap do |mech|
          log_warn("#{value} is not a valid auth mechanism") unless auth_mech
        end
      end

      # Reverts auth mechanism.
      #
      # @param [ Symbol ] value The auth mechanism.
      #
      # @return [ String ] The auth mechanism as a string.
      #
      # @raise [ ArgumentError ] if its an invalid auth mechanism.
      def revert_auth_mech(value)
        found = AUTH_MECH_MAP.detect do |k, v|
          v == value
        end
        if found
          found.first
        else
          raise ArgumentError, "Unknown auth mechanism #{value}"
        end
      end

      # Stringifies auth mechanism.
      #
      # @param [ Symbol ] value The auth mechanism.
      #
      # @return [ String | nil ] The auth mechanism as a string.
      def stringify_auth_mech(value)
        revert_auth_mech(value) rescue nil
      end

      # Auth mechanism properties extractor.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value The auth mechanism properties string.
      #
      # @return [ Hash | nil ] The auth mechanism properties hash.
      def convert_auth_mech_props(name, value)
        properties = hash_extractor('authMechanismProperties', value)
        if properties
          properties.each do |k, v|
            if k.to_s.downcase == 'canonicalize_host_name' && v
              properties[k] = (v.downcase == 'true')
            end
          end
        end
        properties
      end

      # Reverts auth mechanism properties.
      #
      # @param [ Hash | nil ] value The auth mech properties.
      #
      # @return [ Hash | nil ] The passed value.
      def revert_auth_mech_props(value)
        value
      end

      # Stringifies auth mechanism properties.
      #
      # @param [ Hash | nil ] value The auth mech properties.
      #
      # @return [ String | nil ] The string.
      def stringify_auth_mech_props(value)
        return if value.nil?
        value.map { |k, v| "#{k}:#{v}" }.join(',')
      end

      # Parses the max staleness value, which must be either "0" or an integer
      # greater or equal to 90.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | Integer ] value The max staleness string.
      #
      # @return [ Integer | nil ] The max staleness integer parsed out if it is valid, otherwise nil
      #   (and a warning will be logged).
      def convert_max_staleness(name, value)
        int = if value.is_a?(String) && /\A-?\d+\z/ =~ value
          value.to_i
        elsif value.is_a?(Integer)
          value
        end

        if int.nil?
          log_warn("Invalid max staleness value: #{value}")
          return nil
        end

        if int == -1
          int = nil
        end

        if int && (int > 0 && int < 90 || int < 0)
          log_warn("max staleness should be either 0 or greater than 90: #{value}")
          int = nil
        end

        int
      end

      # Reverts max staleness.
      #
      # @param [ Integer | nil ] value The max staleness.
      #
      # @return [ Integer | nil ] The passed value.
      def revert_max_staleness(value)
        value
      end

      # Stringifies max staleness.
      #
      # @param [ Integer | nil ] value The max staleness.
      #
      # @return [ String | nil ] The string.
      def stringify_max_staleness(value)
        revert_max_staleness(value)&.to_s
      end

      # Read preference mode transformation.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value The read mode string value.
      #
      # @return [ Symbol | String ] The read mode.
      def convert_read_mode(name, value)
        READ_MODE_MAP[value.downcase] || value
      end

      # Reverts read mode.
      #
      # @param [ Symbol | String ] value The read mode.
      #
      # @return [ String ] The read mode as a string.
      def revert_read_mode(value)
        value.to_s.gsub(/_(\w)/) { $1.upcase }
      end
      alias :stringify_read_mode :revert_read_mode

      # Read preference tags transformation.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value The string representing tag set.
      #
      # @return [ Array<Hash> | nil ] Array with tag set.
      def convert_read_tags(name, value)
        converted = convert_read_set(name, value)
        if converted
          [converted]
        else
          nil
        end
      end

      # Reverts read tags.
      #
      # @param [ Array<Hash> | nil ] value The read tags.
      #
      # @return [ Array<Hash> | nil ] The passed value.
      def revert_read_tags(value)
        value
      end

      # Stringifies read tags.
      #
      # @param [ Array<Hash> | nil ] value The read tags.
      #
      # @return [ String | nil ] The joined string of read tags.
      def stringify_read_tags(value)
        value&.map { |ar| ar.map { |k, v| "#{k}:#{v}" }.join(',') }
      end

      # Read preference tag set extractor.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value The tag set string.
      #
      # @return [ Hash ] The tag set hash.
      def convert_read_set(name, value)
        hash_extractor('readPreferenceTags', value)
      end

      # Converts +value+ as a write concern.
      #
      # If +value+ is the word "majority", returns the symbol :majority.
      # If +value+ is a number, returns the number as an integer.
      # Otherwise returns the string +value+ unchanged.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | Integer ] value URI option value.
      #
      # @return [ Integer | Symbol | String ] Converted value.
      def convert_w(name, value)
        case value
        when 'majority'
          :majority
        when /\A[0-9]+\z/
          value.to_i
        else
          value
        end
      end

      # Reverts write concern.
      #
      # @param [ Integer | Symbol | String ] value The write concern.
      #
      # @return [ Integer | String ] The write concern as a string.
      def revert_w(value)
        case value
        when Symbol
          value.to_s
        else
          value
        end
      end

      # Stringifies write concern.
      #
      # @param [ Integer | Symbol | String ] value The write concern.
      #
      # @return [ String ] The write concern as a string.
      def stringify_w(value)
        revert_w(value)&.to_s
      end

      # Parses the zlib compression level.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String | Integer ] value The zlib compression level string.
      #
      # @return [ Integer | nil ] The compression level value if it is between -1 and 9 (inclusive),
      #   otherwise nil (and a warning will be logged).
      def convert_zlib_compression_level(name, value)
        i = if value.is_a?(String) && /\A-?\d+\z/ =~ value
          value.to_i
        elsif value.is_a?(Integer)
          value
        end

        if i && (i >= -1 && i <= 9)
          i
        else
          log_warn("#{value} is not a valid zlibCompressionLevel")
          nil
        end
      end

      # Reverts zlib compression level
      #
      # @param [ Integer | nil ] value The write concern.
      #
      # @return [ Integer | nil ] The passed value.
      def revert_zlib_compression_level(value)
        value
      end

      # Stringifies zlib compression level
      #
      # @param [ Integer | nil ] value The write concern.
      #
      # @return [ String | nil ] The string.
      def stringify_zlib_compression_level(value)
        revert_zlib_compression_level(value)&.to_s
      end

      # Extract values from the string and put them into a nested hash.
      #
      # @param [ String ] name Name of the URI option being processed.
      # @param [ String ] value The string to build a hash from.
      #
      # @return [ Hash ] The hash built from the string.
      def hash_extractor(name, value)
        h = {}
        value.split(',').each do |tag|
          k, v = tag.split(':')
          if v.nil?
            log_warn("Invalid hash value for #{name}: key `#{k}` does not have a value: #{value}")
            next
          end

          h[k.to_sym] = v
        end
        if h.empty?
          nil
        else
          h
        end
      end
    end
  end
end