multi_json 1.15.0 → 1.20.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,194 @@
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
+ 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"}
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
34
+
35
+ # Returns the default adapter to use
36
+ #
37
+ # @api private
38
+ # @return [Symbol] adapter name
39
+ # @example
40
+ # AdapterSelector.default_adapter #=> :oj
41
+ def default_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
63
+ end
64
+
65
+ private
66
+
67
+ # Detects the best available JSON adapter
68
+ #
69
+ # @api private
70
+ # @return [Symbol] adapter name
71
+ def detect_best_adapter
72
+ loaded_adapter || installable_adapter || fallback_adapter
73
+ end
74
+
75
+ # Finds an already-loaded JSON library
76
+ #
77
+ # @api private
78
+ # @param excluding [Symbol, nil] adapter name to skip during detection
79
+ # @return [Symbol, nil] adapter name if found
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
86
+ end
87
+
88
+ # Tries to require and use an installable adapter
89
+ #
90
+ # @api private
91
+ # @param excluding [Symbol, nil] adapter name to skip during detection
92
+ # @return [Symbol, nil] adapter name if successfully required
93
+ def installable_adapter(excluding: nil)
94
+ REQUIREMENT_MAP.each_key do |adapter_name|
95
+ next if adapter_name == excluding
96
+ return adapter_name if try_require(adapter_name)
97
+ end
98
+ nil
99
+ end
100
+
101
+ # Attempts to require a JSON library
102
+ #
103
+ # @api private
104
+ # @param adapter_name [Symbol] adapter to require
105
+ # @return [Boolean] true if require succeeded
106
+ def try_require(adapter_name)
107
+ require REQUIREMENT_MAP.fetch(adapter_name)
108
+ true
109
+ rescue ::LoadError
110
+ false
111
+ end
112
+
113
+ # Returns the fallback adapter when no others available
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
+ #
120
+ # @api private
121
+ # @return [Symbol] the json_gem adapter name
122
+ def fallback_adapter
123
+ warn_about_fallback unless @default_adapter_warning_shown
124
+ @default_adapter_warning_shown = true
125
+ :json_gem
126
+ end
127
+
128
+ # Warns the user about reaching the last-resort fallback
129
+ #
130
+ # @api private
131
+ # @return [void]
132
+ def warn_about_fallback
133
+ Kernel.warn(
134
+ "[WARNING] MultiJson is falling back to the json_gem adapter " \
135
+ "because no other JSON library could be loaded."
136
+ )
137
+ end
138
+
139
+ # Loads an adapter from a specification
140
+ #
141
+ # @api private
142
+ # @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
143
+ # @return [Class] the adapter class
144
+ def load_adapter(adapter_spec)
145
+ adapter = 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
152
+ validate_adapter!(adapter)
153
+ rescue ::LoadError => e
154
+ raise AdapterError.build(e)
155
+ end
156
+
157
+ # Loads an adapter by its string name
158
+ #
159
+ # ``jrjackson`` (the JrJackson gem's name) is normalized to
160
+ # ``jr_jackson`` (the adapter file/class name) for backwards
161
+ # compatibility with the original gem-name alias.
162
+ #
163
+ # @api private
164
+ # @param name [String] adapter name
165
+ # @return [Class] the adapter class
166
+ def load_adapter_by_name(name)
167
+ normalized = name.downcase
168
+ normalized = "jr_jackson" if normalized == "jrjackson"
169
+ require_relative "adapters/#{normalized}"
170
+
171
+ class_name = normalized.split("_").map(&:capitalize).join
172
+ ::MultiJson::Adapters.const_get(class_name)
173
+ end
174
+
175
+ # Validate that an adapter satisfies the documented contract
176
+ #
177
+ # Custom adapters are accepted as modules/classes, so fail fast
178
+ # during adapter resolution rather than later on the first load or
179
+ # dump call.
180
+ #
181
+ # @api private
182
+ # @param adapter [Module] adapter class or module
183
+ # @return [Module] the validated adapter
184
+ # @raise [AdapterError] when the adapter is missing a required class method
185
+ # or ParseError constant
186
+ def validate_adapter!(adapter)
187
+ raise AdapterError, "Adapter #{adapter} must respond to .load" unless adapter.respond_to?(:load)
188
+ raise AdapterError, "Adapter #{adapter} must respond to .dump" unless adapter.respond_to?(:dump)
189
+
190
+ MultiJson.parse_error_class_for(adapter)
191
+ adapter
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,72 @@
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_keys: 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
+ # 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
+ #
60
+ # @api private
61
+ # @param string [String] JSON string to parse
62
+ # @param options [Hash] parsing options (only :symbolize_keys is honored)
63
+ # @return [Object] parsed Ruby object
64
+ #
65
+ # @example Parse JSON string
66
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
67
+ def load(string, options = {})
68
+ ::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_keys])
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,11 +1,89 @@
1
- require 'json/ext'
2
- require 'multi_json/adapters/json_common'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../adapter"
4
+ require "json"
3
5
 
4
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, translate_load_options(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
70
+
71
+ private
72
+
73
+ # Translate ``:symbolize_keys`` into JSON gem's ``:symbolize_names``
74
+ #
75
+ # Returns a new hash without mutating the input. ``options`` is the
76
+ # cached hash returned from {Adapter.merged_load_options}, so in-place
77
+ # edits would pollute the cache and corrupt subsequent calls.
78
+ #
79
+ # @api private
80
+ # @param options [Hash] merged load options
81
+ # @return [Hash] options with ``:symbolize_keys`` translated
82
+ def translate_load_options(options)
83
+ return options unless options[:symbolize_keys]
84
+
85
+ options.except(:symbolize_keys).merge(symbolize_names: true)
86
+ end
9
87
  end
10
88
  end
11
89
  end
@@ -1,62 +1,88 @@
1
- require 'set'
2
- require 'oj'
3
- require 'multi_json/adapter'
1
+ # frozen_string_literal: true
2
+
3
+ require "oj"
4
+ require_relative "../adapter"
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
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_keys: 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_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)
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
2
+
3
+ require "yajl"
4
+ require_relative "../adapter"
3
5
 
4
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