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.
- checksums.yaml +4 -4
- data/LICENSE.md +1 -1
- data/README.md +207 -47
- data/lib/multi_json/adapter.rb +205 -20
- data/lib/multi_json/adapter_error.rb +36 -9
- data/lib/multi_json/adapter_selector.rb +214 -0
- data/lib/multi_json/adapters/fast_jsonparser.rb +74 -0
- data/lib/multi_json/adapters/json_gem.rb +65 -4
- data/lib/multi_json/adapters/oj.rb +72 -46
- data/lib/multi_json/adapters/oj_common.rb +44 -0
- data/lib/multi_json/adapters/yajl.rb +25 -4
- data/lib/multi_json/concurrency.rb +57 -0
- data/lib/multi_json/deprecated.rb +113 -0
- data/lib/multi_json/options.rb +162 -15
- data/lib/multi_json/options_cache/mutex_store.rb +65 -0
- data/lib/multi_json/options_cache.rb +77 -20
- data/lib/multi_json/parse_error.rb +96 -10
- data/lib/multi_json/version.rb +20 -7
- data/lib/multi_json.rb +283 -124
- metadata +22 -60
- data/CHANGELOG.md +0 -275
- data/CONTRIBUTING.md +0 -46
- data/lib/multi_json/adapters/gson.rb +0 -20
- data/lib/multi_json/adapters/jr_jackson.rb +0 -25
- data/lib/multi_json/adapters/json_common.rb +0 -23
- data/lib/multi_json/adapters/json_pure.rb +0 -11
- data/lib/multi_json/adapters/nsjsonserialization.rb +0 -35
- data/lib/multi_json/adapters/ok_json.rb +0 -23
- data/lib/multi_json/convertible_hash_keys.rb +0 -43
- data/lib/multi_json/vendor/okjson.rb +0 -606
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7cffb1170fe976efac86851b47b4db7208fb1eefc745ea73e6cf4aaeb4aaa281
|
|
4
|
+
data.tar.gz: d5b3f218ad72f318eb5c86152c87005f27f7fc3b4b265561f1156092cbbd6c4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a61eb7a6f29720708c0944b8288ed8a9ce1917f76d9934f3f417935a606fa3da0c8e6e1b8158a57420c49b97ba617222ff3a5e1ed28bbfc23c7146f46e32d0ce
|
|
7
|
+
data.tar.gz: fc6f0c205a0c6a05740a08de6c3a27d59e482e64f179591b47a718a84d02b7d509e1ffd368d28c6d5edc88a454bb8ce4c64cd784d7283cc4353c82f7d1f2919d
|
data/LICENSE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c) 2010-
|
|
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
|
-
[][tests]
|
|
4
|
+
[][linter]
|
|
5
|
+
[][mutant]
|
|
6
|
+
[][typecheck]
|
|
7
|
+
[][docs]
|
|
8
|
+
[][qlty]
|
|
9
|
+
[][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,66 +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
|
|
17
|
+
require "multi_json"
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
rescue
|
|
27
|
-
exception.data
|
|
28
|
-
exception.cause
|
|
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 `
|
|
136
|
+
When MultiJSON fails to load the specified adapter, it'll throw `MultiJSON::AdapterError`
|
|
40
137
|
which inherits from `ArgumentError`.
|
|
41
138
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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` |
|
|
58
216
|
|
|
59
217
|
## Supported Ruby Versions
|
|
60
|
-
|
|
218
|
+
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* Ruby 2.5
|
|
70
|
-
* Ruby 2.6
|
|
71
|
-
* Ruby 2.7
|
|
72
|
-
* [JRuby][]
|
|
222
|
+
- Ruby 3.2
|
|
223
|
+
- Ruby 3.3
|
|
224
|
+
- Ruby 3.4
|
|
225
|
+
- Ruby 4.0
|
|
226
|
+
- [JRuby][jruby] 10.0 (targets Ruby 3.4 compatibility)
|
|
227
|
+
- [TruffleRuby][truffleruby] 33.0 (native and JVM)
|
|
73
228
|
|
|
74
229
|
If something doesn't work in one of these implementations, it's a bug.
|
|
75
230
|
|
|
@@ -100,22 +255,27 @@ spec.add_dependency 'multi_json', '~> 1.0'
|
|
|
100
255
|
```
|
|
101
256
|
|
|
102
257
|
## Copyright
|
|
103
|
-
Copyright (c) 2010-2018 Michael Bleigh, Josh Kalderimis, Erik Michaels-Ober,
|
|
104
|
-
and Pavel Pravosud. See [LICENSE][] for details.
|
|
105
258
|
|
|
106
|
-
|
|
259
|
+
Copyright (c) 2010-2026 Erik Berlin, Michael Bleigh, Josh Kalderimis, and Pavel
|
|
260
|
+
Pravosud. See [LICENSE][license] for details.
|
|
261
|
+
|
|
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
|
|
107
265
|
[gem]: https://rubygems.org/gems/multi_json
|
|
108
266
|
[gson]: https://github.com/avsej/gson.rb
|
|
109
267
|
[jrjackson]: https://github.com/guyboertje/jrjackson
|
|
110
268
|
[jruby]: http://www.jruby.org/
|
|
111
269
|
[json-gem]: https://github.com/flori/json
|
|
112
|
-
[json-pure]: https://github.com/flori/json
|
|
113
270
|
[license]: LICENSE.md
|
|
271
|
+
[linter]: https://github.com/sferik/multi_json/actions/workflows/linter.yml
|
|
114
272
|
[macruby]: http://www.macruby.org/
|
|
115
|
-
[
|
|
273
|
+
[mutant]: https://github.com/sferik/multi_json/actions/workflows/mutant.yml
|
|
116
274
|
[oj]: https://github.com/ohler55/oj
|
|
117
|
-
[okjson]: https://github.com/kr/okjson
|
|
118
275
|
[pvc]: http://docs.rubygems.org/read/chapter/16#page74
|
|
276
|
+
[qlty]: https://qlty.sh/gh/sferik/projects/multi_json
|
|
119
277
|
[semver]: http://semver.org/
|
|
120
|
-
[
|
|
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
|
|
121
281
|
[yajl]: https://github.com/brianmario/yajl-ruby
|
data/lib/multi_json/adapter.rb
CHANGED
|
@@ -1,49 +1,234 @@
|
|
|
1
|
-
|
|
2
|
-
require 'multi_json/options'
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
require "singleton"
|
|
4
|
+
require_relative "options"
|
|
5
|
+
|
|
6
|
+
module MultiJSON
|
|
7
|
+
# Base class for JSON adapter implementations
|
|
8
|
+
#
|
|
9
|
+
# Each adapter wraps a specific JSON library (Oj, JSON gem, etc.) and
|
|
10
|
+
# provides a consistent interface. Uses Singleton pattern so each adapter
|
|
11
|
+
# class has exactly one instance.
|
|
12
|
+
#
|
|
13
|
+
# Subclasses must implement:
|
|
14
|
+
# - #load(string, options) -> parsed object
|
|
15
|
+
# - #dump(object, options) -> JSON string
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
5
18
|
class Adapter
|
|
6
19
|
extend Options
|
|
7
20
|
include Singleton
|
|
8
21
|
|
|
9
22
|
class << self
|
|
23
|
+
BLANK_PATTERN = /\A\s*\z/
|
|
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
|
|
56
|
+
|
|
57
|
+
# Get default generate options, walking the superclass chain
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
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
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# DSL for setting adapter-specific default options
|
|
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
|
+
#
|
|
73
|
+
# @api private
|
|
74
|
+
# @param action [Symbol] :load or :dump
|
|
75
|
+
# @param value [Hash] default options for the action
|
|
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
|
|
10
83
|
def defaults(action, value)
|
|
11
|
-
|
|
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)
|
|
12
86
|
|
|
13
|
-
|
|
14
|
-
define_method("default_#{action}_options") { value }
|
|
15
|
-
end
|
|
87
|
+
instance_variable_set(:"@default_#{action}_options", value.freeze)
|
|
16
88
|
end
|
|
17
89
|
|
|
90
|
+
# Parse a JSON string into a Ruby object
|
|
91
|
+
#
|
|
92
|
+
# @api private
|
|
93
|
+
# @param string [String, #read] JSON string or IO-like object
|
|
94
|
+
# @param options [Hash] parsing options
|
|
95
|
+
# @return [Object, nil] parsed object or nil for blank input
|
|
18
96
|
def load(string, options = {})
|
|
19
97
|
string = string.read if string.respond_to?(:read)
|
|
20
|
-
|
|
21
|
-
|
|
98
|
+
return nil if blank?(string)
|
|
99
|
+
|
|
100
|
+
instance.load(string, merged_load_options(options))
|
|
22
101
|
end
|
|
23
102
|
|
|
103
|
+
# Serialize a Ruby object to JSON
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
# @param object [Object] object to serialize
|
|
107
|
+
# @param options [Hash] serialization options
|
|
108
|
+
# @return [String] JSON string
|
|
24
109
|
def dump(object, options = {})
|
|
25
|
-
instance.dump(object,
|
|
110
|
+
instance.dump(object, merged_dump_options(options))
|
|
26
111
|
end
|
|
27
112
|
|
|
28
|
-
|
|
113
|
+
private
|
|
29
114
|
|
|
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.
|
|
149
|
+
#
|
|
150
|
+
# @api private
|
|
151
|
+
# @param input [String, nil] input to check
|
|
152
|
+
# @return [Boolean] true if input is blank
|
|
30
153
|
def blank?(input)
|
|
31
|
-
input.nil? ||
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
34
158
|
end
|
|
35
159
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
160
|
+
# Merges generate options from adapter, global, and call-site
|
|
161
|
+
#
|
|
162
|
+
# @api private
|
|
163
|
+
# @param options [Hash] call-site options
|
|
164
|
+
# @return [Hash] merged options hash
|
|
165
|
+
def merged_dump_options(options)
|
|
166
|
+
cache_key = strip_adapter_key(options)
|
|
167
|
+
OptionsCache.dump.fetch(cache_key) do
|
|
168
|
+
generate_options(cache_key).merge(MultiJSON.generate_options(cache_key)).merge!(cache_key)
|
|
39
169
|
end
|
|
40
170
|
end
|
|
41
171
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
180
|
+
#
|
|
181
|
+
# @api private
|
|
182
|
+
# @param options [Hash] call-site options
|
|
183
|
+
# @return [Hash] merged options hash
|
|
184
|
+
def merged_load_options(options)
|
|
185
|
+
cache_key = strip_adapter_key(options)
|
|
186
|
+
OptionsCache.load.fetch(cache_key) do
|
|
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)
|
|
45
191
|
end
|
|
46
192
|
end
|
|
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
|
+
|
|
218
|
+
# Removes the :adapter key from options for cache key
|
|
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
|
+
#
|
|
223
|
+
# @api private
|
|
224
|
+
# @param options [Hash, #to_h] original options (may be JSON::State or similar)
|
|
225
|
+
# @return [Hash] frozen options without :adapter key
|
|
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
|
+
|
|
230
|
+
options.except(:adapter).freeze
|
|
231
|
+
end
|
|
47
232
|
end
|
|
48
233
|
end
|
|
49
234
|
end
|
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJSON
|
|
4
|
+
# Raised when an adapter cannot be loaded or is not recognized
|
|
5
|
+
#
|
|
6
|
+
# @api public
|
|
2
7
|
class AdapterError < ArgumentError
|
|
3
|
-
|
|
8
|
+
# Create a new AdapterError
|
|
9
|
+
#
|
|
10
|
+
# @api public
|
|
11
|
+
# @param message [String, nil] error message
|
|
12
|
+
# @param cause [Exception, nil] the original exception
|
|
13
|
+
# @return [AdapterError] new error instance
|
|
14
|
+
# @example
|
|
15
|
+
# AdapterError.new("Unknown adapter", cause: original_error)
|
|
16
|
+
def initialize(message = nil, cause: nil)
|
|
17
|
+
super(message)
|
|
18
|
+
set_backtrace(cause.backtrace) if cause
|
|
19
|
+
end
|
|
4
20
|
|
|
21
|
+
# Build an AdapterError from an original exception
|
|
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
|
+
#
|
|
29
|
+
# @api public
|
|
30
|
+
# @param original_exception [Exception] the original load error
|
|
31
|
+
# @return [AdapterError] new error with formatted message
|
|
32
|
+
# @example
|
|
33
|
+
# AdapterError.build(LoadError.new("cannot load such file"))
|
|
5
34
|
def self.build(original_exception)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
end
|
|
35
|
+
new(
|
|
36
|
+
"Did not recognize your adapter specification " \
|
|
37
|
+
"(#{original_exception.class}: #{original_exception.message}).",
|
|
38
|
+
cause: original_exception
|
|
39
|
+
)
|
|
13
40
|
end
|
|
14
41
|
end
|
|
15
42
|
end
|