multi_json 1.15.0 → 1.21.1

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.
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
4
+ # Handles adapter discovery, loading, and selection
5
+ #
6
+ # Adapters can be specified as:
7
+ # - Symbol/String: adapter name (e.g., :oj, "json_gem")
8
+ # - Module: adapter class directly
9
+ # - nil/false: use default adapter
10
+ #
11
+ # @api private
12
+ module AdapterSelector
13
+ extend self
14
+
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
+ #
21
+ # The hash order is split per platform: on MRI/TruffleRuby the
22
+ # bundled benchmark suite ranks json_gem ahead of fast_jsonparser/
23
+ # oj/yajl on Ruby 3.4+; on JRuby the FFI-vs-pure-Ruby tradeoff
24
+ # hasn't been re-benchmarked yet, so jr_jackson stays first there.
25
+ # CI re-runs the benchmark with ``--verify-preference`` to fail
26
+ # if the observed ranking diverges.
27
+ # :nocov:
28
+ ADAPTERS = if RUBY_ENGINE == "jruby"
29
+ {
30
+ jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
31
+ json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
32
+ gson: {require: "gson", loaded: "Gson"},
33
+ fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
34
+ oj: {require: "oj", loaded: "Oj"},
35
+ yajl: {require: "yajl", loaded: "Yajl"}
36
+ }.freeze
37
+ else
38
+ {
39
+ json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
40
+ fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
41
+ oj: {require: "oj", loaded: "Oj"},
42
+ yajl: {require: "yajl", loaded: "Yajl"},
43
+ jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
44
+ gson: {require: "gson", loaded: "Gson"}
45
+ }.freeze
46
+ end
47
+ # :nocov:
48
+ private_constant :ADAPTERS
49
+
50
+ # Backwards-compatible view of {ADAPTERS} that exposes only the
51
+ # require paths. Tests still poke at this constant to stub or break
52
+ # the require step.
53
+ REQUIREMENT_MAP = ADAPTERS.transform_values { |meta| meta[:require] }.freeze
54
+
55
+ # Returns the default adapter to use
56
+ #
57
+ # @api private
58
+ # @return [Symbol] adapter name
59
+ # @example
60
+ # AdapterSelector.default_adapter #=> :oj
61
+ def default_adapter
62
+ Concurrency.synchronize(:default_adapter) { @default_adapter ||= detect_best_adapter }
63
+ end
64
+
65
+ # Returns the default adapter class, excluding the given adapter name
66
+ #
67
+ # Used by adapters that only implement one direction (e.g.
68
+ # FastJsonparser only parses) so the other direction can be delegated
69
+ # to whichever library MultiJSON would otherwise pick.
70
+ #
71
+ # @api private
72
+ # @param excluded [Symbol] adapter name to skip during detection
73
+ # @return [Class] the adapter class
74
+ # @example
75
+ # AdapterSelector.default_adapter_excluding(:fast_jsonparser) #=> MultiJSON::Adapters::Oj
76
+ def default_adapter_excluding(excluded)
77
+ Concurrency.synchronize(:default_adapter) do
78
+ name = loaded_adapter(excluding: excluded)
79
+ name ||= installable_adapter(excluding: excluded)
80
+ name ||= fallback_adapter
81
+ load_adapter_by_name(name.to_s)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # Detects the best available JSON adapter
88
+ #
89
+ # @api private
90
+ # @return [Symbol] adapter name
91
+ def detect_best_adapter
92
+ loaded_adapter || installable_adapter || fallback_adapter
93
+ end
94
+
95
+ # Finds an already-loaded JSON library
96
+ #
97
+ # @api private
98
+ # @param excluding [Symbol, nil] adapter name to skip during detection
99
+ # @return [Symbol, nil] adapter name if found
100
+ def loaded_adapter(excluding: nil)
101
+ ADAPTERS.each do |name, meta|
102
+ next if name == excluding
103
+ return name if Object.const_defined?(meta.fetch(:loaded))
104
+ end
105
+ nil
106
+ end
107
+
108
+ # Tries to require and use an installable adapter
109
+ #
110
+ # @api private
111
+ # @param excluding [Symbol, nil] adapter name to skip during detection
112
+ # @return [Symbol, nil] adapter name if successfully required
113
+ def installable_adapter(excluding: nil)
114
+ REQUIREMENT_MAP.each_key do |adapter_name|
115
+ next if adapter_name == excluding
116
+ return adapter_name if try_require(adapter_name)
117
+ end
118
+ nil
119
+ end
120
+
121
+ # Attempts to require a JSON library
122
+ #
123
+ # @api private
124
+ # @param adapter_name [Symbol] adapter to require
125
+ # @return [Boolean] true if require succeeded
126
+ def try_require(adapter_name)
127
+ require REQUIREMENT_MAP.fetch(adapter_name)
128
+ true
129
+ rescue ::LoadError
130
+ false
131
+ end
132
+
133
+ # Returns the fallback adapter when no others available
134
+ #
135
+ # The json gem is a Ruby default gem since Ruby 1.9, so in practice
136
+ # the installable-adapter step always succeeds before reaching this
137
+ # fallback on any supported Ruby version. The warning below only
138
+ # fires in tests that deliberately break the require path.
139
+ #
140
+ # @api private
141
+ # @return [Symbol] the json_gem adapter name
142
+ def fallback_adapter
143
+ warn_about_fallback unless @default_adapter_warning_shown
144
+ @default_adapter_warning_shown = true
145
+ :json_gem
146
+ end
147
+
148
+ # Warns the user about reaching the last-resort fallback
149
+ #
150
+ # @api private
151
+ # @return [void]
152
+ def warn_about_fallback
153
+ Kernel.warn(
154
+ "[WARNING] MultiJSON is falling back to the json_gem adapter " \
155
+ "because no other JSON library could be loaded."
156
+ )
157
+ end
158
+
159
+ # Loads an adapter from a specification
160
+ #
161
+ # @api private
162
+ # @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
163
+ # @return [Class] the adapter class
164
+ def load_adapter(adapter_spec)
165
+ adapter = case adapter_spec
166
+ when ::String then load_adapter_by_name(adapter_spec)
167
+ when ::Symbol then load_adapter_by_name(adapter_spec.to_s)
168
+ when nil, false then load_adapter(default_adapter)
169
+ when ::Module then adapter_spec
170
+ else raise ::LoadError, "expected adapter to be a Symbol, String, or Module, got #{adapter_spec.inspect}"
171
+ end
172
+ validate_adapter!(adapter)
173
+ rescue ::LoadError => e
174
+ raise AdapterError.build(e)
175
+ end
176
+
177
+ # Loads an adapter by its string name
178
+ #
179
+ # ``jrjackson`` (the JrJackson gem's name) is normalized to
180
+ # ``jr_jackson`` (the adapter file/class name) for backwards
181
+ # compatibility with the original gem-name alias.
182
+ #
183
+ # @api private
184
+ # @param name [String] adapter name
185
+ # @return [Class] the adapter class
186
+ def load_adapter_by_name(name)
187
+ normalized = name.downcase
188
+ normalized = "jr_jackson" if normalized == "jrjackson"
189
+ require_relative "adapters/#{normalized}"
190
+
191
+ class_name = normalized.split("_").map(&:capitalize).join
192
+ ::MultiJSON::Adapters.const_get(class_name)
193
+ end
194
+
195
+ # Validate that an adapter satisfies the documented contract
196
+ #
197
+ # Custom adapters are accepted as modules/classes, so fail fast
198
+ # during adapter resolution rather than later on the first load or
199
+ # dump call.
200
+ #
201
+ # @api private
202
+ # @param adapter [Module] adapter class or module
203
+ # @return [Module] the validated adapter
204
+ # @raise [AdapterError] when the adapter is missing a required class method
205
+ # or ParseError constant
206
+ def validate_adapter!(adapter)
207
+ raise AdapterError, "Adapter #{adapter} must respond to .load" unless adapter.respond_to?(:load)
208
+ raise AdapterError, "Adapter #{adapter} must respond to .dump" unless adapter.respond_to?(:dump)
209
+
210
+ MultiJSON.parse_error_class_for(adapter)
211
+ adapter
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast_jsonparser"
4
+ require_relative "../adapter"
5
+ require_relative "../adapter_selector"
6
+
7
+ module MultiJSON
8
+ module Adapters
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).
20
+ class FastJsonparser < Adapter
21
+ defaults :load, symbolize_names: false
22
+
23
+ # Exception raised when JSON parsing fails
24
+ ParseError = ::FastJsonparser::ParseError
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
+
52
+ # Parse a JSON string into a Ruby object
53
+ #
54
+ # FastJsonparser.parse only accepts ``symbolize_keys`` and raises
55
+ # on unknown keyword arguments, so the adapter explicitly forwards
56
+ # MultiJSON's canonical ``:symbolize_names`` option as
57
+ # FastJsonparser's native ``symbolize_keys:`` kwarg and silently
58
+ # drops the rest. Pass other options through
59
+ # ``MultiJSON.parse_options=`` and they'll apply to whichever
60
+ # adapter MultiJSON selects when fast_jsonparser isn't installed.
61
+ #
62
+ # @api private
63
+ # @param string [String] JSON string to parse
64
+ # @param options [Hash] parsing options (only :symbolize_names is honored)
65
+ # @return [Object] parsed Ruby object
66
+ #
67
+ # @example Parse JSON string
68
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
69
+ def load(string, options = {})
70
+ ::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_names])
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,11 +1,72 @@
1
- require 'json/ext'
2
- require 'multi_json/adapters/json_common'
1
+ # frozen_string_literal: true
3
2
 
