multi_json 1.19.1 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MultiJson
2
4
  # Handles adapter discovery, loading, and selection
3
5
  #
@@ -10,17 +12,25 @@ module MultiJson
10
12
  module AdapterSelector
11
13
  extend self
12
14
 
13
- # Alternate spellings for adapter names
14
- ALIASES = {"jrjackson" => "jr_jackson"}.freeze
15
-
16
- # Strategy lambdas for loading adapters based on specification type
17
- LOADERS = {
18
- Module => ->(adapter, _selector) { adapter },
19
- String => ->(name, selector) { selector.send(:load_adapter_by_name, name) },
20
- Symbol => ->(name, selector) { selector.send(:load_adapter_by_name, name.to_s) },
21
- NilClass => ->(_adapter, selector) { selector.send(:load_adapter, selector.default_adapter) },
22
- FalseClass => ->(_adapter, selector) { selector.send(:load_adapter, selector.default_adapter) }
15
+ # Per-adapter metadata, in preference order (fastest first). Each
16
+ # entry maps the adapter symbol to its ``require`` path and the
17
+ # constant whose presence indicates the backing library is already
18
+ # loaded. ``loaded`` is a ``::``-separated path so we can walk it
19
+ # without an explicit ``defined?`` check.
20
+ ADAPTERS = {
21
+ fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
22
+ oj: {require: "oj", loaded: "Oj"},
23
+ yajl: {require: "yajl", loaded: "Yajl"},
24
+ jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
25
+ json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
26
+ gson: {require: "gson", loaded: "Gson"}
23
27
  }.freeze
28
+ private_constant :ADAPTERS
29
+
30
+ # Backwards-compatible view of {ADAPTERS} that exposes only the
31
+ # require paths. Tests still poke at this constant to stub or break
32
+ # the require step.
33
+ REQUIREMENT_MAP = ADAPTERS.transform_values { |meta| meta[:require] }.freeze
24
34
 
25
35
  # Returns the default adapter to use
26
36
  #
@@ -29,7 +39,27 @@ module MultiJson
29
39
  # @example
30
40
  # AdapterSelector.default_adapter #=> :oj
31
41
  def default_adapter
32
- @default_adapter ||= detect_best_adapter
42
+ Concurrency.synchronize(:default_adapter) { @default_adapter ||= detect_best_adapter }
43
+ end
44
+
45
+ # Returns the default adapter class, excluding the given adapter name
46
+ #
47
+ # Used by adapters that only implement one direction (e.g.
48
+ # FastJsonparser only parses) so the other direction can be delegated
49
+ # to whichever library MultiJson would otherwise pick.
50
+ #
51
+ # @api private
52
+ # @param excluded [Symbol] adapter name to skip during detection
53
+ # @return [Class] the adapter class
54
+ # @example
55
+ # AdapterSelector.default_adapter_excluding(:fast_jsonparser) #=> MultiJson::Adapters::Oj
56
+ def default_adapter_excluding(excluded)
57
+ Concurrency.synchronize(:default_adapter) do
58
+ name = loaded_adapter(excluding: excluded)
59
+ name ||= installable_adapter(excluding: excluded)
60
+ name ||= fallback_adapter
61
+ load_adapter_by_name(name.to_s)
62
+ end
33
63
  end
34
64
 
35
65
  private
@@ -45,23 +75,24 @@ module MultiJson
45
75
  # Finds an already-loaded JSON library
46
76
  #
47
77
  # @api private
78
+ # @param excluding [Symbol, nil] adapter name to skip during detection
48
79
  # @return [Symbol, nil] adapter name if found
49
- def loaded_adapter
50
- return :fast_jsonparser if defined?(::FastJsonparser)
51
- return :oj if defined?(::Oj)
52
- return :yajl if defined?(::Yajl)
53
- return :jr_jackson if defined?(::JrJackson)
54
- return :json_gem if defined?(::JSON::Ext::Parser)
55
-
56
- :gson if defined?(::Gson)
80
+ def loaded_adapter(excluding: nil)
81
+ ADAPTERS.each do |name, meta|
82
+ next if name == excluding
83
+ return name if Object.const_defined?(meta.fetch(:loaded))
84
+ end
85
+ nil
57
86
  end
58
87
 
59
88
  # Tries to require and use an installable adapter
60
89
  #
61
90
  # @api private
91
+ # @param excluding [Symbol, nil] adapter name to skip during detection
62
92
  # @return [Symbol, nil] adapter name if successfully required
63
- def installable_adapter
64
- ::MultiJson::REQUIREMENT_MAP.each_key do |adapter_name|
93
+ def installable_adapter(excluding: nil)
94
+ REQUIREMENT_MAP.each_key do |adapter_name|
95
+ next if adapter_name == excluding
65
96
  return adapter_name if try_require(adapter_name)
66
97
  end
67
98
  nil
@@ -73,7 +104,7 @@ module MultiJson
73
104
  # @param adapter_name [Symbol] adapter to require
74
105
  # @return [Boolean] true if require succeeded
75
106
  def try_require(adapter_name)
76
- require ::MultiJson::REQUIREMENT_MAP.fetch(adapter_name)
107
+ require REQUIREMENT_MAP.fetch(adapter_name)
77
108
  true
78
109
  rescue ::LoadError
79
110
  false
@@ -81,58 +112,59 @@ module MultiJson
81
112
 
82
113
  # Returns the fallback adapter when no others available
83
114
  #
115
+ # The json gem is a Ruby default gem since Ruby 1.9, so in practice
116
+ # the installable-adapter step always succeeds before reaching this
117
+ # fallback on any supported Ruby version. The warning below only
118
+ # fires in tests that deliberately break the require path.
119
+ #
84
120
  # @api private
85
- # @return [Symbol] the ok_json adapter name
121
+ # @return [Symbol] the json_gem adapter name
86
122
  def fallback_adapter
87
123
  warn_about_fallback unless @default_adapter_warning_shown
88
124
  @default_adapter_warning_shown = true
89
- :ok_json
125
+ :json_gem
90
126
  end
91
127
 
92
- # Warns the user about using the slow fallback adapter
128
+ # Warns the user about reaching the last-resort fallback
93
129
  #
94
130
  # @api private
95
131
  # @return [void]
96
132
  def warn_about_fallback
97
133
  Kernel.warn(
98
- "[WARNING] MultiJson is using the default adapter (ok_json). " \
99
- "We recommend loading a different JSON library to improve performance."
134
+ "[WARNING] MultiJson is falling back to the json_gem adapter " \
135
+ "because no other JSON library could be loaded."
100
136
  )
101
137
  end
102
138
 
103
139
  # Loads an adapter from a specification
104
140
  #
105
141
  # @api private
106
- # @param adapter_spec [Symbol, String, Module, nil] adapter specification
142
+ # @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
107
143
  # @return [Class] the adapter class
108
144
  def load_adapter(adapter_spec)
109
- loader = find_loader_for(adapter_spec)
110
- return loader.call(adapter_spec, self) if loader
111
-
112
- raise ::LoadError, adapter_spec
145
+ case adapter_spec
146
+ when ::String then load_adapter_by_name(adapter_spec)
147
+ when ::Symbol then load_adapter_by_name(adapter_spec.to_s)
148
+ when nil, false then load_adapter(default_adapter)
149
+ when ::Module then adapter_spec
150
+ else raise ::LoadError, "expected adapter to be a Symbol, String, or Module, got #{adapter_spec.inspect}"
151
+ end
113
152
  rescue ::LoadError => e
114
153
  raise AdapterError.build(e)
115
154
  end
116
155
 
117
- # Finds the appropriate loader for an adapter specification
118
- #
119
- # @api private
120
- # @param adapter_spec [Object] adapter specification
121
- # @return [Proc, nil] loader proc if found
122
- def find_loader_for(adapter_spec)
123
- klass = adapter_spec.class
124
- return LOADERS.fetch(klass) if LOADERS.key?(klass)
125
-
126
- LOADERS.fetch(Module) if adapter_spec.is_a?(Module)
127
- end
128
-
129
156
  # Loads an adapter by its string name
130
157
  #
158
+ # ``jrjackson`` (the JrJackson gem's name) is normalized to
159
+ # ``jr_jackson`` (the adapter file/class name) for backwards
160
+ # compatibility with the original gem-name alias.
161
+ #
131
162
  # @api private
132
163
  # @param name [String] adapter name
133
164
  # @return [Class] the adapter class
134
165
  def load_adapter_by_name(name)
135
- normalized = ALIASES.fetch(name, name).downcase
166
+ normalized = name.downcase
167
+ normalized = "jr_jackson" if normalized == "jrjackson"
136
168
  require_relative "adapters/#{normalized}"
137
169
 
138
170
  class_name = normalized.split("_").map(&:capitalize).join
@@ -1,24 +1,65 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "fast_jsonparser"
2
- require "oj"
3
4
  require_relative "../adapter"
4
- require_relative "oj_common"
5
+ require_relative "../adapter_selector"
5
6
 
6
7
  module MultiJson
7
8
  module Adapters
8
- # Use the FastJsonparser library to load and Oj to dump.
9
+ # Use the FastJsonparser library to load, and the fastest other
10
+ # available adapter to dump.
11
+ #
12
+ # FastJsonparser only implements parsing, so the ``dump`` side of
13
+ # the adapter is delegated to whichever adapter MultiJson would
14
+ # pick if FastJsonparser weren't installed (oj → yajl → jr_jackson
15
+ # → json_gem → gson). The delegate is resolved lazily at the first
16
+ # ``dump`` call, not at file load time, so load order doesn't lock
17
+ # in the wrong delegate. Require any preferred dump backend before
18
+ # the first ``dump`` call (typical applications already have ``oj``
19
+ # loaded by then).
9
20
  class FastJsonparser < Adapter
10
- include OjCommon
11
-
12
21
  defaults :load, symbolize_keys: false
13
- defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
14
22
 
23
+ # Exception raised when JSON parsing fails
15
24
  ParseError = ::FastJsonparser::ParseError
16
25
 
26
+ class << self
27
+ # Serialize a Ruby object to JSON via the lazy delegate
28
+ #
29
+ # @api private
30
+ # @param object [Object] object to serialize
31
+ # @param options [Hash] serialization options
32
+ # @return [String] JSON string
33
+ # @example
34
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
35
+ def dump(object, options = {})
36
+ dump_delegate.dump(object, options)
37
+ end
38
+
39
+ private
40
+
41
+ # Resolve the dump delegate, caching it across calls
42
+ #
43
+ # @api private
44
+ # @return [Class] delegate adapter class
45
+ def dump_delegate
46
+ MultiJson::Concurrency.synchronize(:dump_delegate) do
47
+ @dump_delegate ||= MultiJson::AdapterSelector.default_adapter_excluding(:fast_jsonparser)
48
+ end
49
+ end
50
+ end
51
+
17
52
  # Parse a JSON string into a Ruby object
18
53
  #
54
+ # FastJsonparser.parse only accepts ``symbolize_keys`` and raises
55
+ # on unknown keyword arguments, so the adapter explicitly forwards
56
+ # only that option and silently drops the rest. Pass other options
57
+ # through ``MultiJson.load_options=`` and they'll apply to whichever
58
+ # adapter MultiJson selects when fast_jsonparser isn't installed.
59
+ #
19
60
  # @api private
20
61
  # @param string [String] JSON string to parse
21
- # @param options [Hash] parsing options
62
+ # @param options [Hash] parsing options (only :symbolize_keys is honored)
22
63
  # @return [Object] parsed Ruby object
23
64
  #
24
65
  # @example Parse JSON string
@@ -26,19 +67,6 @@ module MultiJson
26
67
  def load(string, options = {})
27
68
  ::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_keys])
