multi_json 1.19.1 → 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.
data/lib/multi_json.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "multi_json/concurrency"
1
4
  require_relative "multi_json/options"
2
5
  require_relative "multi_json/version"
3
6
  require_relative "multi_json/adapter_error"
@@ -7,229 +10,311 @@ require_relative "multi_json/adapter_selector"
7
10
 
8
11
  # A unified interface for JSON libraries in Ruby
9
12
  #
10
- # MultiJson allows swapping between JSON backends without changing your code.
13
+ # MultiJSON allows swapping between JSON backends without changing your code.
11
14
  # It auto-detects available JSON libraries and uses the fastest one available.
12
15
  #
16
+ # ## Method-definition patterns
17
+ #
18
+ # The current public API uses two patterns, each chosen for a specific reason:
19
+ #
20
+ # 1. ``module_function`` creates both a class method and a private instance
21
+ # method from a single ``def``. This is used for the hot-path API
22
+ # (``adapter``, ``use``, ``adapter=``, ``parse``, ``generate``,
23
+ # ``current_adapter``) so that both ``MultiJSON.parse(...)`` and legacy
24
+ # ``Class.new { include MultiJSON }.new.send(:parse, ...)`` invocations
25
+ # work through the same body. The instance versions are re-publicized
26
+ # below so YARD renders them as part of the public API.
27
+ # 2. ``def self.foo`` creates only a singleton method, giving mutation
28
+ # testing a single canonical definition to target. This is used for
29
+ # {.with_adapter}, which needs precise mutation coverage of its
30
+ # fiber-local save/restore logic.
31
+ #
32
+ # Deprecated public API (``decode``, ``encode``, ``engine``, ``load``,
33
+ # ``dump``, etc.) lives in {file:lib/multi_json/deprecated.rb} so this
34
+ # file stays focused on the current surface.
35
+ #
13
36
  # @example Basic usage
14
- # MultiJson.load('{"foo":"bar"}') #=> {"foo" => "bar"}
15
- # MultiJson.dump({foo: "bar"}) #=> '{"foo":"bar"}'
37
+ # MultiJSON.parse('{"foo":"bar"}') #=> {"foo" => "bar"}
38
+ # MultiJSON.generate({foo: "bar"}) #=> '{"foo":"bar"}'
16
39
  #
17
40
  # @example Specifying an adapter
18
- # MultiJson.use(:oj)
19
- # MultiJson.load('{"foo":"bar"}', adapter: :json_gem)
41
+ # MultiJSON.use(:oj)
42
+ # MultiJSON.parse('{"foo":"bar"}', adapter: :json_gem)
20
43
  #
21
44
  # @api public
22
- module MultiJson
45
+ module MultiJSON
23
46
  extend Options
24
47
  extend AdapterSelector
25
48
 
26
- # @!visibility private
27
- module_function
28
-
29
- # @!group Configuration
49
+ # Tracks which deprecation warnings have already been emitted so each one
50
+ # fires at most once per process. Stored as a Set rather than a Hash so
51
+ # presence checks have unambiguous semantics for mutation tests.
52
+ DEPRECATION_WARNINGS_SHOWN = Set.new
53
+ private_constant :DEPRECATION_WARNINGS_SHOWN
30
54
 
31
- # Set default options for both load and dump operations
55
+ # Emit a deprecation warning at most once per process for the given key
32
56
  #
33
- # @api private
34
- # @deprecated Use {.load_options=} and {.dump_options=} instead
35
- # @param value [Hash] options hash
36
- # @return [Hash] the options hash
37
- # @example
38
- # MultiJson.default_options = {symbolize_keys: true}
39
- def default_options=(value)
40
- Kernel.warn "MultiJson.default_options setter is deprecated\n" \
41
- "Use MultiJson.load_options and MultiJson.dump_options instead"
42
- self.load_options = self.dump_options = value
43
- end
44
-
45
- # Get the default options
57
+ # Defined as a singleton method (rather than via module_function) so
58
+ # there is exactly one definition for mutation tests to target.
59
+ # Public so the deprecated ``load_options`` / ``dump_options``
60
+ # aliases on the {Options} mixin can invoke it without routing
61
+ # through ``MultiJSON.send(...)``.
62
+ #
63
+ # The warning is tagged with the ``:deprecated`` category so callers
64
+ # can silence the whole set with ``Warning[:deprecated] = false`` or
65
+ # surface it via ``ruby -W:deprecated`` — the standard Ruby idiom for
66
+ # library deprecations since 2.7.
46
67
  #
