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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d32c6ecf7a376a7280adac3692fd9b256d0934b1cff4724955b815a76a8b8d1
4
- data.tar.gz: f273e11d29ac7ec97954410f1b9af76d4e3f43a299c0507583371e15d01af0cd
3
+ metadata.gz: 7cffb1170fe976efac86851b47b4db7208fb1eefc745ea73e6cf4aaeb4aaa281
4
+ data.tar.gz: d5b3f218ad72f318eb5c86152c87005f27f7fc3b4b265561f1156092cbbd6c4b
5
5
  SHA512:
6
- metadata.gz: 01ae1a673f2ccb2bbe25ee7e960f1e2984859495c50bdeeab4e1c5aa0315915f2b8f815b88a23bd5de781fed52d35b0ab8f0986a1aad8d74a3e055d554347107
7
- data.tar.gz: 69f7aa6acd371a2852f12a616d0501f33acd85c01ac0204669ef4c30ebb058b7399cccb4b0fa830c41ec0a4cdd2ad8273ca134d9915b59dc53c4f5c4f6bf664c
6
+ metadata.gz: a61eb7a6f29720708c0944b8288ed8a9ce1917f76d9934f3f417935a606fa3da0c8e6e1b8158a57420c49b97ba617222ff3a5e1ed28bbfc23c7146f46e32d0ce
7
+ data.tar.gz: fc6f0c205a0c6a05740a08de6c3a27d59e482e64f179591b47a718a84d02b7d509e1ffd368d28c6d5edc88a454bb8ce4c64cd784d7283cc4353c82f7d1f2919d
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010-2025 Michael Bleigh, Josh Kalderimis, Erik Berlin, Pavel Pravosud
1
+ Copyright (c) 2010-2026 Erik Berlin, Michael Bleigh, Josh Kalderimis, Pavel Pravosud
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # MultiJSON
2
2
 