28
69
  end
29
-
30
- # Serialize a Ruby object to JSON
31
- #
32
- # @api private
33
- # @param object [Object] object to serialize
34
- # @param options [Hash] serialization options
35
- # @return [String] JSON string
36
- #
37
- # @example Serialize object to JSON
38
- # adapter.dump({key: "value"}) #=> '{"key":"value"}'
39
- def dump(object, options = {})
40
- ::Oj.dump(object, prepare_dump_options(options))
41
- end
42
70
  end
43
71
  end
44
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../adapter"
2
4
  require "json"
3
5
 
@@ -5,6 +7,7 @@ module MultiJson
5
7
  module Adapters
6
8
  # Use the JSON gem to dump/load.
7
9
  class JsonGem < Adapter
10
+ # Exception raised when JSON parsing fails
8
11
  ParseError = ::JSON::ParserError
9
12
 
10
13
  defaults :load, create_additions: false, quirks_mode: true
@@ -23,14 +26,21 @@ module MultiJson
23
26
  # @param string [String] JSON string to parse
24
27
  # @param options [Hash] parsing options
25
28
  # @return [Object] parsed Ruby object
29
+ # @raise [::JSON::ParserError] when input contains invalid bytes that
30
+ # cannot be transcoded to UTF-8
26
31
  #