47
68
  # @api private
48
- # @deprecated Use {.load_options} or {.dump_options} instead
49
- # @return [Hash] the current load options
69
+ # @param key [Symbol] identifier for the deprecation (typically the method name)
70
+ # @param message [String] warning message to emit on first call
71
+ # @return [void]
50
72
  # @example
51
- # MultiJson.default_options #=> {}
52
- def default_options
53
- Kernel.warn "MultiJson.default_options is deprecated\n" \
54
- "Use MultiJson.load_options or MultiJson.dump_options instead"
55
- load_options
56
- end
73
+ # MultiJSON.warn_deprecation_once(:foo, "MultiJSON.foo is deprecated")
74
+ def self.warn_deprecation_once(key, message)
75
+ Concurrency.synchronize(:deprecation_warnings) do
76
+ return if DEPRECATION_WARNINGS_SHOWN.include?(key)
57
77
 
58
- # @deprecated These methods are no longer used
59
- %w[cached_options reset_cached_options!].each do |method_name|
60
- define_method(method_name) do |*|
61
- Kernel.warn "MultiJson.#{method_name} method is deprecated and no longer used."
78
+ Kernel.warn(message, category: :deprecated)
79
+ DEPRECATION_WARNINGS_SHOWN.add(key)
62
80
  end
63
81
  end
64
82
 
65
- # Legacy alias for adapter name mappings
66
- ALIASES = AdapterSelector::ALIASES
67
-
68
- # Maps adapter symbols to their require paths for auto-loading
69
- REQUIREMENT_MAP = {
70
- fast_jsonparser: "fast_jsonparser",
71
- oj: "oj",
72
- yajl: "yajl",
73
- jr_jackson: "jrjackson",
74
- json_gem: "json",
75
- gson: "gson"
76
- }.freeze
83
+ # Resolve the ``ParseError`` constant for an adapter class
84
+ #
85
+ # The result is memoized on the adapter class itself in a
86
+ # ``@_multi_json_parse_error`` ivar so subsequent ``MultiJSON.load``
87
+ # calls skip the constant lookup entirely. The lookup is performed
88
+ # with ``inherit: false`` so a stray top-level ``::ParseError``
89
+ # constant in the host process is correctly ignored on every
90
+ # supported Ruby implementation — TruffleRuby's ``::`` operator
91
+ # walks the ancestor chain and would otherwise pick up the top-level
92
+ # constant. Custom adapters that don't define their own
93
+ # ``ParseError`` get a clear {AdapterError} instead of the bare
94
+ # ``NameError`` Ruby would raise from the rescue clause.
95
+ #
96
+ # @api private
97
+ # @param adapter_class [Class] adapter class to inspect
98
+ # @return [Class] the adapter's ParseError class
99
+ # @raise [AdapterError] when the adapter doesn't define ParseError
100
+ def self.parse_error_class_for(adapter_class)
101
+ cached = adapter_class.instance_variable_get(:@_multi_json_parse_error)
102
+ return cached if cached
77
103
 
78
- class << self
79
- # Returns the default adapter name (alias for default_adapter)
80
- #
81
- # @api public
82
- # @deprecated Use {.default_adapter} instead
83
- # @return [Symbol] the default adapter name
84
- # @example
85
- # MultiJson.default_engine #=> :oj
86
- alias_method :default_engine, :default_adapter
104
+ resolved = adapter_class.const_get(:ParseError, false)
105
+ adapter_class.instance_variable_set(:@_multi_json_parse_error, resolved)
106
+ rescue NameError
107
+ raise AdapterError, "Adapter #{adapter_class} must define a ParseError constant"
87
108
  end
88
109
 
