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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJSON
|
|
4
|
+
# Handles adapter discovery, loading, and selection
|
|
5
|
+
#
|
|
6
|
+
# Adapters can be specified as:
|
|
7
|
+
# - Symbol/String: adapter name (e.g., :oj, "json_gem")
|
|
8
|
+
# - Module: adapter class directly
|
|
9
|
+
# - nil/false: use default adapter
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module AdapterSelector
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
# Per-adapter metadata, in preference order (fastest first). Each
|
|
16
|
+
# entry maps the adapter symbol to its ``require`` path and the
|
|
17
|
+
# constant whose presence indicates the backing library is already
|
|
18
|
+
# loaded. ``loaded`` is a ``::``-separated path so we can walk it
|
|
19
|
+
# without an explicit ``defined?`` check.
|
|
20
|
+
#
|
|
21
|
+
# The hash order is split per platform: on MRI/TruffleRuby the
|
|
22
|
+
# bundled benchmark suite ranks json_gem ahead of fast_jsonparser/
|
|
23
|
+
# oj/yajl on Ruby 3.4+; on JRuby the FFI-vs-pure-Ruby tradeoff
|
|
24
|
+
# hasn't been re-benchmarked yet, so jr_jackson stays first there.
|
|
25
|
+
# CI re-runs the benchmark with ``--verify-preference`` to fail
|
|
26
|
+
# if the observed ranking diverges.
|
|
27
|
+
# :nocov:
|
|
28
|
+
ADAPTERS = if RUBY_ENGINE == "jruby"
|
|
29
|
+
{
|
|
30
|
+
jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
|
|
31
|
+
json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
|
|
32
|
+
gson: {require: "gson", loaded: "Gson"},
|
|
33
|
+
fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
|
|
34
|
+
oj: {require: "oj", loaded: "Oj"},
|
|
35
|
+
yajl: {require: "yajl", loaded: "Yajl"}
|
|
36
|
+
}.freeze
|
|
37
|
+
else
|
|
38
|
+
{
|
|
39
|
+
json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
|
|
40
|
+
fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
|
|
41
|
+
oj: {require: "oj", loaded: "Oj"},
|
|
42
|
+
yajl: {require: "yajl", loaded: "Yajl"},
|
|
43
|
+
jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
|
|
44
|
+
gson: {require: "gson", loaded: "Gson"}
|
|
45
|
+
}.freeze
|
|
46
|
+
end
|
|
47
|
+
# :nocov:
|
|
48
|
+
private_constant :ADAPTERS
|
|
49
|
+
|
|
50
|
+
# Backwards-compatible view of {ADAPTERS} that exposes only the
|
|
51
|
+
# require paths. Tests still poke at this constant to stub or break
|
|
52
|
+
# the require step.
|
|
53
|
+
REQUIREMENT_MAP = ADAPTERS.transform_values { |meta| meta[:require] }.freeze
|
|
54
|
+
|
|
55
|
+
# Returns the default adapter to use
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
# @return [Symbol] adapter name
|
|
59
|
+
# @example
|
|
60
|
+
# AdapterSelector.default_adapter #=> :oj
|
|
61
|
+
def default_adapter
|
|
62
|
+
Concurrency.synchronize(:default_adapter) { @default_adapter ||= detect_best_adapter }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the default adapter class, excluding the given adapter name
|
|
66
|
+
#
|
|
67
|
+
# Used by adapters that only implement one direction (e.g.
|
|
68
|
+
# FastJsonparser only parses) so the other direction can be delegated
|
|
69
|
+
# to whichever library MultiJSON would otherwise pick.
|
|
70
|
+
#
|
|
71
|
+
# @api private
|
|
72
|
+
# @param excluded [Symbol] adapter name to skip during detection
|
|
73
|
+
# @return [Class] the adapter class
|
|
74
|
+
# @example
|
|
75
|
+
# AdapterSelector.default_adapter_excluding(:fast_jsonparser) #=> MultiJSON::Adapters::Oj
|
|
76
|
+
def default_adapter_excluding(excluded)
|
|
77
|
+
Concurrency.synchronize(:default_adapter) do
|
|
78
|
+
name = loaded_adapter(excluding: excluded)
|
|
79
|
+
name ||= installable_adapter(excluding: excluded)
|
|
80
|
+
name ||= fallback_adapter
|
|
81
|
+
load_adapter_by_name(name.to_s)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Detects the best available JSON adapter
|
|
88
|
+
#
|
|
89
|
+
# @api private
|
|
90
|
+
# @return [Symbol] adapter name
|
|
91
|
+
def detect_best_adapter
|
|
92
|
+
loaded_adapter || installable_adapter || fallback_adapter
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Finds an already-loaded JSON library
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
98
|
+
# @param excluding [Symbol, nil] adapter name to skip during detection
|
|
99
|
+
# @return [Symbol, nil] adapter name if found
|
|
100
|
+
def loaded_adapter(excluding: nil)
|
|
101
|
+
ADAPTERS.each do |name, meta|
|
|
102
|
+
next if name == excluding
|
|
103
|
+
return name if Object.const_defined?(meta.fetch(:loaded))
|
|
104
|
+
end
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Tries to require and use an installable adapter
|
|
109
|
+
#
|
|
110
|
+
# @api private
|
|
111
|
+
# @param excluding [Symbol, nil] adapter name to skip during detection
|
|
112
|
+
# @return [Symbol, nil] adapter name if successfully required
|
|
113
|
+
def installable_adapter(excluding: nil)
|
|
114
|
+
REQUIREMENT_MAP.each_key do |adapter_name|
|
|
115
|
+
next if adapter_name == excluding
|
|
116
|
+
return adapter_name if try_require(adapter_name)
|
|
117
|
+
end
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Attempts to require a JSON library
|
|
122
|
+
#
|
|
123
|
+
# @api private
|
|
124
|
+
# @param adapter_name [Symbol] adapter to require
|
|
125
|
+
# @return [Boolean] true if require succeeded
|
|
126
|
+
def try_require(adapter_name)
|
|
127
|
+
require REQUIREMENT_MAP.fetch(adapter_name)
|
|
128
|
+
true
|
|
129
|
+
rescue ::LoadError
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns the fallback adapter when no others available
|
|
134
|
+
#
|
|
135
|
+
# The json gem is a Ruby default gem since Ruby 1.9, so in practice
|
|
136
|
+
# the installable-adapter step always succeeds before reaching this
|
|
137
|
+
# fallback on any supported Ruby version. The warning below only
|
|
138
|
+
# fires in tests that deliberately break the require path.
|
|
139
|
+
#
|
|
140
|
+
# @api private
|
|
141
|
+
# @return [Symbol] the json_gem adapter name
|
|
142
|
+
def fallback_adapter
|
|
143
|
+
warn_about_fallback unless @default_adapter_warning_shown
|
|
144
|
+
@default_adapter_warning_shown = true
|
|
145
|
+
:json_gem
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Warns the user about reaching the last-resort fallback
|
|
149
|
+
#
|
|
150
|
+
# @api private
|
|
151
|
+
# @return [void]
|
|
152
|
+
def warn_about_fallback
|
|
153
|
+
Kernel.warn(
|
|
154
|
+
"[WARNING] MultiJSON is falling back to the json_gem adapter " \
|
|
155
|
+
"because no other JSON library could be loaded."
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Loads an adapter from a specification
|
|
160
|
+
#
|
|
161
|
+
# @api private
|
|
162
|
+
# @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
|
|
163
|
+
# @return [Class] the adapter class
|
|
164
|
+
def load_adapter(adapter_spec)
|
|
165
|
+
adapter = case adapter_spec
|
|
166
|
+
when ::String then load_adapter_by_name(adapter_spec)
|
|
167
|
+
when ::Symbol then load_adapter_by_name(adapter_spec.to_s)
|
|
168
|
+
when nil, false then load_adapter(default_adapter)
|
|
169
|
+
when ::Module then adapter_spec
|
|
170
|
+
else raise ::LoadError, "expected adapter to be a Symbol, String, or Module, got #{adapter_spec.inspect}"
|
|
171
|
+
end
|
|
172
|
+
validate_adapter!(adapter)
|
|
173
|
+
rescue ::LoadError => e
|
|
174
|
+
raise AdapterError.build(e)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Loads an adapter by its string name
|
|
178
|
+
#
|
|
179
|
+
# ``jrjackson`` (the JrJackson gem's name) is normalized to
|
|
180
|
+
# ``jr_jackson`` (the adapter file/class name) for backwards
|
|
181
|
+
# compatibility with the original gem-name alias.
|
|
182
|
+
#
|
|
183
|
+
# @api private
|
|
184
|
+
# @param name [String] adapter name
|
|
185
|
+
# @return [Class] the adapter class
|
|
186
|
+
def load_adapter_by_name(name)
|
|
187
|
+
normalized = name.downcase
|
|
188
|
+
normalized = "jr_jackson" if normalized == "jrjackson"
|
|
189
|
+
require_relative "adapters/#{normalized}"
|
|
190
|
+
|
|
191
|
+
class_name = normalized.split("_").map(&:capitalize).join
|
|
192
|
+
::MultiJSON::Adapters.const_get(class_name)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Validate that an adapter satisfies the documented contract
|
|
196
|
+
#
|
|
197
|
+
# Custom adapters are accepted as modules/classes, so fail fast
|
|
198
|
+
# during adapter resolution rather than later on the first load or
|
|
199
|
+
# dump call.
|
|
200
|
+
#
|
|
201
|
+
# @api private
|
|
202
|
+
# @param adapter [Module] adapter class or module
|
|
203
|
+
# @return [Module] the validated adapter
|
|
204
|
+
# @raise [AdapterError] when the adapter is missing a required class method
|
|
205
|
+
# or ParseError constant
|
|
206
|
+
def validate_adapter!(adapter)
|
|
207
|
+
raise AdapterError, "Adapter #{adapter} must respond to .load" unless adapter.respond_to?(:load)
|
|
208
|
+
raise AdapterError, "Adapter #{adapter} must respond to .dump" unless adapter.respond_to?(:dump)
|
|
209
|
+
|
|
210
|
+
MultiJSON.parse_error_class_for(adapter)
|
|
211
|
+
adapter
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fast_jsonparser"
|
|
4
|
+
require_relative "../adapter"
|
|
5
|
+
require_relative "../adapter_selector"
|
|
6
|
+
|
|
7
|
+
module MultiJSON
|
|
8
|
+
module Adapters
|
|
9
|
+
# Use the FastJsonparser library to load, and the fastest other
|
|
10
|
+
# available adapter to dump.
|
|
11
|
+
#
|
|
12
|
+
# FastJsonparser only implements parsing, so the ``dump`` side of
|
|
13
|
+
# the adapter is delegated to whichever adapter MultiJSON would
|
|
14
|
+
# pick if FastJsonparser weren't installed (oj → yajl → jr_jackson
|
|
15
|
+
# → json_gem → gson). The delegate is resolved lazily at the first
|
|
16
|
+
# ``dump`` call, not at file load time, so load order doesn't lock
|
|
17
|
+
# in the wrong delegate. Require any preferred dump backend before
|
|
18
|
+
# the first ``dump`` call (typical applications already have ``oj``
|
|
19
|
+
# loaded by then).
|
|
20
|
+
class FastJsonparser < Adapter
|
|
21
|
+
defaults :load, symbolize_names: false
|
|
22
|
+
|
|
23
|
+
# Exception raised when JSON parsing fails
|
|
24
|
+
ParseError = ::FastJsonparser::ParseError
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Serialize a Ruby object to JSON via the lazy delegate
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
# @param object [Object] object to serialize
|
|
31
|
+
# @param options [Hash] serialization options
|
|
32
|
+
# @return [String] JSON string
|
|
33
|
+
# @example
|
|
34
|
+
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
35
|
+
def dump(object, options = {})
|
|
36
|
+
dump_delegate.dump(object, options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Resolve the dump delegate, caching it across calls
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
44
|
+
# @return [Class] delegate adapter class
|
|
45
|
+
def dump_delegate
|
|
46
|
+
MultiJSON::Concurrency.synchronize(:dump_delegate) do
|
|
47
|
+
@dump_delegate ||= MultiJSON::AdapterSelector.default_adapter_excluding(:fast_jsonparser)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parse a JSON string into a Ruby object
|
|
53
|
+
#
|
|
54
|
+
# FastJsonparser.parse only accepts ``symbolize_keys`` and raises
|
|
55
|
+
# on unknown keyword arguments, so the adapter explicitly forwards
|
|
56
|
+
# MultiJSON's canonical ``:symbolize_names`` option as
|
|
57
|
+
# FastJsonparser's native ``symbolize_keys:`` kwarg and silently
|
|
58
|
+
# drops the rest. Pass other options through
|
|
59
|
+
# ``MultiJSON.parse_options=`` and they'll apply to whichever
|
|
60
|
+
# adapter MultiJSON selects when fast_jsonparser isn't installed.
|
|
61
|
+
#
|
|
62
|
+
# @api private
|
|
63
|
+
# @param string [String] JSON string to parse
|
|
64
|
+
# @param options [Hash] parsing options (only :symbolize_names is honored)
|
|
65
|
+
# @return [Object] parsed Ruby object
|
|
66
|
+
#
|
|
67
|
+
# @example Parse JSON string
|
|
68
|
+
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
69
|
+
def load(string, options = {})
|
|
70
|
+
::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_names])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -1,11 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
require 'multi_json/adapters/json_common'
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
require_relative "../adapter"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module MultiJSON
|
|
5
7
|
module Adapters
|
|
6
8
|
# Use the JSON gem to dump/load.
|
|
7
|
-
class JsonGem <
|
|
9
|
+
class JsonGem < Adapter
|
|
10
|
+
# Exception raised when JSON parsing fails
|
|
8
11
|
ParseError = ::JSON::ParserError
|
|
12
|
+
|
|
13
|
+
defaults :load, create_additions: false, quirks_mode: true
|
|
14
|
+
|
|
15
|
+
PRETTY_STATE_PROTOTYPE = {
|
|
16
|
+
indent: " ",
|
|
17
|
+
space: " ",
|
|
18
|
+
object_nl: "\n",
|
|
19
|
+
array_nl: "\n"
|
|
20
|
+
}.freeze
|
|
21
|
+
private_constant :PRETTY_STATE_PROTOTYPE
|
|
22
|
+
|
|
23
|
+
# Parse a JSON string into a Ruby object
|
|
24
|
+
#
|
|
25
|
+
# Non-UTF-8 strings are re-labeled via ``force_encoding`` (not
|
|
26
|
+
# transcoded) and then validated. This handles the dominant
|
|
27
|
+
# real-world case: Ruby HTTP libraries return response bodies
|
|
28
|
+
# tagged as ``ASCII-8BIT`` even when the bytes are valid UTF-8.
|
|
29
|
+
# ``encode(Encoding::UTF_8)`` would raise on any multi-byte
|
|
30
|
+
# sequence in that scenario because it tries to transcode each
|
|
31
|
+
# byte individually from ASCII-8BIT to UTF-8.
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
# @param string [String] JSON string to parse
|
|
35
|
+
# @param options [Hash] parsing options
|
|
36
|
+
# @return [Object] parsed Ruby object
|
|
37
|
+
# @raise [::JSON::ParserError] when the input is not valid UTF-8
|
|
38
|
+
#
|
|
39
|
+
# @example Parse JSON string
|
|
40
|
+
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
41
|
+
def load(string, options = {})
|
|
42
|
+
if string.encoding != Encoding::UTF_8
|
|
43
|
+
string = string.dup.force_encoding(Encoding::UTF_8)
|
|
44
|
+
raise ::JSON::ParserError, "Invalid UTF-8 byte sequence in JSON input" unless string.valid_encoding?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
::JSON.parse(string, options)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Serialize a Ruby object to JSON
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
53
|
+
# @param object [Object] object to serialize
|
|
54
|
+
# @param options [Hash] serialization options
|
|
55
|
+
# @return [String] JSON string
|
|
56
|
+
#
|
|
57
|
+
# @example Serialize object to JSON
|
|
58
|
+
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
59
|
+
def dump(object, options = {})
|
|
60
|
+
json_object = object.respond_to?(:as_json) ? object.as_json : object
|
|
61
|
+
return ::JSON.dump(json_object) if options.empty?
|
|
62
|
+
return ::JSON.generate(json_object, options) unless options.key?(:pretty)
|
|
63
|
+
|
|
64
|
+
# Common case: ``pretty: true`` is the only option, so the merge
|
|
65
|
+
# would just produce a copy of PRETTY_STATE_PROTOTYPE.
|
|
66
|
+
return ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE) if options.size == 1
|
|
67
|
+
|
|
68
|
+
::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE.merge(options.except(:pretty)))
|
|
69
|
+
end
|
|
9
70
|
end
|
|
10
71
|
end
|
|
11
72
|
end
|
|
@@ -1,62 +1,88 @@
|
|
|
1
|
-
|
|
2
|
-
require 'oj'
|
|
3
|
-
require 'multi_json/adapter'
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
require "oj"
|
|
4
|
+
require_relative "../adapter"
|
|
5
|
+
require_relative "oj_common"
|
|
6
|
+
|
|
7
|
+
module MultiJSON
|
|
8
|
+
# Namespace for JSON adapter implementations
|
|
9
|
+
#
|
|
10
|
+
# Each adapter wraps a specific JSON library and provides a consistent
|
|
11
|
+
# interface for loading and dumping JSON data.
|
|
6
12
|
module Adapters
|
|
7
13
|
# Use the Oj library to dump/load.
|
|
8
14
|
class Oj < Adapter
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
15
|
+
include OjCommon
|
|
16
|
+
|
|
17
|
+
defaults :load, mode: :strict, symbolize_names: false
|
|
18
|
+
defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
|
|
19
|
+
|
|
20
|
+
# In certain cases the Oj gem may throw a ``JSON::ParserError``
|
|
21
|
+
# exception instead of its own class. Neither ``::JSON::ParserError``
|
|
22
|
+
# nor ``::Oj::ParseError`` is guaranteed to be defined, so we can't
|
|
23
|
+
# reference them directly — match by walking the exception's
|
|
24
|
+
# ancestry by class name instead. This will not catch subclasses
|
|
25
|
+
# of those classes, which shouldn't be a problem since neither
|
|
26
|
+
# library is known to subclass them.
|
|
19
27
|
class ParseError < ::SyntaxError
|
|
20
|
-
WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].
|
|
28
|
+
WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].freeze
|
|
29
|
+
private_constant :WRAPPED_CLASSES
|
|
21
30
|
|
|
31
|
+
# Case equality for exception matching in rescue clauses
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
# @param exception [Exception] exception to check
|
|
35
|
+
# @return [Boolean] true if exception is a parse error
|
|
36
|
+
#
|
|
37
|
+
# @example Match parse errors in rescue
|
|
38
|
+
# rescue ParseError => e
|
|
22
39
|
def self.===(exception)
|
|
23
|
-
|
|
24
|
-
when ::SyntaxError
|
|
25
|
-
true
|
|
26
|
-
else
|
|
27
|
-
WRAPPED_CLASSES.include?(exception.class.to_s)
|
|
28
|
-
end
|
|
40
|
+
exception.class.ancestors.any? { |ancestor| WRAPPED_CLASSES.include?(ancestor.to_s) }
|
|
29
41
|
end
|
|
30
42
|
end
|
|
31
43
|
|
|
44
|
+
# Parse a JSON string into a Ruby object
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
47
|
+
# @param string [String] JSON string to parse
|
|
48
|
+
# @param options [Hash] parsing options
|
|
49
|
+
# @return [Object] parsed Ruby object
|
|
50
|
+
#
|
|
51
|
+
# @example Parse JSON string
|
|
52
|
+
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
32
53
|
def load(string, options = {})
|
|
33
|
-
|
|
34
|
-
::Oj.load(string, options)
|
|
54
|
+
::Oj.load(string, translate_load_options(options))
|
|
35
55
|
end
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
# Serialize a Ruby object to JSON
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
# @param object [Object] object to serialize
|
|
61
|
+
# @param options [Hash] serialization options
|
|
62
|
+
# @return [String] JSON string
|
|
63
|
+
#
|
|
64
|
+
# @example Serialize object to JSON
|
|
65
|
+
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
66
|
+
def dump(object, options = {})
|
|
67
|
+
::Oj.dump(object, prepare_dump_options(options))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Translate ``:symbolize_names`` into Oj's ``:symbol_keys``
|
|
73
|
+
#
|
|
74
|
+
# Returns a new hash without mutating the input.
|
|
75
|
+
# ``:symbol_keys`` is always set (true or false) so MultiJSON's
|
|
76
|
+
# behavior is independent of any global ``Oj.default_options``
|
|
77
|
+
# the host application may have set. The input is the cached hash
|
|
78
|
+
# returned from {Adapter.merged_load_options}, so in-place edits
|
|
79
|
+
# would pollute the cache.
|
|
80
|
+
#
|
|
81
|
+
# @api private
|
|
82
|
+
# @param options [Hash] merged load options
|
|
83
|
+
# @return [Hash] options with ``:symbolize_names`` translated
|
|
84
|
+
def translate_load_options(options)
|
|
85
|
+
options.except(:symbolize_names).merge(symbol_keys: options[:symbolize_names] == true)
|
|
60
86
|
end
|
|
61
87
|
end
|
|
62
88
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJSON
|
|
4
|
+
module Adapters
|
|
5
|
+
# Shared functionality for the Oj adapter
|
|
6
|
+
#
|
|
7
|
+
# Provides option preparation for Oj.dump. Targets Oj 3.x; Oj 2.x is
|
|
8
|
+
# no longer supported.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module OjCommon
|
|
12
|
+
PRETTY_STATE_PROTOTYPE = {
|
|
13
|
+
indent: " ",
|
|
14
|
+
space: " ",
|
|
15
|
+
space_before: "",
|
|
16
|
+
object_nl: "\n",
|
|
17
|
+
array_nl: "\n",
|
|
18
|
+
ascii_only: false
|
|
19
|
+
}.freeze
|
|
20
|
+
private_constant :PRETTY_STATE_PROTOTYPE
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Prepare options for Oj.dump
|
|
25
|
+
#
|
|
26
|
+
# Returns a fresh hash; never mutates the input. The input is the
|
|
27
|
+
# cached options hash returned from Adapter.merged_dump_options, so
|
|
28
|
+
# in-place mutation would pollute the cache and corrupt subsequent
|
|
29
|
+
# dump calls.
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
# @param options [Hash] serialization options
|
|
33
|
+
# @return [Hash] processed options for Oj.dump
|
|
34
|
+
#
|
|
35
|
+
# @example Prepare dump options
|
|
36
|
+
# prepare_dump_options(pretty: true)
|
|
37
|
+
def prepare_dump_options(options)
|
|
38
|
+
return options unless options.key?(:pretty)
|
|
39
|
+
|
|
40
|
+
options.except(:pretty).merge(PRETTY_STATE_PROTOTYPE)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
require 'multi_json/adapter'
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
require "yajl"
|
|
4
|
+
require_relative "../adapter"
|
|
5
|
+
|
|
6
|
+
module MultiJSON
|
|
5
7
|
module Adapters
|
|
6
8
|
# Use the Yajl-Ruby library to dump/load.
|
|
7
9
|
class Yajl < Adapter
|
|
10
|
+
# Exception raised when JSON parsing fails
|
|
8
11
|
ParseError = ::Yajl::ParseError
|
|
9
12
|
|
|
13
|
+
# Parse a JSON string into a Ruby object
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
# @param string [String] JSON string to parse
|
|
17
|
+
# @param options [Hash] parsing options
|
|
18
|
+
# @return [Object] parsed Ruby object
|
|
19
|
+
#
|
|
20
|
+
# @example Parse JSON string
|
|
21
|
+
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
10
22
|
def load(string, options = {})
|
|
11
|
-
::Yajl::Parser.new(
|
|
23
|
+
::Yajl::Parser.new(options).parse(string)
|
|
12
24
|
end
|
|
13
25
|
|
|
26
|
+
# Serialize a Ruby object to JSON
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
# @param object [Object] object to serialize
|
|
30
|
+
# @param options [Hash] serialization options
|
|
31
|
+
# @return [String] JSON string
|
|
32
|
+
#
|
|
33
|
+
# @example Serialize object to JSON
|
|
34
|
+
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
14
35
|
def dump(object, options = {})
|
|
15
36
|
::Yajl::Encoder.encode(object, options)
|
|
16
37
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJSON
|
|
4
|
+
# Catalog of process-wide mutexes used to serialize MultiJSON's lazy
|
|
5
|
+
# initializers and adapter swaps. Each mutex protects a distinct
|
|
6
|
+
# piece of mutable state. Callers go through {.synchronize} rather
|
|
7
|
+
# than touching the mutex constants directly so the constants
|
|
8
|
+
# themselves can stay {.private_constant} and the surface of the
|
|
9
|
+
# module is documented in one place.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module Concurrency
|
|
13
|
+
# Catalog of mutexes keyed by symbolic name. Each entry maps the
|
|
14
|
+
# public name passed to {.synchronize} to the underlying mutex
|
|
15
|
+
# instance. The names are documented inline so callers can find
|
|
16
|
+
# what each mutex protects without leaving this file.
|
|
17
|
+
MUTEXES = {
|
|
18
|
+
# Guards the {DEPRECATION_WARNINGS_SHOWN} set in `MultiJSON` so the
|
|
19
|
+
# check-then-add pair in `warn_deprecation_once` doesn't race.
|
|
20
|
+
deprecation_warnings: Mutex.new,
|
|
21
|
+
# Guards the process-wide `@adapter` swap in `MultiJSON.use` so two
|
|
22
|
+
# threads can't interleave their `OptionsCache.reset` and adapter
|
|
23
|
+
# assignment.
|
|
24
|
+
adapter: Mutex.new,
|
|
25
|
+
# Guards the lazy `@default_adapter` initializer and the
|
|
26
|
+
# `default_adapter_excluding` detection chain in `AdapterSelector`,
|
|
27
|
+
# so the chain runs at most once and `fallback_adapter`'s one-time
|
|
28
|
+
# warning fires at most once.
|
|
29
|
+
default_adapter: Mutex.new,
|
|
30
|
+
# Guards the lazy `default_load_options` / `default_dump_options`
|
|
31
|
+
# initializers in `MultiJSON::Options`.
|
|
32
|
+
default_options: Mutex.new,
|
|
33
|
+
# Guards the lazy dump-delegate resolution in
|
|
34
|
+
# `MultiJSON::Adapters::FastJsonparser`.
|
|
35
|
+
dump_delegate: Mutex.new
|
|
36
|
+
}.freeze
|
|
37
|
+
private_constant :MUTEXES
|
|
38
|
+
|
|
39
|
+
# Run a block while holding the named mutex
|
|
40
|
+
#
|
|
41
|
+
# The ``name`` symbol must be one of the keys in the internal
|
|
42
|
+
# ``MUTEXES`` table; an unknown name raises ``KeyError`` so a
|
|
43
|
+
# typo at the call site fails fast instead of silently dropping
|
|
44
|
+
# synchronization on the floor.
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
47
|
+
# @param name [Symbol] mutex identifier
|
|
48
|
+
# @yield block to execute while holding the mutex
|
|
49
|
+
# @return [Object] the block's return value
|
|
50
|
+
# @raise [KeyError] when ``name`` does not match a known mutex
|
|
51
|
+
# @example
|
|
52
|
+
# MultiJSON::Concurrency.synchronize(:adapter) { ... }
|
|
53
|
+
def self.synchronize(name, &)
|
|
54
|
+
MUTEXES.fetch(name).synchronize(&)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|