27
32
  # @example Parse JSON string
28
33
  # adapter.load('{"key":"value"}') #=> {"key" => "value"}
29
34
  def load(string, options = {})
30
- string = string.dup.force_encoding(Encoding::UTF_8) if string.encoding != Encoding::UTF_8
35
+ if string.encoding != Encoding::UTF_8
36
+ begin
37
+ string = string.encode(Encoding::UTF_8)
38
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
39
+ raise ::JSON::ParserError, e.message
40
+ end
41
+ end
31
42
 
32
- options[:symbolize_names] = true if options.delete(:symbolize_keys)
33
- ::JSON.parse(string, options)
43
+ ::JSON.parse(string, translate_load_options(options))
34
44
  end
35
45
 
36
46
  # Serialize a Ruby object to JSON
@@ -43,16 +53,32 @@ module MultiJson
43
53
  # @example Serialize object to JSON
44
54
  # adapter.dump({key: "value"}) #=> '{"key":"value"}'
45
55
  def dump(object, options = {})
46
- opts = options.except(:adapter)
47
56
  json_object = object.respond_to?(:as_json) ? object.as_json : object
48
- return ::JSON.dump(json_object) if opts.empty?
57
+ return ::JSON.dump(json_object) if options.empty?
58
+ return ::JSON.generate(json_object, options) unless options.key?(:pretty)
49
59
 