89
- # @!endgroup
110
+ # ===========================================================================
111
+ # Public API (module_function: class + private instance method)
112
+ # ===========================================================================
90
113
 
91
- # @!group Adapter Management
114
+ # @!visibility private
115
+ module_function
92
116
 
93
117
  # Returns the current adapter class
94
118
  #
95
- # @api private
119
+ # Honors a fiber-local override set by {.with_adapter} so concurrent
120
+ # blocks observe their own adapter without clobbering the process-wide
121
+ # default. Falls back to the process default when no override is set.
122
+ #
123
+ # @api public
96
124
  # @return [Class] the current adapter class
97
125
  # @example
98
- # MultiJson.adapter #=> MultiJson::Adapters::Oj
126
+ # MultiJSON.adapter #=> MultiJSON::Adapters::Oj
99
127
  def adapter
128
+ override = Fiber[:multi_json_adapter]
129
+ return override if override
130
+
100
131
  @adapter ||= use(nil)
101
132
  end
102
133
 
103
- # Returns the current adapter class (alias for adapter)
104
- #
105
- # @api private
106
- # @deprecated Use {.adapter} instead
107
- # @return [Class] the current adapter class
108
- # @example
109
- # MultiJson.engine #=> MultiJson::Adapters::Oj
110
- alias_method :engine, :adapter
111
-
112
134
  # Sets the adapter to use for JSON operations
113
135
  #
114
- # @api private
136
+ # The merged-options cache is only reset when the new adapter loads
137
+ # successfully. A failed ``use(:nonexistent)`` leaves the cache in
138
+ # place so the previously-active adapter keeps its cached entries.
139
+ #
140
+ # @api public
115
141
  # @param new_adapter [Symbol, String, Module, nil] adapter specification
116
142
  # @return [Class] the loaded adapter class
117
143
  # @example
118
- # MultiJson.use(:oj)
144
+ # MultiJSON.use(:oj)
119
145
  def use(new_adapter)
120
- @adapter = load_adapter(new_adapter)
121
- ensure
122
- OptionsCache.reset
146
+ loaded = load_adapter(new_adapter)
147
+ Concurrency.synchronize(:adapter) do
148
+ OptionsCache.reset
149
+ @adapter = loaded
150
+ end
123
151
  end
124
152
 
125
153
  # Sets the adapter to use for JSON operations
126
154
  #
127
- # @api private
155
+ # @api public
128
156
  # @return [Class] the loaded adapter class
129
157
  # @example
130
- # MultiJson.adapter = :json_gem
158
+ # MultiJSON.adapter = :json_gem
131
159
  alias_method :adapter=, :use
132
-
133
- # Sets the adapter to use for JSON operations
134
- #
135
- # @api private
136
- # @deprecated Use {.adapter=} instead
137
- # @return [Class] the loaded adapter class
138
- # @example
139
- # MultiJson.engine = :json_gem
140
- alias_method :engine=, :use
141
- module_function :adapter=, :engine=
142
-
143
- # @!endgroup
144
-
145
- # @!group JSON Operations
160
+ module_function :adapter=
146
161
 
147
162
  # Parses a JSON string into a Ruby object
148
163
  #
149
- # @api private
164
+ # Returns ``nil`` for ``nil``, empty, and whitespace-only inputs
165
+ # instead of raising. Pass an explicit non-blank string if you want
166
+ # to surface a {ParseError} for empty payloads at the call site.
167
+ #
168
+ # @api public
150
169
  # @param string [String, #read] JSON string or IO-like object
151
170
  # @param options [Hash] parsing options (adapter-specific)
152
- # @return [Object] parsed Ruby object
171
+ # @return [Object, nil] parsed Ruby object, or nil for blank input
153
172
  # @raise [ParseError] if parsing fails
173
+ # @raise [AdapterError] if the adapter doesn't define a ``ParseError`` constant
154
174
  # @example