4
- module MultiJson
3
+ require_relative "../adapter"
4
+ require "json"
5
+
6
+ module MultiJSON
5
7
  module Adapters
6
8
  # Use the JSON gem to dump/load.
7
- class JsonGem < JsonCommon
9
+ class JsonGem < Adapter
10
+ # Exception raised when JSON parsing fails
8
11
  ParseError = ::JSON::ParserError
12
+
13
+ defaults :load, create_additions: false, quirks_mode: true
14
+
15
+ PRETTY_STATE_PROTOTYPE = {
16
+ indent: " ",
17
+ space: " ",
18
+ object_nl: "\n",
19
+ array_nl: "\n"
20
+ }.freeze
21
+ private_constant :PRETTY_STATE_PROTOTYPE
22
+
23
+ # Parse a JSON string into a Ruby object
24
+ #
25
+ # Non-UTF-8 strings are re-labeled via ``force_encoding`` (not
26
+ # transcoded) and then validated. This handles the dominant
27
+ # real-world case: Ruby HTTP libraries return response bodies
28
+ # tagged as ``ASCII-8BIT`` even when the bytes are valid UTF-8.
29
+ # ``encode(Encoding::UTF_8)`` would raise on any multi-byte
30
+ # sequence in that scenario because it tries to transcode each
31
+ # byte individually from ASCII-8BIT to UTF-8.
32
+ #
33
+ # @api private
34
+ # @param string [String] JSON string to parse
35
+ # @param options [Hash] parsing options
36
+ # @return [Object] parsed Ruby object
37
+ # @raise [::JSON::ParserError] when the input is not valid UTF-8
38
+ #
39
+ # @example Parse JSON string
40
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
41
+ def load(string, options = {})
42
+ if string.encoding != Encoding::UTF_8
43
+ string = string.dup.force_encoding(Encoding::UTF_8)
44
+ raise ::JSON::ParserError, "Invalid UTF-8 byte sequence in JSON input" unless string.valid_encoding?
45
+ end
46
+
47
+ ::JSON.parse(string, options)
48
+ end
49
+
50
+ # Serialize a Ruby object to JSON
51
+ #
52
+ # @api private
53
+ # @param object [Object] object to serialize
54
+ # @param options [Hash] serialization options
55
+ # @return [String] JSON string
56
+ #
57
+ # @example Serialize object to JSON
58
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
59
+ def dump(object, options = {})
60
+ json_object = object.respond_to?(:as_json) ? object.as_json : object
61
+ return ::JSON.dump(json_object) if options.empty?
62
+ return ::JSON.generate(json_object, options) unless options.key?(:pretty)
63
+
64
+ # Common case: ``pretty: true`` is the only option, so the merge
65
+ # would just produce a copy of PRETTY_STATE_PROTOTYPE.
66
+ return ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE) if options.size == 1
67
+
68
+ ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE.merge(options.except(:pretty)))
69
+ end
9
70
  end