3
- [![Gem Version](http://img.shields.io/gem/v/multi_json.svg)][gem]
4
- [![Build Status](https://github.com/sferik/multi_json/actions/workflows/tests.yml/badge.svg)][build]
3
+ [![Tests](https://github.com/sferik/multi_json/actions/workflows/tests.yml/badge.svg)][tests]
4
+ [![Linter](https://github.com/sferik/multi_json/actions/workflows/linter.yml/badge.svg)][linter]
5
+ [![Mutant](https://github.com/sferik/multi_json/actions/workflows/mutant.yml/badge.svg)][mutant]
6
+ [![Typecheck](https://github.com/sferik/multi_json/actions/workflows/typecheck.yml/badge.svg)][typecheck]
7
+ [![Docs](https://github.com/sferik/multi_json/actions/workflows/docs.yml/badge.svg)][docs]
5
8
  [![Maintainability](https://qlty.sh/badges/fde3f4a8-c331-44be-b1e6-45842137def9/maintainability.svg)][qlty]
9
+ [![Gem Version](https://badge.fury.io/rb/multi_json.svg)][gem]
6
10
 
7
11
  Lots of Ruby libraries parse JSON and everyone has their favorite JSON coder.
8
12
  Instead of choosing a single JSON coder and forcing users of your library to be
@@ -10,63 +14,217 @@ stuck with it, you can use MultiJSON instead, which will simply choose the
10
14
  fastest available JSON coder. Here's how to use it:
11
15
 
12
16
  ```ruby
13
- require 'multi_json'
17
+ require "multi_json"
14
18
 
15
- MultiJson.load('{"abc":"def"}') #=> {"abc" => "def"}
16
- MultiJson.load('{"abc":"def"}', :symbolize_keys => true) #=> {:abc => "def"}
17
- MultiJson.dump({:abc => 'def'}) # convert Ruby back to JSON
18
- MultiJson.dump({:abc => 'def'}, :pretty => true) # encoded in a pretty form (if supported by the coder)
19
+ MultiJSON.parse('{"abc":"def"}') #=> {"abc" => "def"}
20
+ MultiJSON.parse('{"abc":"def"}', symbolize_names: true) #=> {abc: "def"}
21
+ MultiJSON.generate({abc: "def"}) # convert Ruby back to JSON
22
+ MultiJSON.generate({abc: "def"}, pretty: true) # encoded in a pretty form (if supported by the coder)
19
23
  ```
20
24
 
21
- When loading invalid JSON, MultiJSON will throw a `MultiJson::ParseError`. `MultiJson::DecodeError` and `MultiJson::LoadError` are aliases for backwards compatibility.
25
+ > [!IMPORTANT]
26
+ > **1.21.0 renames the public API to match Ruby stdlib `JSON`.** The canonical
27
+ > verbs are now `MultiJSON.parse` / `MultiJSON.generate`, and the canonical
28
+ > module is `MultiJSON` (all-caps). The legacy `MultiJson` constant,
29
+ > `MultiJSON.load` / `MultiJSON.dump`, `:symbolize_keys`, and friends still
30
+ > work but emit one-time deprecation warnings and **will be removed in 2.0.0**.
31
+ > Run your app with `ruby -W:deprecated` to surface them; the warnings are
32
+ > tagged with the `:deprecated` category so you can silence the whole set with
33
+ > `Warning[:deprecated] = false`. See [Deprecated in 1.21.0](#deprecated-in-1210)
34
+ > for the full list.
35
+
36
+ `MultiJSON.parse` returns `nil` for `nil`, empty, and whitespace-only inputs
37
+ instead of raising, so a missing or blank payload is observable as a `nil`
38
+ return value rather than an exception. When parsing invalid JSON, MultiJSON
39
+ will throw a `MultiJSON::ParseError`. `MultiJSON::DecodeError` and
40
+ `MultiJSON::LoadError` are aliases for backwards compatibility.
22
41
 
23
42
  ```ruby
24
43
  begin
25
- MultiJson.load('{invalid json}')
26
- rescue MultiJson::ParseError => exception
27
- exception.data # => "{invalid json}"
28
- exception.cause # => JSON::ParserError: 795: unexpected token at '{invalid json}'
44
+ MultiJSON.parse("{invalid json}")
45
+ rescue MultiJSON::ParseError => exception
46
+ exception.data #=> "{invalid json}"
47
+ exception.cause #=> JSON::ParserError: ...
48
+ exception.line #=> 1 (for adapters that report a location, e.g. Oj or the json gem)
49
+ exception.column #=> 2
29
50
  end
30
51
  ```
31
52
 
53
+ ### Drop-in replacement for stdlib `JSON`
54
+
55
+ MultiJSON mirrors the surface of Ruby's stdlib [`JSON`][json-gem] so
56
+ most call sites swap in with a one-line change:
57
+
58
+ ```diff
59
+ - require "json"
60
+ + require "multi_json"
61
+
62
+ - JSON.parse(text, symbolize_names: true)
63
+ + MultiJSON.parse(text, symbolize_names: true)
64
+
65
+ - JSON.generate(object, pretty: true)
66
+ + MultiJSON.generate(object, pretty: true)
67
+ ```
68
+
69
+ Method names and the common options line up with stdlib so existing
70
+ pretty-print calls and option keys keep working without changes:
71
+
72
+ | stdlib `JSON` | `MultiJSON` | Status |
73
+ | ---------------------- | -------------------------- | :---: |
74
+ | `JSON.parse(str)` | `MultiJSON.parse(str)` | ✓ |
75
+ | `JSON.generate(obj)` | `MultiJSON.generate(obj)` | ✓ |
76
+ | `pretty: true` | `pretty: true` | ✓ |
77
+ | `symbolize_names: true` | `symbolize_names: true` | ✓ |
78
+
79
+ ### Deprecated in 1.21.0
80
+
81
+ The module constant and primary verbs were renamed to match Ruby
82
+ stdlib `JSON.parse` / `JSON.generate` and the JSON spec (RFC 8259).
83
+ The old names still work in 1.x but now emit a one-time deprecation
84
+ warning; **they will be removed in 2.0.0**.
85
+
86
+ | Deprecated | Use instead |
87
+ | ----------------------------- | ------------------------------- |
88
+ | `MultiJson` (constant) | `MultiJSON` (all-caps) |
89
+ | `MultiJSON.load(str)` | `MultiJSON.parse(str)` |
90
+ | `MultiJSON.dump(obj)` | `MultiJSON.generate(obj)` |
91
+ | `MultiJSON.load_options=` | `MultiJSON.parse_options=` |
92
+ | `MultiJSON.load_options` | `MultiJSON.parse_options` |
93
+ | `MultiJSON.dump_options=` | `MultiJSON.generate_options=` |
94
+ | `MultiJSON.dump_options` | `MultiJSON.generate_options` |
95
+ | `symbolize_keys:` option | `symbolize_names:` option |
96
+
97
+ The `MultiJson` constant (CamelCase) continues to work as a thin
98
+ delegator; every method call, constant lookup, and rescue clause
99
+ routes through `MultiJSON` transparently.
100
+
101
+ > [!TIP]
102
+ > The recommended upgrade path to 2.0 is: pin `~> 1.21` first, run
103
+ > `ruby -W:deprecated` against your app or test suite to surface every
104
+ > deprecation, migrate each call site to the canonical name, then bump to
105
+ > `~> 2.0`. The 2.0 release deletes the deprecated aliases entirely, so the
106
+ > warnings during 1.21.x are your map.
107
+
32
108
  `ParseError` instance has `cause` reader which contains the original exception.
33
- It also has `data` reader with the input that caused the problem.
109
+ It also has `data` reader with the input that caused the problem, and `line`/`column`
110
+ readers populated for adapters whose error messages include a location (Oj and the
111
+ json gem). Adapters that don't include one (Yajl, fast_jsonparser) leave both nil.
112
+
113
+ ### Tuning the options cache
114
+
115
+ MultiJSON memoizes the merged option hash for each `parse`/`generate` call so
116
+ identical option hashes don't trigger repeated work. The cache is bounded —
117
+ defaulting to 1000 entries per direction — and applications that generate many
118
+ distinct option hashes can raise the ceiling at runtime:
119
+
120
+ ```ruby
121
+ MultiJSON::OptionsCache.max_cache_size = 5000
122
+ ```
123
+
124
+ `max_cache_size` must be a positive integer; `0`, negative values, and
125
+ non-integers raise `ArgumentError`.
126
+
127
+ Lowering the limit only takes effect for *new* inserts; existing cache
128
+ entries are left in place until normal eviction trims them below the
129
+ new ceiling. Call `MultiJSON::OptionsCache.reset` if you want to evict
130
+ immediately.
34
131
 
35
132
  The `use` method, which sets the MultiJSON adapter, takes either a symbol or a
36
133
  class (to allow for custom JSON parsers) that responds to both `.load` and `.dump`
37
134
  at the class level.
38
135
 
39
- When MultiJSON fails to load the specified adapter, it'll throw `MultiJson::AdapterError`
136
+ When MultiJSON fails to load the specified adapter, it'll throw `MultiJSON::AdapterError`
40
137
  which inherits from `ArgumentError`.
41
138
 
42
- MultiJSON tries to have intelligent defaulting. That is, if you have any of the
43
- supported engines already loaded, it will utilize them before attempting to
44
- load any. When loading, libraries are ordered by speed. First fast_jsonparser,
45
- then Oj, then Yajl, then the JSON gem. If no other JSON library is available,
46
- MultiJSON falls back to [OkJson][], a simple, vendorable JSON parser.
139
+ ### Writing a custom adapter
140
+
141
+ A custom adapter is any class that responds to two class methods plus
142
+ defines a `ParseError` constant:
143
+
144
+ ```ruby
145
+ class MyAdapter
146
+ ParseError = Class.new(StandardError)
147
+
148
+ def self.load(string, options)
149
+ # parse string into a Ruby object, raising ParseError on failure
150
+ end
151
+
152
+ def self.dump(object, options)
153
+ # serialize object to a JSON string
154
+ end
155
+ end
156
+
157
+ MultiJSON.use(MyAdapter)
158
+ ```
47
159
 
48
- ## Supported JSON Engines
160
+ `ParseError` is required: `MultiJSON.parse` rescues `MyAdapter::ParseError`
161
+ to wrap parse failures in `MultiJSON::ParseError`, and an adapter that
162
+ omits the constant raises `MultiJSON::AdapterError` on the first parse
163
+ attempt instead of producing a confusing `NameError`.
49
164
 
50
- - [fast_jsonparser][fast_jsonparser] Fast JSON parser by Anil Maurya
51
- - [Oj][oj] Optimized JSON by Peter Ohler
52
- - [Yajl][yajl] Yet Another JSON Library by Brian Lopez
53
- - [JSON][json-gem] The default JSON gem with C-extensions (ships with Ruby 1.9+)
54
- - [gson.rb][gson] A Ruby wrapper for google-gson library (JRuby only)
55
- - [JrJackson][jrjackson] JRuby wrapper for Jackson (JRuby only)
56
- - [OkJson][okjson] A simple, vendorable JSON parser
165
+ For more, inherit from `MultiJSON::Adapter` to pick up shared option
166
+ merging, the `defaults :load, ...` / `defaults :dump, ...` DSL, and the
167
+ blank-input short-circuit. The built-in adapters in
168
+ `lib/multi_json/adapters/` are working examples.
169
+
170
+ > [!NOTE]
171
+ > The adapter contract methods on the adapter class itself stay named
172
+ > `.load` / `.dump` in 1.21.x (and the `defaults :load, ...` / `defaults
173
+ > :dump, ...` DSL keys match). The 2.0 release renames them to `.parse` /
174
+ > `.generate` to align with the public API; if you ship a custom adapter,
175
+ > you'll need to rename those methods (and the `defaults` keys) when you
176
+ > upgrade.
177
+
178
+ MultiJSON tries to have intelligent defaulting. If any supported library is
179
+ already loaded, MultiJSON uses it before attempting to load others. When no
180
+ backend is preloaded, MultiJSON walks its preference list and uses the first
181
+ one that loads successfully. The list is split per platform — JRuby's
182
+ available adapter set differs from MRI's, and the bundled benchmark suite
183
+ ranks `json_gem` ahead of `fast_jsonparser`/`oj`/`yajl` on Ruby 3.4+. CI
184
+ re-runs the benchmark and fails if the observed ranking diverges from the
185
+ table below.
186
+
187
+ | rank | MRI / TruffleRuby | JRuby |
188
+ | ---- | ----------------- | --------------- |
189
+ | 1 | The JSON gem | `jrjackson` |
190
+ | 2 | `fast_jsonparser` | The JSON gem |
191
+ | 3 | `oj` | `gson` |
192
+ | 4 | `yajl-ruby` | — |
193
+
194
+ A dash means the adapter isn't usable on that runtime: `fast_jsonparser`,
195
+ `oj`, and `yajl-ruby` are MRI/TruffleRuby C extensions with no JRuby builds;
196
+ `jrjackson` and `gson` are JRuby-only. The JSON gem is a Ruby default gem,
197
+ so it's always available as a last-resort fallback on any supported Ruby.
198
+ If you have a workload where a different backend is faster, set it
199
+ explicitly with `MultiJSON.use(:your_adapter)`.
200
+
201
+ ## Gem Variants
202
+
203
+ MultiJSON ships as two platform-specific gems. Bundler and RubyGems
204
+ automatically select the correct variant for your Ruby implementation:
205
+
206
+ | | `ruby` platform (MRI) | `java` platform (JRuby) |
207
+ | ---------------------------------------------- | :---: | :---: |
208
+ | Runtime dependency | none | [concurrent-ruby][concurrent-ruby] `~> 1.2` |
209
+ | [`fast_jsonparser`][fast_jsonparser] adapter | ✓ | |
210
+ | [`oj`][oj] adapter | ✓ | |
211
+ | [`yajl`][yajl] adapter | ✓ | |
212
+ | [`json_gem`][json-gem] adapter | ✓ | ✓ |
213
+ | [`gson`][gson] adapter | | ✓ |
214
+ | [`jr_jackson`][jrjackson] adapter | | ✓ |
215
+ | `OptionsCache` thread-safe store | `Hash` + `Mutex` | `Concurrent::Map` |
57
216
 
58
217
  ## Supported Ruby Versions
59
218
 
60
- This library aims to support and is [tested against](https://github.com/sferik/multi_json/actions/workflows/ci.yml) the following Ruby
219
+ This library aims to support and is [tested against](https://github.com/sferik/multi_json/actions/workflows/tests.yml) the following Ruby
61
220
  implementations:
62
221
 
63
- - Ruby 3.0
64
- - Ruby 3.1
65
222
  - Ruby 3.2
66
223
  - Ruby 3.3
67
224
  - Ruby 3.4
68
- - [JRuby][jruby] 9.4 (targets Ruby 3.1 compatibility)
225
+ - Ruby 4.0
69
226
  - [JRuby][jruby] 10.0 (targets Ruby 3.4 compatibility)
227
+ - [TruffleRuby][truffleruby] 33.0 (native and JVM)
70
228
 
71
229
  If something doesn't work in one of these implementations, it's a bug.
72
230
 
@@ -98,21 +256,26 @@ spec.add_dependency 'multi_json', '~> 1.0'
98
256
 
99
257
  ## Copyright
100
258
 
101
- Copyright (c) 2010-2025 Michael Bleigh, Josh Kalderimis, Erik Berlin,
102
- and Pavel Pravosud. See [LICENSE][license] for details.
259
+ Copyright (c) 2010-2026 Erik Berlin, Michael Bleigh, Josh Kalderimis, and Pavel
260
+ Pravosud. See [LICENSE][license] for details.
103
261
 
104
- [build]: https://github.com/sferik/multi_json/actions/workflows/tests.yml
262
+ [concurrent-ruby]: https://github.com/ruby-concurrency/concurrent-ruby
263
+ [docs]: https://github.com/sferik/multi_json/actions/workflows/docs.yml
264
+ [fast_jsonparser]: https://github.com/anilmaurya/fast_jsonparser
105
265
  [gem]: https://rubygems.org/gems/multi_json
106
266
  [gson]: https://github.com/avsej/gson.rb
107
267
  [jrjackson]: https://github.com/guyboertje/jrjackson
108
268
  [jruby]: http://www.jruby.org/
109
269
  [json-gem]: https://github.com/flori/json
110
270
  [license]: LICENSE.md
271
+ [linter]: https://github.com/sferik/multi_json/actions/workflows/linter.yml
111
272
  [macruby]: http://www.macruby.org/
273
+ [mutant]: https://github.com/sferik/multi_json/actions/workflows/mutant.yml
112
274
  [oj]: https://github.com/ohler55/oj
113
- [okjson]: https://github.com/kr/okjson
114
- [fast_jsonparser]: https://github.com/anilmaurya/fast_jsonparser
115
275
  [pvc]: http://docs.rubygems.org/read/chapter/16#page74
116
276
  [qlty]: https://qlty.sh/gh/sferik/projects/multi_json
117
277
  [semver]: http://semver.org/
278
+ [tests]: https://github.com/sferik/multi_json/actions/workflows/tests.yml
279
+ [truffleruby]: https://www.graalvm.org/ruby/
280
+ [typecheck]: https://github.com/sferik/multi_json/actions/workflows/typecheck.yml
118
281
  [yajl]: https://github.com/brianmario/yajl-ruby
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "singleton"
2
4
  require_relative "options"
3
5
 
4
- module MultiJson
6
+ module MultiJSON
5
7
  # Base class for JSON adapter implementations
6
8
  #
7
9
  # Each adapter wraps a specific JSON library (Oj, JSON gem, etc.) and
@@ -19,27 +21,69 @@ module MultiJson
19
21
 
20
22
  class << self
21
23
  BLANK_PATTERN = /\A\s*\z/
22
- private_constant :BLANK_PATTERN
24
+ VALID_DEFAULTS_ACTIONS = %i[load dump].freeze
25
+ private_constant :BLANK_PATTERN, :VALID_DEFAULTS_ACTIONS
26
+
27
+ # Get default parse options, walking the superclass chain
28
+ #
29
+ # Returns the closest ancestor's `@default_load_options` ivar so a
30
+ # parent class calling {.defaults} after a subclass has been
31
+ # defined still propagates to the subclass. Falls back to the
32
+ # shared frozen empty hash when no ancestor has defaults set.
33
+ #
34
+ # @api private
35
+ # @return [Hash] frozen options hash
36
+ def default_parse_options
37
+ walk_default_options(:@default_load_options)
38
+ end
39
+
40
+ # Get default generate options, walking the superclass chain
41
+ #
42
+ # @api private
43
+ # @return [Hash] frozen options hash
44
+ def default_generate_options
45
+ walk_default_options(:@default_dump_options)
46
+ end
47
+
48
+ # Get default parse options, walking the superclass chain
49
+ #
50
+ # @api private
51
+ # @deprecated Use {.default_parse_options} instead. Will be removed in v2.0.
52
+ # @return [Hash] frozen options hash
53
+ def default_load_options
54
+ default_parse_options
55
+ end
23
56
 
24
- # Hook called when a subclass is created
57
+ # Get default generate options, walking the superclass chain
25
58
  #
26
59
  # @api private
27
- # @param subclass [Class] the new subclass
28
- # @return [void]
29
- def inherited(subclass)
30
- super
31
- # Propagate default options to subclasses
32
- subclass.instance_variable_set(:@default_load_options, @default_load_options) if defined?(@default_load_options)
33
- subclass.instance_variable_set(:@default_dump_options, @default_dump_options) if defined?(@default_dump_options)
60
+ # @deprecated Use {.default_generate_options} instead. Will be removed in v2.0.
61
+ # @return [Hash] frozen options hash
62
+ def default_dump_options
63
+ default_generate_options
34
64
  end
35
65
 
36
66
  # DSL for setting adapter-specific default options
37
67
  #
68
+ # ``action`` must be ``:load`` or ``:dump``; ``value`` must be a
69
+ # Hash. Both arguments are validated up front so a typo at the
70
+ # adapter's class definition fails fast instead of producing a
71
+ # silent no-op default that crashes later in the merge path.
72
+ #
38
73
  # @api private
39
74
  # @param action [Symbol] :load or :dump
40
75
  # @param value [Hash] default options for the action
41
76
  # @return [Hash] the frozen options hash
77
+ # @raise [ArgumentError] when action is anything other than :load
78
+ # or :dump, or when value isn't a Hash
79
+ # @example Set load defaults for an adapter
80
+ # class MyAdapter < MultiJSON::Adapter
81
+ # defaults :load, symbolize_keys: false
82
+ # end
42
83
  def defaults(action, value)
84
+ raise ArgumentError, "expected action to be :load or :dump, got #{action.inspect}" unless VALID_DEFAULTS_ACTIONS.include?(action)
85
+ raise ArgumentError, "expected value to be a Hash, got #{value.class}" unless value.is_a?(Hash)
86
+
43
87
  instance_variable_set(:"@default_#{action}_options", value.freeze)
44
88
  end
45
89
 
@@ -68,19 +112,52 @@ module MultiJson
68
112
 
69
113
  private
70
114
 
71
- # Checks if the input is blank (nil or whitespace-only)
115
+ # Walk the superclass chain looking for a default options ivar
116
+ #
117
+ # Stops at the first ancestor whose ``ivar`` is set and returns
118
+ # that value. Returns {Options::EMPTY_OPTIONS} when no ancestor
119
+ # has the ivar set, so adapters without defaults always observe a
120
+ # frozen empty hash instead of nil.
121
+ #
122
+ # @api private
123
+ # @param ivar [Symbol] ivar name (`:@default_load_options` or `:@default_dump_options`)
124
+ # @return [Hash] frozen options hash
125
+ def walk_default_options(ivar)
126
+ # @type var klass: Class?
127
+ klass = self
128
+ while klass
129
+ return klass.instance_variable_get(ivar) if klass.instance_variable_defined?(ivar)
130
+
131
+ klass = klass.superclass
132
+ end
133
+ Options::EMPTY_OPTIONS
134
+ end
135
+
136
+ # Checks if the input is blank (nil, empty, or whitespace-only)
137
+ #
138
+ # The dominant call path arrives with a non-blank string starting
139
+ # with ``{`` or ``[`` (the JSON object/array sigils), so a
140
+ # ``start_with?`` short-circuit skips the regex entirely on the
141
+ # hot path. Falls through to the full check for everything else
142
+ # — strings, numbers, booleans, ``null``, whitespace-prefixed
143
+ # input — at which point ``String#scrub`` is only invoked when
144
+ # the input has invalid encoding so the common valid-UTF-8 path
145
+ # doesn't allocate a scrubbed copy on every call. Scrubbing
146
+ # replaces invalid bytes with U+FFFD before the regex runs so a
147
+ # string with bad bytes is still treated as non-blank without a
148
+ # broad rescue.
72
149
  #
73
150
  # @api private
74
151
  # @param input [String, nil] input to check
75
152
  # @return [Boolean] true if input is blank
76
153
  def blank?(input)
77
- input.nil? || BLANK_PATTERN.match?(input)
78
- rescue ArgumentError
79
- # Invalid byte sequence in UTF-8 - treat as non-blank
80
- false
154
+ return true if input.nil? || input.empty?
155
+ return false if input.start_with?("{", "[")
156
+
157
+ BLANK_PATTERN.match?(input.valid_encoding? ? input : input.scrub)
81
158
  end
82
159
 
83
- # Merges dump options from adapter, global, and call-site
160
+ # Merges generate options from adapter, global, and call-site
84
161
  #
85
162
  # @api private
86
163
  # @param options [Hash] call-site options
@@ -88,11 +165,18 @@ module MultiJson
88
165
  def merged_dump_options(options)
89
166
  cache_key = strip_adapter_key(options)
90
167
  OptionsCache.dump.fetch(cache_key) do
91
- dump_options(cache_key).merge(MultiJson.dump_options(cache_key)).merge!(cache_key)
168
+ generate_options(cache_key).merge(MultiJSON.generate_options(cache_key)).merge!(cache_key)
92
169
  end
93
170
  end
94
171
 
95
- # Merges load options from adapter, global, and call-site
172
+ # Merges parse options from adapter, global, and call-site
173
+ #
174
+ # Each layer is normalized first so a deprecated ``:symbolize_keys``
175
+ # key in any source becomes the canonical ``:symbolize_names`` —
176
+ # done per-layer rather than post-merge so the expected override
177
+ # semantics (call-site > global > adapter default) still apply
178
+ # when a caller mixes the deprecated and canonical names across
179
+ # layers.
96
180
  #
97
181
  # @api private
98
182
  # @param options [Hash] call-site options
@@ -100,16 +184,49 @@ module MultiJson
100
184
  def merged_load_options(options)
101
185
  cache_key = strip_adapter_key(options)
102
186
  OptionsCache.load.fetch(cache_key) do
103
- load_options(cache_key).merge(MultiJson.load_options(cache_key)).merge!(cache_key)
187
+ adapter = normalize_symbolize_option(parse_options(cache_key))
188
+ global = normalize_symbolize_option(MultiJSON.parse_options(cache_key))
189
+ call_site = normalize_symbolize_option(cache_key)
190
+ adapter.merge(global).merge!(call_site)
104
191
  end
105
192
  end
106
193
 
194
+ # Translate the deprecated ``:symbolize_keys`` option to ``:symbolize_names``
195
+ #
196
+ # Matches Ruby stdlib's ``JSON.parse`` naming. Emits a one-time
197
+ # deprecation warning on first encounter of ``:symbolize_keys``.
198
+ # When both names appear in the same layer (unusual — only
199
+ # possible if the caller explicitly set both), the canonical
200
+ # ``:symbolize_names`` value wins and ``:symbolize_keys`` is
201
+ # silently dropped.
202
+ #
203
+ # @api private
204
+ # @param options [Hash] options layer to normalize
205
+ # @return [Hash] hash with ``:symbolize_keys`` translated, or the
206
+ # original hash when no translation is needed
207
+ def normalize_symbolize_option(options)
208
+ return options unless options.key?(:symbolize_keys)
209
+
210
+ MultiJSON.warn_deprecation_once(:symbolize_keys_option,
211
+ "The :symbolize_keys option is deprecated and will be removed in v2.0. Use :symbolize_names instead.")
212
+
213
+ new_opts = options.except(:symbolize_keys)
214
+ new_opts[:symbolize_names] = options[:symbolize_keys] unless new_opts.key?(:symbolize_names)
215
+ new_opts
216
+ end
217
+
107
218
  # Removes the :adapter key from options for cache key
108
219
  #
220
+ # Returns a shared frozen empty hash for the common no-options call
221
+ # path so the hot path avoids allocating a fresh hash on every call.
222
+ #
109
223
  # @api private
110
- # @param options [Hash] original options
224
+ # @param options [Hash, #to_h] original options (may be JSON::State or similar)
111
225
  # @return [Hash] frozen options without :adapter key
112
226
  def strip_adapter_key(options)
227
+ options = options.to_h unless options.is_a?(Hash)
228
+ return Options::EMPTY_OPTIONS if options.empty? || (options.size == 1 && options.key?(:adapter))
229
+
113
230
  options.except(:adapter).freeze
114
231
  end
115
232
  end
@@ -1,4 +1,6 @@
1
- module MultiJson
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
2
4
  # Raised when an adapter cannot be loaded or is not recognized
3
5
  #
4
6
  # @api public
@@ -18,6 +20,12 @@ module MultiJson
18
20
 
19
21
  # Build an AdapterError from an original exception
20
22
  #
23
+ # The original exception's class name is included in the message
24
+ # so a downstream consumer reading just the AdapterError can tell
25
+ # whether the underlying failure was a `LoadError`, an
26
+ # `ArgumentError` from the spec validator, or some other class
27
+ # without having to look at `error.cause` separately.
28
+ #
21
29
  # @api public
22
30
  # @param original_exception [Exception] the original load error
23
31
  # @return [AdapterError] new error with formatted message
@@ -25,7 +33,8 @@ module MultiJson
25
33
  # AdapterError.build(LoadError.new("cannot load such file"))
26
34
  def self.build(original_exception)
27
35
  new(
28
- "Did not recognize your adapter specification (#{original_exception.message}).",
36
+ "Did not recognize your adapter specification " \
37
+ "(#{original_exception.class}: #{original_exception.message}).",
29
38
  cause: original_exception
30
39
  )
31
40
  end