155
- # MultiJson.load('{"foo":"bar"}') #=> {"foo" => "bar"}
156
- def load(string, options = {})
175
+ # MultiJSON.parse('{"foo":"bar"}') #=> {"foo" => "bar"}
176
+ # MultiJSON.parse("") #=> nil
177
+ # MultiJSON.parse(" \n") #=> nil
178
+ def parse(string, options = {})
157
179
  adapter_class = current_adapter(options)
158
- adapter_class.load(string, options)
159
- rescue adapter_class::ParseError => e
160
- raise ParseError.build(e, string)
180
+ parse_error_class = MultiJSON.parse_error_class_for(adapter_class)
181
+ begin
182
+ adapter_class.load(string, options)
183
+ rescue parse_error_class => e
184
+ raise ParseError.build(e, string)
185
+ end
161
186
  end
162
187
 
163
- # Parses a JSON string into a Ruby object
164
- #
165
- # @api private
166
- # @deprecated Use {.load} instead
167
- # @return [Object] parsed Ruby object
168
- # @example
169
- # MultiJson.decode('{"foo":"bar"}') #=> {"foo" => "bar"}
170
- alias_method :decode, :load
171
- module_function :decode
172
-
173
188
  # Returns the adapter to use for the given options
174
189
  #
175
- # @api private
176
- # @param options [Hash] options that may contain :adapter key
190
+ # ``nil`` is accepted as a no-options sentinel — explicit
191
+ # ``current_adapter(nil)`` calls fall through to the process default
192
+ # adapter without raising.
193
+ #
194
+ # @api public
195
+ # @param options [Hash, nil] options that may contain :adapter key, or
196
+ # nil to use the process default
177
197
  # @return [Class] adapter class
178
198
  # @example
179
- # MultiJson.current_adapter(adapter: :oj) #=> MultiJson::Adapters::Oj
199
+ # MultiJSON.current_adapter(adapter: :oj) #=> MultiJSON::Adapters::Oj
180
200
  def current_adapter(options = {})
181
- options ||= {}
201
+ options ||= Options::EMPTY_OPTIONS
182
202
  adapter_override = options[:adapter]
183
203
  adapter_override ? load_adapter(adapter_override) : adapter
184
204
  end
185
205
 
186
206
  # Serializes a Ruby object to a JSON string
187
207
  #
188
- # @api private
208
+ # @api public
189
209
  # @param object [Object] object to serialize
190
210
  # @param options [Hash] serialization options (adapter-specific)
191
211
  # @return [String] JSON string
192
212
  # @example
193
- # MultiJson.dump({foo: "bar"}) #=> '{"foo":"bar"}'
194
- def dump(object, options = {})
213
+ # MultiJSON.generate({foo: "bar"}) #=> '{"foo":"bar"}'
214
+ def generate(object, options = {})
195
215
  current_adapter(options).dump(object, options)
196
216
  end
197
217
 
198
- # Serializes a Ruby object to a JSON string
199
- #
200
- # @api private
201
- # @deprecated Use {.dump} instead
202
- # @return [String] JSON string
203
- # @example
204
- # MultiJson.encode({foo: "bar"}) #=> '{"foo":"bar"}'
205
- alias_method :encode, :dump
206
- module_function :encode
218
+ # Re-publicize the instance versions of the module_function methods so
219
+ # YARD/yardstick render them as part of the public API and legacy
220
+ # ``include MultiJSON`` consumers can call them without ``.send``.
221
+ public :adapter, :use, :adapter=, :parse, :current_adapter, :generate
222
+
223
+ # ===========================================================================
224
+ # Public API (def self.foo: singleton-only, for mutation-test precision)
225
+ # ===========================================================================
207
226
 
208
227
  # Executes a block using the specified adapter
209
228
  #
210
- # @api private
229
+ # Defined as a singleton method so mutation testing has exactly one
230
+ # definition to target. The override is stored in fiber-local storage
231
+ # so concurrent fibers and threads each see their own adapter without
232
+ # racing on a shared module variable; nested calls save and restore
233
+ # the previous fiber-local value.
234
+ #
235
+ # @api public
211
236
  # @param new_adapter [Symbol, String, Module] adapter to use
212
237
  # @yield block to execute with the temporary adapter
213
238
  # @return [Object] result of the block
214
239
  # @example