50
- if opts.delete(:pretty)
51
- opts = PRETTY_STATE_PROTOTYPE.merge(opts)
52
- return ::JSON.pretty_generate(json_object, opts)
53
- end
60
+ # Common case: ``pretty: true`` is the only option, so the merge
61
+ # would just produce a copy of PRETTY_STATE_PROTOTYPE.
62
+ return ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE) if options.size == 1
63
+
64
+ ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE.merge(options.except(:pretty)))
65
+ end
66
+
67
+ private
68
+
69
+ # Translate ``:symbolize_keys`` into JSON gem's ``:symbolize_names``
70
+ #
71
+ # Returns a new hash without mutating the input. ``options`` is the
72
+ # cached hash returned from {Adapter.merged_load_options}, so in-place
73
+ # edits would pollute the cache and corrupt subsequent calls.
74
+ #
75
+ # @api private
76
+ # @param options [Hash] merged load options
77
+ # @return [Hash] options with ``:symbolize_keys`` translated
78
+ def translate_load_options(options)
79
+ return options unless options[:symbolize_keys]
54
80
 
55
- ::JSON.generate(json_object, opts)
81
+ options.except(:symbolize_keys).merge(symbolize_names: true)
56
82
  end
57
83
  end
58
84
  end
@@ -1,8 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oj"
2
4
  require_relative "../adapter"
3
5
  require_relative "oj_common"
4
6
 
5
7
  module MultiJson
8
+ # Namespace for JSON adapter implementations
9
+ #
10
+ # Each adapter wraps a specific JSON library and provides a consistent
11
+ # interface for loading and dumping JSON data.
6
12
  module Adapters
7
13
  # Use the Oj library to dump/load.
8
14
  class Oj < Adapter
@@ -11,13 +17,13 @@ module MultiJson
11
17
  defaults :load, mode: :strict, symbolize_keys: false
12
18
  defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
13
19
 
14
- # In certain cases OJ gem may throw JSON::ParserError exception instead
15
- # of its own class. Also, we can't expect ::JSON::ParserError and
16
- # ::Oj::ParseError to always be defined, since it's often not the case.
17
- # Because of this, we can't reference those classes directly and have to
18
- # do string comparison instead. This will not catch subclasses, but it
19
- # shouldn't be a problem since the library is not known to be using it
20
- # (at least for now).
20
+ # In certain cases the Oj gem may throw a ``JSON::ParserError``
21
+ # exception instead of its own class. Neither ``::JSON::ParserError``
22
+ # nor ``::Oj::ParseError`` is guaranteed to be defined, so we can't
23
+ # reference them directly match by walking the exception's
24
+ # ancestry by class name instead. This will not catch subclasses
25
+ # of those classes, which shouldn't be a problem since neither
26
+ # library is known to subclass them.
21
27
  class ParseError < ::SyntaxError
