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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +272 -235
- data/CONTRIBUTING.md +23 -16
- data/LICENSE.md +1 -1
- data/README.md +128 -45
- data/lib/multi_json/adapter.rb +152 -19
- data/lib/multi_json/adapter_error.rb +35 -8
- data/lib/multi_json/adapter_selector.rb +194 -0
- data/lib/multi_json/adapters/fast_jsonparser.rb +72 -0
- data/lib/multi_json/adapters/json_gem.rb +81 -3
- data/lib/multi_json/adapters/oj.rb +71 -45
- data/lib/multi_json/adapters/oj_common.rb +44 -0
- data/lib/multi_json/adapters/yajl.rb +24 -3
- data/lib/multi_json/concurrency.rb +57 -0
- data/lib/multi_json/deprecated.rb +110 -0
- data/lib/multi_json/options.rb +83 -10
- data/lib/multi_json/options_cache/mutex_store.rb +65 -0
- data/lib/multi_json/options_cache.rb +76 -19
- data/lib/multi_json/parse_error.rb +95 -9
- data/lib/multi_json/version.rb +17 -4
- data/lib/multi_json.rb +226 -119
- metadata +22 -58
- 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
data/CONTRIBUTING.md
CHANGED
|
@@ -10,36 +10,43 @@ Here are some ways *you* can contribute:
|
|
|
10
10
|
* by reporting bugs
|
|
11
11
|
* by suggesting new features
|
|
12
12
|
* by writing or editing documentation
|
|
13
|
-
* by writing
|
|
13
|
+
* by writing tests
|
|
14
14
|
* by writing code (**no patch is too small**: fix typos, add comments, clean up
|
|
15
15
|
inconsistent whitespace)
|
|
16
16
|
* by refactoring code
|
|
17
17
|
* by closing [issues][]
|
|
18
18
|
* by reviewing patches
|
|
19
19
|
|
|
20
|
-
[issues]: https://github.com/
|
|
20
|
+
[issues]: https://github.com/sferik/multi_json/issues
|
|
21
21
|
|
|
22
22
|
## Submitting an Issue
|
|
23
23
|
We use the [GitHub issue tracker][issues] to track bugs and features. Before
|
|
24
24
|
submitting a bug report or feature request, check to make sure it hasn't
|
|
25
|
-
already been submitted. When submitting a bug report, please include a
|
|
26
|
-
that includes a stack trace and any details that may be necessary
|
|
27
|
-
the bug, including your gem version, Ruby version, and operating
|
|
28
|
-
Ideally, a bug report should include a pull request with failing
|
|
25
|
+
already been submitted. When submitting a bug report, please include a
|
|
26
|
+
[Gist][gist] that includes a stack trace and any details that may be necessary
|
|
27
|
+
to reproduce the bug, including your gem version, Ruby version, and operating
|
|
28
|
+
system. Ideally, a bug report should include a pull request with failing tests.
|
|
29
29
|
|
|
30
30
|
[gist]: https://gist.github.com/
|
|
31
31
|
|
|
32
32
|
## Submitting a Pull Request
|
|
33
|
-
1.
|
|
34
|
-
2.
|
|
35
|
-
3.
|
|
36
|
-
4.
|
|
37
|
-
5.
|
|
38
|
-
6.
|
|
39
|
-
7.
|
|
40
|
-
|
|
41
|
-
8.
|
|
42
|
-
9.
|
|
33
|
+
1. [Fork the repository.][fork]
|
|
34
|
+
2. [Create a topic branch.][branch]
|
|
35
|
+
3. Add tests for your unimplemented feature or bug fix.
|
|
36
|
+
4. Run `bundle exec rake test`. If your tests pass, return to step 3.
|
|
37
|
+
5. Implement your feature or bug fix.
|
|
38
|
+
6. Run `bundle exec rake test`. If your tests fail, return to step 5.
|
|
39
|
+
7. Run `open coverage/index.html`. If your changes are not completely covered
|
|
40
|
+
by your tests, return to step 3.
|
|
41
|
+
8. Run `bundle exec rake mutant` and kill any surviving mutants.
|
|
42
|
+
9. Run `bundle exec rake lint` and fix any offenses.
|
|
43
|
+
10. Run `bundle exec rake yardstick` and add any missing documentation.
|
|
44
|
+
11. Run `bundle exec rake steep` and ensure that the code typechecks.
|
|
45
|
+
12. Add, commit, and push your changes.
|
|
46
|
+
13. [Submit a pull request][pr] that summarizes *what* changes you made and
|
|
47
|
+
*why* you made them. If you made any significant decisions along the way,
|
|
48
|
+
describe the options you considered and how you thought about the
|
|
49
|
+
tradeoffs.
|
|
43
50
|
|
|
44
51
|
[fork]: http://help.github.com/fork-a-repo/
|
|
45
52
|
[branch]: http://learn.github.com/p/branching.html
|
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,27 +14,54 @@ 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
|
-
MultiJson.load('{"abc":"def"}')
|
|
16
|
-
MultiJson.load('{"abc":"def"}', :
|
|
17
|
-
MultiJson.dump({:
|
|
18
|
-
MultiJson.dump({:
|
|
19
|
+
MultiJson.load('{"abc":"def"}') #=> {"abc" => "def"}
|
|
20
|
+
MultiJson.load('{"abc":"def"}', symbolize_keys: true) #=> {abc: "def"}
|
|
21
|
+
MultiJson.dump({abc: "def"}) # convert Ruby back to JSON
|
|
22
|
+
MultiJson.dump({abc: "def"}, pretty: true) # encoded in a pretty form (if supported by the coder)
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
`MultiJson.load` returns `nil` for `nil`, empty, and whitespace-only inputs
|
|
26
|
+
instead of raising, so a missing or blank payload is observable as a `nil`
|
|
27
|
+
return value rather than an exception. When loading invalid JSON, MultiJSON
|
|
28
|
+
will throw a `MultiJson::ParseError`. `MultiJson::DecodeError` and
|
|
29
|
+
`MultiJson::LoadError` are aliases for backwards compatibility.
|
|
22
30
|
|
|
23
31
|
```ruby
|
|
24
32
|
begin
|
|
25
|
-
MultiJson.load(
|
|
33
|
+
MultiJson.load("{invalid json}")
|
|
26
34
|
rescue MultiJson::ParseError => exception
|
|
27
|
-
exception.data
|
|
28
|
-
exception.cause
|
|
35
|
+
exception.data #=> "{invalid json}"
|
|
36
|
+
exception.cause #=> JSON::ParserError: ...
|
|
37
|
+
exception.line #=> 1 (for adapters that report a location, e.g. Oj or the json gem)
|
|
38
|
+
exception.column #=> 2
|
|
29
39
|
end
|
|
30
40
|
```
|
|
31
41
|
|
|
32
42
|
`ParseError` instance has `cause` reader which contains the original exception.
|
|
33
|
-
It also has `data` reader with the input that caused the problem
|
|
43
|
+
It also has `data` reader with the input that caused the problem, and `line`/`column`
|
|
44
|
+
readers populated for adapters whose error messages include a location (Oj and the
|
|
45
|
+
json gem). Adapters that don't include one (Yajl, fast_jsonparser) leave both nil.
|
|
46
|
+
|
|
47
|
+
### Tuning the options cache
|
|
48
|
+
|
|
49
|
+
MultiJSON memoizes the merged option hash for each `load`/`dump` call so identical
|
|
50
|
+
option hashes don't trigger repeated work. The cache is bounded — defaulting to 1000
|
|
51
|
+
entries per direction — and applications that generate many distinct option hashes
|
|
52
|
+
can raise the ceiling at runtime:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
MultiJson::OptionsCache.max_cache_size = 5000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`max_cache_size` must be a positive integer; `0`, negative values, and
|
|
59
|
+
non-integers raise `ArgumentError`.
|
|
60
|
+
|
|
61
|
+
Lowering the limit only takes effect for *new* inserts; existing cache
|
|
62
|
+
entries are left in place until normal eviction trims them below the
|
|
63
|
+
new ceiling. Call `MultiJson::OptionsCache.reset` if you want to evict
|
|
64
|
+
immediately.
|
|
34
65
|
|
|
35
66
|
The `use` method, which sets the MultiJSON adapter, takes either a symbol or a
|
|
36
67
|
class (to allow for custom JSON parsers) that responds to both `.load` and `.dump`
|
|
@@ -39,37 +70,84 @@ at the class level.
|
|
|
39
70
|
When MultiJSON fails to load the specified adapter, it'll throw `MultiJson::AdapterError`
|
|
40
71
|
which inherits from `ArgumentError`.
|
|
41
72
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
MultiJSON falls back to [OkJson][], a simple, vendorable JSON parser.
|
|
73
|
+
### Writing a custom adapter
|
|
74
|
+
|
|
75
|
+
A custom adapter is any class that responds to two class methods plus
|
|
76
|
+
defines a `ParseError` constant:
|
|
47
77
|
|
|
48
|
-
|
|
78
|
+
```ruby
|
|
79
|
+
class MyAdapter
|
|
80
|
+
ParseError = Class.new(StandardError)
|
|
81
|
+
|
|
82
|
+
def self.load(string, options)
|
|
83
|
+
# parse string into a Ruby object, raising ParseError on failure
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.dump(object, options)
|
|
87
|
+
# serialize object to a JSON string
|
|
88
|
+
end
|
|
89
|
+
end
|
|
49
90
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
MultiJson.use(MyAdapter)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`ParseError` is required: `MultiJson.load` rescues `MyAdapter::ParseError`
|
|
95
|
+
to wrap parse failures in `MultiJson::ParseError`, and an adapter that
|
|
96
|
+
omits the constant raises `MultiJson::AdapterError` on the first parse
|
|
97
|
+
attempt instead of producing a confusing `NameError`.
|
|
98
|
+
|
|
99
|
+
For more, inherit from `MultiJson::Adapter` to pick up shared option
|
|
100
|
+
merging, the `defaults :load, ...` / `defaults :dump, ...` DSL, and the
|
|
101
|
+
blank-input short-circuit. The built-in adapters in
|
|
102
|
+
`lib/multi_json/adapters/` are working examples.
|
|
103
|
+
|
|
104
|
+
MultiJSON tries to have intelligent defaulting. If any supported library is
|
|
105
|
+
already loaded, MultiJSON uses it before attempting to load others. When no
|
|
106
|
+
backend is preloaded, MultiJSON walks its preference list and uses the first
|
|
107
|
+
one that loads successfully:
|
|
108
|
+
|
|
109
|
+
1. `fast_jsonparser`
|
|
110
|
+
2. `oj`
|
|
111
|
+
3. `yajl-ruby`
|
|
112
|
+
4. `jrjackson`
|
|
113
|
+
5. The JSON gem
|
|
114
|
+
6. `gson`
|
|
115
|
+
|
|
116
|
+
This order is a best-effort historical ranking by typical parse/dump
|
|
117
|
+
throughput on representative workloads, not a guaranteed benchmark. Real-world
|
|
118
|
+
performance depends on the document shape, the Ruby implementation, and
|
|
119
|
+
whether you're calling `load` or `dump`. The JSON gem is a Ruby default gem,
|
|
120
|
+
so it's always available as a last-resort fallback on any supported Ruby. If
|
|
121
|
+
you have a workload where a different backend is faster, set it explicitly
|
|
122
|
+
with `MultiJson.use(:your_adapter)`.
|
|
123
|
+
|
|
124
|
+
## Gem Variants
|
|
125
|
+
|
|
126
|
+
MultiJSON ships as two platform-specific gems. Bundler and RubyGems
|
|
127
|
+
automatically select the correct variant for your Ruby implementation:
|
|
128
|
+
|
|
129
|
+
| | `ruby` platform (MRI) | `java` platform (JRuby) |
|
|
130
|
+
| ---------------------------------------------- | :---: | :---: |
|
|
131
|
+
| Runtime dependency | none | [concurrent-ruby][concurrent-ruby] `~> 1.2` |
|
|
132
|
+
| [`fast_jsonparser`][fast_jsonparser] adapter | ✓ | |
|
|
133
|
+
| [`oj`][oj] adapter | ✓ | |
|
|
134
|
+
| [`yajl`][yajl] adapter | ✓ | |
|
|
135
|
+
| [`json_gem`][json-gem] adapter | ✓ | ✓ |
|
|
136
|
+
| [`gson`][gson] adapter | | ✓ |
|
|
137
|
+
| [`jr_jackson`][jrjackson] adapter | | ✓ |
|
|
138
|
+
| `OptionsCache` thread-safe store | `Hash` + `Mutex` | `Concurrent::Map` |
|
|
58
139
|
|
|
59
140
|
## Supported Ruby Versions
|
|
60
|
-
|
|
141
|
+
|
|
142
|
+
This library aims to support and is [tested against](https://github.com/sferik/multi_json/actions/workflows/tests.yml) the following Ruby
|
|
61
143
|
implementations:
|
|
62
144
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* Ruby 2.5
|
|
70
|
-
* Ruby 2.6
|
|
71
|
-
* Ruby 2.7
|
|
72
|
-
* [JRuby][]
|
|
145
|
+
- Ruby 3.2
|
|
146
|
+
- Ruby 3.3
|
|
147
|
+
- Ruby 3.4
|
|
148
|
+
- Ruby 4.0
|
|
149
|
+
- [JRuby][jruby] 10.0 (targets Ruby 3.4 compatibility)
|
|
150
|
+
- [TruffleRuby][truffleruby] 33.0 (native and JVM)
|
|
73
151
|
|
|
74
152
|
If something doesn't work in one of these implementations, it's a bug.
|
|
75
153
|
|
|
@@ -100,22 +178,27 @@ spec.add_dependency 'multi_json', '~> 1.0'
|
|
|
100
178
|
```
|
|
101
179
|
|
|
102
180
|
## Copyright
|
|
103
|
-
Copyright (c) 2010-2018 Michael Bleigh, Josh Kalderimis, Erik Michaels-Ober,
|
|
104
|
-
and Pavel Pravosud. See [LICENSE][] for details.
|
|
105
181
|
|
|
106
|
-
|
|
182
|
+
Copyright (c) 2010-2026 Erik Berlin, Michael Bleigh, Josh Kalderimis, and Pavel
|
|
183
|
+
Pravosud. See [LICENSE][license] for details.
|
|
184
|
+
|
|
185
|
+
[concurrent-ruby]: https://github.com/ruby-concurrency/concurrent-ruby
|
|
186
|
+
[docs]: https://github.com/sferik/multi_json/actions/workflows/docs.yml
|
|
187
|
+
[fast_jsonparser]: https://github.com/anilmaurya/fast_jsonparser
|
|
107
188
|
[gem]: https://rubygems.org/gems/multi_json
|
|
108
189
|
[gson]: https://github.com/avsej/gson.rb
|
|
109
190
|
[jrjackson]: https://github.com/guyboertje/jrjackson
|
|
110
191
|
[jruby]: http://www.jruby.org/
|
|
111
192
|
[json-gem]: https://github.com/flori/json
|
|
112
|
-
[json-pure]: https://github.com/flori/json
|
|
113
193
|
[license]: LICENSE.md
|
|
194
|
+
[linter]: https://github.com/sferik/multi_json/actions/workflows/linter.yml
|
|
114
195
|
[macruby]: http://www.macruby.org/
|
|
115
|
-
[
|
|
196
|
+
[mutant]: https://github.com/sferik/multi_json/actions/workflows/mutant.yml
|
|
116
197
|
[oj]: https://github.com/ohler55/oj
|
|
117
|
-
[okjson]: https://github.com/kr/okjson
|
|
118
198
|
[pvc]: http://docs.rubygems.org/read/chapter/16#page74
|
|
199
|
+
[qlty]: https://qlty.sh/gh/sferik/projects/multi_json
|
|
119
200
|
[semver]: http://semver.org/
|
|
120
|
-
[
|
|
201
|
+
[tests]: https://github.com/sferik/multi_json/actions/workflows/tests.yml
|
|
202
|
+
[truffleruby]: https://www.graalvm.org/ruby/
|
|
203
|
+
[typecheck]: https://github.com/sferik/multi_json/actions/workflows/typecheck.yml
|
|
121
204
|
[yajl]: https://github.com/brianmario/yajl-ruby
|
data/lib/multi_json/adapter.rb
CHANGED
|
@@ -1,49 +1,182 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require_relative "options"
|
|
3
5
|
|
|
4
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 load 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_load_options
|
|
37
|
+
walk_default_options(:@default_load_options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get default dump options, walking the superclass chain
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
# @return [Hash] frozen options hash
|
|
44
|
+
def default_dump_options
|
|
45
|
+
walk_default_options(:@default_dump_options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# DSL for setting adapter-specific default options
|
|
49
|
+
#
|
|
50
|
+
# ``action`` must be ``:load`` or ``:dump``; ``value`` must be a
|
|
51
|
+
# Hash. Both arguments are validated up front so a typo at the
|
|
52
|
+
# adapter's class definition fails fast instead of producing a
|
|
53
|
+
# silent no-op default that crashes later in the merge path.
|
|
54
|
+
#
|
|
55
|
+
# @api private
|
|
56
|
+
# @param action [Symbol] :load or :dump
|
|
57
|
+
# @param value [Hash] default options for the action
|
|
58
|
+
# @return [Hash] the frozen options hash
|
|
59
|
+
# @raise [ArgumentError] when action is anything other than :load
|
|
60
|
+
# or :dump, or when value isn't a Hash
|
|
61
|
+
# @example Set load defaults for an adapter
|
|
62
|
+
# class MyAdapter < MultiJson::Adapter
|
|
63
|
+
# defaults :load, symbolize_keys: false
|
|
64
|
+
# end
|
|
10
65
|
def defaults(action, value)
|
|
11
|
-
|
|
66
|
+
raise ArgumentError, "expected action to be :load or :dump, got #{action.inspect}" unless VALID_DEFAULTS_ACTIONS.include?(action)
|
|
67
|
+
raise ArgumentError, "expected value to be a Hash, got #{value.class}" unless value.is_a?(Hash)
|
|
12
68
|
|
|
13
|
-
|
|
14
|
-
define_method("default_#{action}_options") { value }
|
|
15
|
-
end
|
|
69
|
+
instance_variable_set(:"@default_#{action}_options", value.freeze)
|
|
16
70
|
end
|
|
17
71
|
|
|
72
|
+
# Parse a JSON string into a Ruby object
|
|
73
|
+
#
|
|
74
|
+
# @api private
|
|
75
|
+
# @param string [String, #read] JSON string or IO-like object
|
|
76
|
+
# @param options [Hash] parsing options
|
|
77
|
+
# @return [Object, nil] parsed object or nil for blank input
|
|
18
78
|
def load(string, options = {})
|
|
19
79
|
string = string.read if string.respond_to?(:read)
|
|
20
|
-
|
|
21
|
-
|
|
80
|
+
return nil if blank?(string)
|
|
81
|
+
|
|
82
|
+
instance.load(string, merged_load_options(options))
|
|
22
83
|
end
|
|
23
84
|
|
|
85
|
+
# Serialize a Ruby object to JSON
|
|
86
|
+
#
|
|
87
|
+
# @api private
|
|
88
|
+
# @param object [Object] object to serialize
|
|
89
|
+
# @param options [Hash] serialization options
|
|
90
|
+
# @return [String] JSON string
|
|
24
91
|
def dump(object, options = {})
|
|
25
|
-
instance.dump(object,
|
|
92
|
+
instance.dump(object, merged_dump_options(options))
|
|
26
93
|
end
|
|
27
94
|
|
|
28
|
-
|
|
95
|
+
private
|
|
29
96
|
|
|
97
|
+
# Walk the superclass chain looking for a default options ivar
|
|
98
|
+
#
|
|
99
|
+
# Stops at the first ancestor whose ``ivar`` is set and returns
|
|
100
|
+
# that value. Returns {Options::EMPTY_OPTIONS} when no ancestor
|
|
101
|
+
# has the ivar set, so adapters without defaults always observe a
|
|
102
|
+
# frozen empty hash instead of nil.
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
# @param ivar [Symbol] ivar name (`:@default_load_options` or `:@default_dump_options`)
|
|
106
|
+
# @return [Hash] frozen options hash
|
|
107
|
+
def walk_default_options(ivar)
|
|
108
|
+
# @type var klass: Class?
|
|
109
|
+
klass = self
|
|
110
|
+
while klass
|
|
111
|
+
return klass.instance_variable_get(ivar) if klass.instance_variable_defined?(ivar)
|
|
112
|
+
|
|
113
|
+
klass = klass.superclass
|
|
114
|
+
end
|
|
115
|
+
Options::EMPTY_OPTIONS
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Checks if the input is blank (nil, empty, or whitespace-only)
|
|
119
|
+
#
|
|
120
|
+
# The dominant call path arrives with a non-blank string starting
|
|
121
|
+
# with ``{`` or ``[`` (the JSON object/array sigils), so a
|
|
122
|
+
# ``start_with?`` short-circuit skips the regex entirely on the
|
|
123
|
+
# hot path. Falls through to the full check for everything else
|
|
124
|
+
# — strings, numbers, booleans, ``null``, whitespace-prefixed
|
|
125
|
+
# input — at which point ``String#scrub`` is only invoked when
|
|
126
|
+
# the input has invalid encoding so the common valid-UTF-8 path
|
|
127
|
+
# doesn't allocate a scrubbed copy on every call. Scrubbing
|
|
128
|
+
# replaces invalid bytes with U+FFFD before the regex runs so a
|
|
129
|
+
# string with bad bytes is still treated as non-blank without a
|
|
130
|
+
# broad rescue.
|
|
131
|
+
#
|
|
132
|
+
# @api private
|
|
133
|
+
# @param input [String, nil] input to check
|
|
134
|
+
# @return [Boolean] true if input is blank
|
|
30
135
|
def blank?(input)
|
|
31
|
-
input.nil? ||
|
|
32
|
-
|
|
33
|
-
|
|
136
|
+
return true if input.nil? || input.empty?
|
|
137
|
+
return false if input.start_with?("{", "[")
|
|
138
|
+
|
|
139
|
+
BLANK_PATTERN.match?(input.valid_encoding? ? input : input.scrub)
|
|
34
140
|
end
|
|
35
141
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
142
|
+
# Merges dump options from adapter, global, and call-site
|
|
143
|
+
#
|
|
144
|
+
# @api private
|
|
145
|
+
# @param options [Hash] call-site options
|
|
146
|
+
# @return [Hash] merged options hash
|
|
147
|
+
def merged_dump_options(options)
|
|
148
|
+
cache_key = strip_adapter_key(options)
|
|
149
|
+
OptionsCache.dump.fetch(cache_key) do
|
|
150
|
+
dump_options(cache_key).merge(MultiJson.dump_options(cache_key)).merge!(cache_key)
|
|
39
151
|
end
|
|
40
152
|
end
|
|
41
153
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
154
|
+
# Merges load options from adapter, global, and call-site
|
|
155
|
+
#
|
|
156
|
+
# @api private
|
|
157
|
+
# @param options [Hash] call-site options
|
|
158
|
+
# @return [Hash] merged options hash
|
|
159
|
+
def merged_load_options(options)
|
|
160
|
+
cache_key = strip_adapter_key(options)
|
|
161
|
+
OptionsCache.load.fetch(cache_key) do
|
|
162
|
+
load_options(cache_key).merge(MultiJson.load_options(cache_key)).merge!(cache_key)
|
|
45
163
|
end
|
|
46
164
|
end
|
|
165
|
+
|
|
166
|
+
# Removes the :adapter key from options for cache key
|
|
167
|
+
#
|
|
168
|
+
# Returns a shared frozen empty hash for the common no-options call
|
|
169
|
+
# path so the hot path avoids allocating a fresh hash on every call.
|
|
170
|
+
#
|
|
171
|
+
# @api private
|
|
172
|
+
# @param options [Hash, #to_h] original options (may be JSON::State or similar)
|
|
173
|
+
# @return [Hash] frozen options without :adapter key
|
|
174
|
+
def strip_adapter_key(options)
|
|
175
|
+
options = options.to_h unless options.is_a?(Hash)
|
|
176
|
+
return Options::EMPTY_OPTIONS if options.empty? || (options.size == 1 && options.key?(:adapter))
|
|
177
|
+
|
|
178
|
+
options.except(:adapter).freeze
|
|
179
|
+
end
|
|
47
180
|
end
|
|
48
181
|
end
|
|
49
182
|
end
|
|
@@ -1,15 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
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
|