215
- # MultiJson.with_adapter(:json_gem) { MultiJson.dump({}) }
216
- def with_adapter(new_adapter)
217
- previous_adapter = adapter
218
- self.adapter = new_adapter
240
+ # MultiJSON.with_adapter(:json_gem) { MultiJSON.dump({}) }
241
+ def self.with_adapter(new_adapter)
242
+ previous_override = Fiber[:multi_json_adapter]
243
+ Fiber[:multi_json_adapter] = load_adapter(new_adapter)
219
244
  yield
220
245
  ensure
221
- self.adapter = previous_adapter
246
+ Fiber[:multi_json_adapter] = previous_override
222
247
  end
223
248
 
224
- # Executes a block using the specified adapter
249
+ # ===========================================================================
250
+ # Private instance-method delegates for the singleton-only methods above
251
+ # ===========================================================================
252
+
253
+ private
254
+
255
+ # Instance-method delegate for {MultiJSON.with_adapter}
225
256
  #
226
257
  # @api private
227
- # @deprecated Use {.with_adapter} instead
258
+ # @param new_adapter [Symbol, String, Module] adapter to use
259
+ # @yield block to execute with the temporary adapter
228
260
  # @return [Object] result of the block
229
261
  # @example
230
- # MultiJson.with_engine(:json_gem) { MultiJson.dump({}) }
231
- alias_method :with_engine, :with_adapter
232
- module_function :with_engine
262
+ # class Foo; include MultiJSON; end
263
+ # Foo.new.send(:with_adapter, :json_gem) { ... }
264
+ def with_adapter(new_adapter, &)
265
+ MultiJSON.with_adapter(new_adapter, &)
266
+ end
267
+ end
233
268
 
234
- # @!endgroup
269
+ require_relative "multi_json/deprecated"
270
+
271
+ # Backward-compatible alias for the legacy ``MultiJson`` constant name
272
+ #
273
+ # Downstream code that still writes ``MultiJson.parse(...)`` or
274
+ # ``rescue MultiJson::ParseError`` continues to work, but emits a
275
+ # one-time deprecation warning pointing at ``MultiJSON``. Each public
276
+ # method on {MultiJSON} gets an explicit forwarder defined on this
277
+ # module, and constant access resolves via {.const_missing}, so both
278
+ # dotted calls and ``::`` constant lookups (including rescue clauses)
279
+ # route through the canonical module.
280
+ #
281
+ # @api public
282
+ # @deprecated Use {MultiJSON} (all-caps) instead. Will be removed in v2.0.
283
+ module MultiJson
284
+ # Forward every public method MultiJSON exposes through an explicit
285
+ # singleton method on the legacy MultiJson module, so callers that
286
+ # capture the method as a Method object (``MultiJson.method(:load)``)
287
+ # find this forwarder instead of falling back to inherited methods like
288
+ # ``Kernel#load``. The earlier ``method_missing``-based shim left
289
+ # ``MultiJson.method(:load)`` resolving to ``Kernel#load`` (because
290
+ # ``Module#method`` doesn't consult ``method_missing``) and broke
291
+ # libraries (Sawyer, Octokit, Danger) that capture decoders as Method
292
+ # objects. Forwarding eagerly fixes the capture path while preserving
293
+ # the one-time deprecation warning each call emits.
294
+ (::MultiJSON.public_methods - ::Module.public_methods).each do |forwarded|
295
+ define_singleton_method(forwarded) do |*args, **kwargs, &block|
296
+ ::MultiJSON.warn_deprecation_once(:multi_json_constant,
297
+ "The MultiJson constant is deprecated and will be removed in v2.0. Use MultiJSON instead.")
298
+ ::MultiJSON.public_send(forwarded, *args, **kwargs, &block)
299
+ end
300
+ end
301
+
302
+ class << self
303
+ # Resolve missing constants to their {MultiJSON} counterparts
304
+ #
305
+ # Enables ``rescue MultiJson::ParseError`` and
306
+ # ``MultiJson::Adapters::Oj`` to keep working during the
307
+ # deprecation cycle.
308
+ #
309
+ # @api public
310
+ # @param name [Symbol] constant name
311
+ # @return [Object] the resolved constant from {MultiJSON}
312
+ # @example
313
+ # MultiJson::ParseError # returns MultiJSON::ParseError
314
+ def const_missing(name)
315
+ ::MultiJSON.warn_deprecation_once(:multi_json_constant,
316
+ "The MultiJson constant is deprecated and will be removed in v2.0. Use MultiJSON instead.")
317
+ ::MultiJSON.const_get(name)
318
+ end
319
+ end
235
320
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.1
4
+ version: 1.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Bleigh
@@ -13,15 +13,13 @@ cert_chain: []
13
13
  date: 1980-01-02 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: A common interface to multiple JSON libraries, including fast_jsonparser,