22
28
  WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].freeze
23
29
  private_constant :WRAPPED_CLASSES
@@ -31,7 +37,7 @@ module MultiJson
31
37
  # @example Match parse errors in rescue
32
38
  # rescue ParseError => e
33
39
  def self.===(exception)
34
- exception.is_a?(::SyntaxError) || WRAPPED_CLASSES.include?(exception.class.to_s)
40
+ exception.class.ancestors.any? { |ancestor| WRAPPED_CLASSES.include?(ancestor.to_s) }
35
41
  end
36
42
  end
37
43
 
@@ -45,8 +51,7 @@ module MultiJson
45
51
  # @example Parse JSON string
46
52
  # adapter.load('{"key":"value"}') #=> {"key" => "value"}
47
53
  def load(string, options = {})
48
- options[:symbol_keys] = options[:symbolize_keys]
49
- ::Oj.load(string, options)
54
+ ::Oj.load(string, translate_load_options(options))
50
55
  end
51
56
 
52
57
  # Serialize a Ruby object to JSON
@@ -61,6 +66,24 @@ module MultiJson
61
66
  def dump(object, options = {})
62
67
  ::Oj.dump(object, prepare_dump_options(options))
63
68
  end
69
+
70
+ private
71
+
72
+ # Translate ``:symbolize_keys`` into Oj's ``:symbol_keys``
73
+ #
74
+ # Returns a new hash without mutating the input.
75
+ # ``:symbol_keys`` is always set (true or false) so MultiJson's
76
+ # behavior is independent of any global ``Oj.default_options``
77
+ # the host application may have set. The input is the cached hash
78
+ # returned from {Adapter.merged_load_options}, so in-place edits
79
+ # would pollute the cache.
80
+ #
81
+ # @api private
82
+ # @param options [Hash] merged load options
83
+ # @return [Hash] options with ``:symbolize_keys`` translated
84
+ def translate_load_options(options)
85
+ options.except(:symbolize_keys).merge(symbol_keys: options[:symbolize_keys] == true)
86
+ end
64
87
  end
65
88
  end
66
89
  end
@@ -1,28 +1,32 @@
1
- require "rubygems/version"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module MultiJson
4
4
  module Adapters
5
+ # Shared functionality for the Oj adapter
6
+ #
7
+ # Provides option preparation for Oj.dump. Targets Oj 3.x; Oj 2.x is
8
+ # no longer supported.
9
+ #
10
+ # @api private
5
11
  module OjCommon
6
- OJ_VERSION = Gem::Version.new(::Oj::VERSION)
7
- OJ_V2 = OJ_VERSION.segments.first == 2
8
- OJ_V3 = OJ_VERSION.segments.first == 3
9
- private_constant :OJ_VERSION, :OJ_V2, :OJ_V3
10
-
11
- if OJ_V3
12
- PRETTY_STATE_PROTOTYPE = {
13
- indent: " ",
14
- space: " ",
15
- space_before: "",
16
- object_nl: "\n",
17
- array_nl: "\n",
18
- ascii_only: false
19
- }.freeze
20
- private_constant :PRETTY_STATE_PROTOTYPE
21
- end
12
+ PRETTY_STATE_PROTOTYPE = {
13
+ indent: " ",
14
+ space: " ",
15
+ space_before: "",
16
+ object_nl: "\n",
17
+ array_nl: "\n",
18
+ ascii_only: false
19
+ }.freeze
20
+ private_constant :PRETTY_STATE_PROTOTYPE
22
21
 
23
22
  private
24
23
 
25
- # Prepare options for Oj.dump based on Oj version
24
+ # Prepare options for Oj.dump
25
+ #
26
+ # Returns a fresh hash; never mutates the input. The input is the
27
+ # cached options hash returned from Adapter.merged_dump_options, so
28
+ # in-place mutation would pollute the cache and corrupt subsequent
29
+ # dump calls.
26
30
  #
27
31
  # @api private
28
32
  # @param options [Hash] serialization options
@@ -31,16 +35,9 @@ module MultiJson
31
35
  # @example Prepare dump options
32
36
  # prepare_dump_options(pretty: true)