10
71
  end
11
72
  end
@@ -1,62 +1,88 @@
1
- require 'set'
2
- require 'oj'
3
- require 'multi_json/adapter'
1
+ # frozen_string_literal: true
4
2
 
5
- module MultiJson
3
+ require "oj"
4
+ require_relative "../adapter"
5
+ require_relative "oj_common"
6
+
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
9
- defaults :load, :mode => :strict, :symbolize_keys => false
10
- defaults :dump, :mode => :compat, :time_format => :ruby, :use_to_json => true
11
-
12
- # In certain cases OJ gem may throw JSON::ParserError exception instead
13
- # of its own class. Also, we can't expect ::JSON::ParserError and
14
- # ::Oj::ParseError to always be defined, since it's often not the case.
15
- # Because of this, we can't reference those classes directly and have to
16
- # do string comparison instead. This will not catch subclasses, but it
17
- # shouldn't be a problem since the library is not known to be using it
18
- # (at least for now).
15
+ include OjCommon
16
+
17
+ defaults :load, mode: :strict, symbolize_names: false
18
+ defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
19
+
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.
19
27
  class ParseError < ::SyntaxError
20
- WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].to_set.freeze
28
+ WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].freeze
29
+ private_constant :WRAPPED_CLASSES
21
30
 
31
+ # Case equality for exception matching in rescue clauses
32
+ #
33
+ # @api private
34
+ # @param exception [Exception] exception to check
35
+ # @return [Boolean] true if exception is a parse error
36
+ #
37
+ # @example Match parse errors in rescue
38
+ # rescue ParseError => e
22
39
  def self.===(exception)