16
- Oj, Yajl, the JSON gem (with C-extensions), gson, JrJackson, and OkJson.
16
+ Oj, Yajl, and the JSON gem.
17
17
  email:
18
18
  - sferik@gmail.com
19
19
  executables: []
20
20
  extensions: []
21
21
  extra_rdoc_files: []
22
22
  files:
23
- - CHANGELOG.md
24
- - CONTRIBUTING.md
25
23
  - LICENSE.md
26
24
  - README.md
27
25
  - lib/multi_json.rb
@@ -29,28 +27,26 @@ files:
29
27
  - lib/multi_json/adapter_error.rb
30
28
  - lib/multi_json/adapter_selector.rb
31
29
  - lib/multi_json/adapters/fast_jsonparser.rb
32
- - lib/multi_json/adapters/gson.rb
33
- - lib/multi_json/adapters/jr_jackson.rb
34
30
  - lib/multi_json/adapters/json_gem.rb
35
31
  - lib/multi_json/adapters/oj.rb
36
32
  - lib/multi_json/adapters/oj_common.rb
37
- - lib/multi_json/adapters/ok_json.rb
38
33
  - lib/multi_json/adapters/yajl.rb
39
- - lib/multi_json/convertible_hash_keys.rb
34
+ - lib/multi_json/concurrency.rb
35
+ - lib/multi_json/deprecated.rb
40
36
  - lib/multi_json/options.rb
41
37
  - lib/multi_json/options_cache.rb
38
+ - lib/multi_json/options_cache/mutex_store.rb
42
39
  - lib/multi_json/parse_error.rb
43
- - lib/multi_json/vendor/okjson.rb
44
40
  - lib/multi_json/version.rb
45
41
  homepage: https://github.com/sferik/multi_json
46
42
  licenses:
47
43
  - MIT
48
44
  metadata:
49
45
  bug_tracker_uri: https://github.com/sferik/multi_json/issues
50
- changelog_uri: https://github.com/sferik/multi_json/blob/v1.19.1/CHANGELOG.md
51
- documentation_uri: https://www.rubydoc.info/gems/multi_json/1.19.1
46
+ changelog_uri: https://github.com/sferik/multi_json/blob/v1.21.1/CHANGELOG.md
47
+ documentation_uri: https://www.rubydoc.info/gems/multi_json/1.21.1
52
48
  rubygems_mfa_required: 'true'
53
- source_code_uri: https://github.com/sferik/multi_json/tree/v1.19.1
49
+ source_code_uri: https://github.com/sferik/multi_json/tree/v1.21.1
54
50
  wiki_uri: https://github.com/sferik/multi_json/wiki
55
51
  rdoc_options: []
56
52
  require_paths:
@@ -59,14 +55,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
59
55
  requirements:
60
56
  - - ">="
61
57
  - !ruby/object:Gem::Version
62
- version: '3.0'
58
+ version: '3.2'
63
59
  required_rubygems_version: !ruby/object:Gem::Requirement
64
60
  requirements:
65
61
  - - ">="
66
62
  - !ruby/object:Gem::Version
67
63
  version: '0'
68
64
  requirements: []
69
- rubygems_version: 4.0.3
65
+ rubygems_version: 4.0.11
70
66
  specification_version: 4
71
67
  summary: A common interface to multiple JSON libraries.
72
68
  test_files: []