33
37
  def prepare_dump_options(options)
34
- if OJ_V2
35
- options[:indent] = 2 if options[:pretty]
36
- options[:indent] = options[:indent].to_i if options[:indent]
37
- elsif OJ_V3
38
- options.merge!(PRETTY_STATE_PROTOTYPE.dup) if options.delete(:pretty)
39
- else
40
- raise "Unsupported Oj version: #{::Oj::VERSION}"
41
- end
38
+ return options unless options.key?(:pretty)
42
39
 
43
- options
40
+ options.except(:pretty).merge(PRETTY_STATE_PROTOTYPE)
44
41
  end
45
42
  end
46
43
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "yajl"
2
4
  require_relative "../adapter"
3
5
 
@@ -5,6 +7,7 @@ module MultiJson
5
7
  module Adapters
6
8
  # Use the Yajl-Ruby library to dump/load.
7
9
  class Yajl < Adapter
10
+ # Exception raised when JSON parsing fails
8
11
  ParseError = ::Yajl::ParseError
9
12
 
10
13
  # Parse a JSON string into a Ruby object
@@ -17,7 +20,7 @@ module MultiJson
17
20
  # @example Parse JSON string
18
21
  # adapter.load('{"key":"value"}') #=> {"key" => "value"}
19
22
  def load(string, options = {})
20
- ::Yajl::Parser.new(symbolize_keys: options[:symbolize_keys]).parse(string)
23
+ ::Yajl::Parser.new(options).parse(string)
21
24
  end
22
25
 
23
26
  # Serialize a Ruby object to JSON
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJson
4
+ # Catalog of process-wide mutexes used to serialize MultiJson's lazy
5
+ # initializers and adapter swaps. Each mutex protects a distinct
6
+ # piece of mutable state. Callers go through {.synchronize} rather
7
+ # than touching the mutex constants directly so the constants
8
+ # themselves can stay {.private_constant} and the surface of the
9
+ # module is documented in one place.
10
+ #
11
+ # @api private
12
+ module Concurrency
13
+ # Catalog of mutexes keyed by symbolic name. Each entry maps the
14
+ # public name passed to {.synchronize} to the underlying mutex
15
+ # instance. The names are documented inline so callers can find
16
+ # what each mutex protects without leaving this file.
17
+ MUTEXES = {
18
+ # Guards the {DEPRECATION_WARNINGS_SHOWN} set in `MultiJson` so the
19
+ # check-then-add pair in `warn_deprecation_once` doesn't race.
20
+ deprecation_warnings: Mutex.new,
21
+ # Guards the process-wide `@adapter` swap in `MultiJson.use` so two
22
+ # threads can't interleave their `OptionsCache.reset` and adapter
23
+ # assignment.
24
+ adapter: Mutex.new,
25
+ # Guards the lazy `@default_adapter` initializer and the
26
+ # `default_adapter_excluding` detection chain in `AdapterSelector`,
27
+ # so the chain runs at most once and `fallback_adapter`'s one-time
28
+ # warning fires at most once.
29
+ default_adapter: Mutex.new,
30
+ # Guards the lazy `default_load_options` / `default_dump_options`
31
+ # initializers in `MultiJson::Options`.
32
+ default_options: Mutex.new,
33
+ # Guards the lazy dump-delegate resolution in
34
+ # `MultiJson::Adapters::FastJsonparser`.
35
+ dump_delegate: Mutex.new
36
+ }.freeze
37
+ private_constant :MUTEXES
38
+
39
+ # Run a block while holding the named mutex
40
+ #
41
+ # The ``name`` symbol must be one of the keys in the internal
42
+ # ``MUTEXES`` table; an unknown name raises ``KeyError`` so a
43
+ # typo at the call site fails fast instead of silently dropping
44
+ # synchronization on the floor.
45
+ #
46
+ # @api private
47
+ # @param name [Symbol] mutex identifier
48
+ # @yield block to execute while holding the mutex
49
+ # @return [Object] the block's return value
50
+ # @raise [KeyError] when ``name`` does not match a known mutex
51
+ # @example
52
+ # MultiJson::Concurrency.synchronize(:adapter) { ... }
53
+ def self.synchronize(name, &)
54
+ MUTEXES.fetch(name).synchronize(&)
55
+ end
56
+ end
57
+ end