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.
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 specifications
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/intridea/multi_json/issues
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 [Gist][]
26
- that includes a stack trace and any details that may be necessary to reproduce
27
- the bug, including your gem version, Ruby version, and operating system.
28
- Ideally, a bug report should include a pull request with failing specs.
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. [Fork the repository.][fork]
34
- 2. [Create a topic branch.][branch]
35
- 3. Add specs for your unimplemented feature or bug fix.
36
- 4. Run `bundle exec rake spec`. If your specs pass, return to step 3.
37
- 5. Implement your feature or bug fix.
38
- 6. Run `bundle exec rake spec`. If your specs 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. Add, commit, and push your changes.
42
- 9. [Submit a pull request.][pr]
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-2013 Michael Bleigh, Josh Kalderimis, Erik Michaels-Ober, 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](http://travis-ci.org/intridea/multi_json.svg)][travis]
5
- [![Code Climate](https://codeclimate.com/github/intridea/multi_json.svg)][codeclimate]
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]
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,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 '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.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
- When loading invalid JSON, MultiJSON will throw a `MultiJson::ParseError`. `MultiJson::DecodeError` and `MultiJson::LoadError` are aliases for backwards compatibility.
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('{invalid json}')
33
+ MultiJson.load("{invalid json}")
26
34
  rescue MultiJson::ParseError => exception
27
- exception.data # => "{invalid json}"
28
- exception.cause # => JSON::ParserError: 795: unexpected token at '{invalid json}'
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
- 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 Oj, then Yajl,
45
- then the JSON gem, then JSON pure. If no other JSON library is available,
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
- ## Supported JSON Engines
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
- * [Oj][oj] Optimized JSON by Peter Ohler
51
- * [Yajl][yajl] Yet Another JSON Library by Brian Lopez
52
- * [JSON][json-gem] The default JSON gem with C-extensions (ships with Ruby 1.9+)
53
- * [JSON Pure][json-gem] A Ruby variant of the JSON gem
54
- * [NSJSONSerialization][nsjson] Wrapper for Apple's NSJSONSerialization in the Cocoa Framework (MacRuby only)
55
- * [gson.rb][gson] A Ruby wrapper for google-gson library (JRuby only)
56
- * [JrJackson][jrjackson] JRuby wrapper for Jackson (JRuby only)
57
- * [OkJson][okjson] A simple, vendorable JSON parser
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
- This library aims to support and is [tested against][travis] the following Ruby
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
- * Ruby 1.8
64
- * Ruby 1.9
65
- * Ruby 2.0
66
- * Ruby 2.1
67
- * Ruby 2.2
68
- * Ruby 2.4
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
- [codeclimate]: https://codeclimate.com/github/intridea/multi_json
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
- [nsjson]: https://developer.apple.com/library/ios/#documentation/Foundation/Reference/NSJSONSerialization_Class/Reference/Reference.html
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
- [travis]: http://travis-ci.org/intridea/multi_json
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
@@ -1,49 +1,182 @@
1
- require 'singleton'
2
- require 'multi_json/options'
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
- metaclass = class << self; self; end
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
- metaclass.instance_eval do
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
- fail self::ParseError if blank?(string)
21
- instance.load(string, cached_load_options(options))
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, cached_dump_options(options))
92
+ instance.dump(object, merged_dump_options(options))
26
93
  end
27
94
 
28
- private
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? || /\A\s*\z/ === input
32
- rescue ArgumentError # invalid byte sequence in UTF-8
33
- false
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
- def cached_dump_options(options)
37
- OptionsCache.fetch(:dump, options) do
38
- dump_options(options).merge(MultiJson.dump_options(options)).merge!(options)
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
- def cached_load_options(options)
43
- OptionsCache.fetch(:load, options) do
44
- load_options(options).merge(MultiJson.load_options(options)).merge!(options)
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
- attr_reader :cause
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
- message = "Did not recognize your adapter specification (#{original_exception.message})."
7
- new(message).tap do |exception|
8
- exception.instance_eval do
9
- @cause = original_exception
10
- set_backtrace original_exception.backtrace
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