23
- case exception
24
- when ::SyntaxError
25
- true
26
- else
27
- WRAPPED_CLASSES.include?(exception.class.to_s)
28
- end
40
+ exception.class.ancestors.any? { |ancestor| WRAPPED_CLASSES.include?(ancestor.to_s) }
29
41
  end
30
42
  end
31
43
 
44
+ # Parse a JSON string into a Ruby object
45
+ #
46
+ # @api private
47
+ # @param string [String] JSON string to parse
48
+ # @param options [Hash] parsing options
49
+ # @return [Object] parsed Ruby object
50
+ #
51
+ # @example Parse JSON string
52
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
32
53
  def load(string, options = {})
33
- options[:symbol_keys] = options[:symbolize_keys]
34
- ::Oj.load(string, options)
54
+ ::Oj.load(string, translate_load_options(options))
35
55
  end
36
56
 
37
- case ::Oj::VERSION
38
- when /\A2\./
39
- def dump(object, options = {})
40
- options.merge!(:indent => 2) if options[:pretty]
41
- options[:indent] = options[:indent].to_i if options[:indent]
42
- ::Oj.dump(object, options)
43
- end
44
- when /\A3\./
45
- PRETTY_STATE_PROTOTYPE = {
46
- :indent => " ",
47
- :space => " ",
48
- :space_before => "",
49
- :object_nl => "\n",
50
- :array_nl => "\n",
51
- :ascii_only => false,
52
- }
53
-
54
- def dump(object, options = {})
55
- options.merge!(PRETTY_STATE_PROTOTYPE.dup) if options.delete(:pretty)
56
- ::Oj.dump(object, options)
57
- end
58
- else
59
- fail "Unsupported Oj version: #{::Oj::VERSION}"
57
+ # Serialize a Ruby object to JSON
58
+ #
59
+ # @api private
60
+ # @param object [Object] object to serialize
61
+ # @param options [Hash] serialization options
62
+ # @return [String] JSON string
63
+ #
64
+ # @example Serialize object to JSON
65
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
66
+ def dump(object, options = {})
67
+ ::Oj.dump(object, prepare_dump_options(options))
68
+ end
69
+
70
+ private
71
+
72
+ # Translate ``:symbolize_names`` 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_names`` translated
84
+ def translate_load_options(options)
85
+ options.except(:symbolize_names).merge(symbol_keys: options[:symbolize_names] == true)
60
86
  end
61
87
  end
62
88
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
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
11
+ module OjCommon
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
+
22
+ private
23
+
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.
30
+ #
31
+ # @api private
32
+ # @param options [Hash] serialization options
33
+ # @return [Hash] processed options for Oj.dump
34
+ #
35
+ # @example Prepare dump options
36
+ # prepare_dump_options(pretty: true)
37
+ def prepare_dump_options(options)
38
+ return options unless options.key?(:pretty)
39
+
40
+ options.except(:pretty).merge(PRETTY_STATE_PROTOTYPE)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,16 +1,37 @@
1
- require 'yajl'
2
- require 'multi_json/adapter'
1
+ # frozen_string_literal: true
3
2
 
4
- module MultiJson
3
+ require "yajl"
4
+ require_relative "../adapter"
5
+
6
+ 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
 
13
+ # Parse a JSON string into a Ruby object
14
+ #
15
+ # @api private
16
+ # @param string [String] JSON string to parse
17
+ # @param options [Hash] parsing options
18
+ # @return [Object] parsed Ruby object
19
+ #
20
+ # @example Parse JSON string
21
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
10
22
  def load(string, options = {})
11
- ::Yajl::Parser.new(:symbolize_keys => options[:symbolize_keys]).parse(string)
23
+ ::Yajl::Parser.new(options).parse(string)
12
24
  end
13
25
 
26
+ # Serialize a Ruby object to JSON
27
+ #
28
+ # @api private
29
+ # @param object [Object] object to serialize
30
+ # @param options [Hash] serialization options
31
+ # @return [String] JSON string
32
+ #
33
+ # @example Serialize object to JSON
34
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
14
35
  def dump(object, options = {})
15
36
  ::Yajl::Encoder.encode(object, options)
16
37
  end
@@ -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