multi_json 1.19.1 → 1.20.0
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 +81 -0
- data/CONTRIBUTING.md +3 -2
- data/LICENSE.md +1 -1
- data/README.md +117 -34
- data/lib/multi_json/adapter.rb +80 -15
- data/lib/multi_json/adapter_error.rb +10 -1
- data/lib/multi_json/adapter_selector.rb +77 -45
- data/lib/multi_json/adapters/fast_jsonparser.rb +48 -20
- data/lib/multi_json/adapters/json_gem.rb +36 -10
- data/lib/multi_json/adapters/oj.rb +33 -10
- data/lib/multi_json/adapters/oj_common.rb +24 -27
- data/lib/multi_json/adapters/yajl.rb +4 -1
- data/lib/multi_json/concurrency.rb +57 -0
- data/lib/multi_json/deprecated.rb +110 -0
- data/lib/multi_json/options.rb +42 -16
- data/lib/multi_json/options_cache/mutex_store.rb +65 -0
- data/lib/multi_json/options_cache.rb +39 -65
- data/lib/multi_json/parse_error.rb +59 -2
- data/lib/multi_json/version.rb +4 -2
- data/lib/multi_json.rb +158 -125
- metadata +10 -12
- data/lib/multi_json/adapters/gson.rb +0 -37
- data/lib/multi_json/adapters/jr_jackson.rb +0 -52
- data/lib/multi_json/adapters/ok_json.rb +0 -43
- data/lib/multi_json/convertible_hash_keys.rb +0 -66
- data/lib/multi_json/vendor/okjson.rb +0 -545
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MultiJson
|
|
2
4
|
# Handles adapter discovery, loading, and selection
|
|
3
5
|
#
|
|
@@ -10,17 +12,25 @@ module MultiJson
|
|
|
10
12
|
module AdapterSelector
|
|
11
13
|
extend self
|
|
12
14
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
ADAPTERS = {
|
|
21
|
+
fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
|
|
22
|
+
oj: {require: "oj", loaded: "Oj"},
|
|
23
|
+
yajl: {require: "yajl", loaded: "Yajl"},
|
|
24
|
+
jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
|
|
25
|
+
json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
|
|
26
|
+
gson: {require: "gson", loaded: "Gson"}
|
|
23
27
|
}.freeze
|
|
28
|
+
private_constant :ADAPTERS
|
|
29
|
+
|
|
30
|
+
# Backwards-compatible view of {ADAPTERS} that exposes only the
|
|
31
|
+
# require paths. Tests still poke at this constant to stub or break
|
|
32
|
+
# the require step.
|
|
33
|
+
REQUIREMENT_MAP = ADAPTERS.transform_values { |meta| meta[:require] }.freeze
|
|
24
34
|
|
|
25
35
|
# Returns the default adapter to use
|
|
26
36
|
#
|
|
@@ -29,7 +39,27 @@ module MultiJson
|
|
|
29
39
|
# @example
|
|
30
40
|
# AdapterSelector.default_adapter #=> :oj
|
|
31
41
|
def default_adapter
|
|
32
|
-
@default_adapter ||= detect_best_adapter
|
|
42
|
+
Concurrency.synchronize(:default_adapter) { @default_adapter ||= detect_best_adapter }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns the default adapter class, excluding the given adapter name
|
|
46
|
+
#
|
|
47
|
+
# Used by adapters that only implement one direction (e.g.
|
|
48
|
+
# FastJsonparser only parses) so the other direction can be delegated
|
|
49
|
+
# to whichever library MultiJson would otherwise pick.
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
# @param excluded [Symbol] adapter name to skip during detection
|
|
53
|
+
# @return [Class] the adapter class
|
|
54
|
+
# @example
|
|
55
|
+
# AdapterSelector.default_adapter_excluding(:fast_jsonparser) #=> MultiJson::Adapters::Oj
|
|
56
|
+
def default_adapter_excluding(excluded)
|
|
57
|
+
Concurrency.synchronize(:default_adapter) do
|
|
58
|
+
name = loaded_adapter(excluding: excluded)
|
|
59
|
+
name ||= installable_adapter(excluding: excluded)
|
|
60
|
+
name ||= fallback_adapter
|
|
61
|
+
load_adapter_by_name(name.to_s)
|
|
62
|
+
end
|
|
33
63
|
end
|
|
34
64
|
|
|
35
65
|
private
|
|
@@ -45,23 +75,24 @@ module MultiJson
|
|
|
45
75
|
# Finds an already-loaded JSON library
|
|
46
76
|
#
|
|
47
77
|
# @api private
|
|
78
|
+
# @param excluding [Symbol, nil] adapter name to skip during detection
|
|
48
79
|
# @return [Symbol, nil] adapter name if found
|
|
49
|
-
def loaded_adapter
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
:gson if defined?(::Gson)
|
|
80
|
+
def loaded_adapter(excluding: nil)
|
|
81
|
+
ADAPTERS.each do |name, meta|
|
|
82
|
+
next if name == excluding
|
|
83
|
+
return name if Object.const_defined?(meta.fetch(:loaded))
|
|
84
|
+
end
|
|
85
|
+
nil
|
|
57
86
|
end
|
|
58
87
|
|
|
59
88
|
# Tries to require and use an installable adapter
|
|
60
89
|
#
|
|
61
90
|
# @api private
|
|
91
|
+
# @param excluding [Symbol, nil] adapter name to skip during detection
|
|
62
92
|
# @return [Symbol, nil] adapter name if successfully required
|
|
63
|
-
def installable_adapter
|
|
64
|
-
|
|
93
|
+
def installable_adapter(excluding: nil)
|
|
94
|
+
REQUIREMENT_MAP.each_key do |adapter_name|
|
|
95
|
+
next if adapter_name == excluding
|
|
65
96
|
return adapter_name if try_require(adapter_name)
|
|
66
97
|
end
|
|
67
98
|
nil
|
|
@@ -73,7 +104,7 @@ module MultiJson
|
|
|
73
104
|
# @param adapter_name [Symbol] adapter to require
|
|
74
105
|
# @return [Boolean] true if require succeeded
|
|
75
106
|
def try_require(adapter_name)
|
|
76
|
-
require
|
|
107
|
+
require REQUIREMENT_MAP.fetch(adapter_name)
|
|
77
108
|
true
|
|
78
109
|
rescue ::LoadError
|
|
79
110
|
false
|
|
@@ -81,58 +112,59 @@ module MultiJson
|
|
|
81
112
|
|
|
82
113
|
# Returns the fallback adapter when no others available
|
|
83
114
|
#
|
|
115
|
+
# The json gem is a Ruby default gem since Ruby 1.9, so in practice
|
|
116
|
+
# the installable-adapter step always succeeds before reaching this
|
|
117
|
+
# fallback on any supported Ruby version. The warning below only
|
|
118
|
+
# fires in tests that deliberately break the require path.
|
|
119
|
+
#
|
|
84
120
|
# @api private
|
|
85
|
-
# @return [Symbol] the
|
|
121
|
+
# @return [Symbol] the json_gem adapter name
|
|
86
122
|
def fallback_adapter
|
|
87
123
|
warn_about_fallback unless @default_adapter_warning_shown
|
|
88
124
|
@default_adapter_warning_shown = true
|
|
89
|
-
:
|
|
125
|
+
:json_gem
|
|
90
126
|
end
|
|
91
127
|
|
|
92
|
-
# Warns the user about
|
|
128
|
+
# Warns the user about reaching the last-resort fallback
|
|
93
129
|
#
|
|
94
130
|
# @api private
|
|
95
131
|
# @return [void]
|
|
96
132
|
def warn_about_fallback
|
|
97
133
|
Kernel.warn(
|
|
98
|
-
"[WARNING] MultiJson is
|
|
99
|
-
"
|
|
134
|
+
"[WARNING] MultiJson is falling back to the json_gem adapter " \
|
|
135
|
+
"because no other JSON library could be loaded."
|
|
100
136
|
)
|
|
101
137
|
end
|
|
102
138
|
|
|
103
139
|
# Loads an adapter from a specification
|
|
104
140
|
#
|
|
105
141
|
# @api private
|
|
106
|
-
# @param adapter_spec [Symbol, String, Module, nil] adapter specification
|
|
142
|
+
# @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
|
|
107
143
|
# @return [Class] the adapter class
|
|
108
144
|
def load_adapter(adapter_spec)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
case adapter_spec
|
|
146
|
+
when ::String then load_adapter_by_name(adapter_spec)
|
|
147
|
+
when ::Symbol then load_adapter_by_name(adapter_spec.to_s)
|
|
148
|
+
when nil, false then load_adapter(default_adapter)
|
|
149
|
+
when ::Module then adapter_spec
|
|
150
|
+
else raise ::LoadError, "expected adapter to be a Symbol, String, or Module, got #{adapter_spec.inspect}"
|
|
151
|
+
end
|
|
113
152
|
rescue ::LoadError => e
|
|
114
153
|
raise AdapterError.build(e)
|
|
115
154
|
end
|
|
116
155
|
|
|
117
|
-
# Finds the appropriate loader for an adapter specification
|
|
118
|
-
#
|
|
119
|
-
# @api private
|
|
120
|
-
# @param adapter_spec [Object] adapter specification
|
|
121
|
-
# @return [Proc, nil] loader proc if found
|
|
122
|
-
def find_loader_for(adapter_spec)
|
|
123
|
-
klass = adapter_spec.class
|
|
124
|
-
return LOADERS.fetch(klass) if LOADERS.key?(klass)
|
|
125
|
-
|
|
126
|
-
LOADERS.fetch(Module) if adapter_spec.is_a?(Module)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
156
|
# Loads an adapter by its string name
|
|
130
157
|
#
|
|
158
|
+
# ``jrjackson`` (the JrJackson gem's name) is normalized to
|
|
159
|
+
# ``jr_jackson`` (the adapter file/class name) for backwards
|
|
160
|
+
# compatibility with the original gem-name alias.
|
|
161
|
+
#
|
|
131
162
|
# @api private
|
|
132
163
|
# @param name [String] adapter name
|
|
133
164
|
# @return [Class] the adapter class
|
|
134
165
|
def load_adapter_by_name(name)
|
|
135
|
-
normalized =
|
|
166
|
+
normalized = name.downcase
|
|
167
|
+
normalized = "jr_jackson" if normalized == "jrjackson"
|
|
136
168
|
require_relative "adapters/#{normalized}"
|
|
137
169
|
|
|
138
170
|
class_name = normalized.split("_").map(&:capitalize).join
|
|
@@ -1,24 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "fast_jsonparser"
|
|
2
|
-
require "oj"
|
|
3
4
|
require_relative "../adapter"
|
|
4
|
-
require_relative "
|
|
5
|
+
require_relative "../adapter_selector"
|
|
5
6
|
|
|
6
7
|
module MultiJson
|
|
7
8
|
module Adapters
|
|
8
|
-
# Use the FastJsonparser library to load and
|
|
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).
|
|
9
20
|
class FastJsonparser < Adapter
|
|
10
|
-
include OjCommon
|
|
11
|
-
|
|
12
21
|
defaults :load, symbolize_keys: false
|
|
13
|
-
defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
|
|
14
22
|
|
|
23
|
+
# Exception raised when JSON parsing fails
|
|
15
24
|
ParseError = ::FastJsonparser::ParseError
|
|
16
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
|
+
|
|
17
52
|
# Parse a JSON string into a Ruby object
|
|
18
53
|
#
|
|
54
|
+
# FastJsonparser.parse only accepts ``symbolize_keys`` and raises
|
|
55
|
+
# on unknown keyword arguments, so the adapter explicitly forwards
|
|
56
|
+
# only that option and silently drops the rest. Pass other options
|
|
57
|
+
# through ``MultiJson.load_options=`` and they'll apply to whichever
|
|
58
|
+
# adapter MultiJson selects when fast_jsonparser isn't installed.
|
|
59
|
+
#
|
|
19
60
|
# @api private
|
|
20
61
|
# @param string [String] JSON string to parse
|
|
21
|
-
# @param options [Hash] parsing options
|
|
62
|
+
# @param options [Hash] parsing options (only :symbolize_keys is honored)
|
|
22
63
|
# @return [Object] parsed Ruby object
|
|
23
64
|
#
|
|
24
65
|
# @example Parse JSON string
|
|
@@ -26,19 +67,6 @@ module MultiJson
|
|
|
26
67
|
def load(string, options = {})
|
|
27
68
|
::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_keys])
|
|
28
69
|
end
|
|
29
|
-
|
|
30
|
-
# Serialize a Ruby object to JSON
|
|
31
|
-
#
|
|
32
|
-
# @api private
|
|
33
|
-
# @param object [Object] object to serialize
|
|
34
|
-
# @param options [Hash] serialization options
|
|
35
|
-
# @return [String] JSON string
|
|
36
|
-
#
|
|
37
|
-
# @example Serialize object to JSON
|
|
38
|
-
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
39
|
-
def dump(object, options = {})
|
|
40
|
-
::Oj.dump(object, prepare_dump_options(options))
|
|
41
|
-
end
|
|
42
70
|
end
|
|
43
71
|
end
|
|
44
72
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative "../adapter"
|
|
2
4
|
require "json"
|
|
3
5
|
|
|
@@ -5,6 +7,7 @@ module MultiJson
|
|
|
5
7
|
module Adapters
|
|
6
8
|
# Use the JSON gem to dump/load.
|
|
7
9
|
class JsonGem < Adapter
|
|
10
|
+
# Exception raised when JSON parsing fails
|
|
8
11
|
ParseError = ::JSON::ParserError
|
|
9
12
|
|
|
10
13
|
defaults :load, create_additions: false, quirks_mode: true
|
|
@@ -23,14 +26,21 @@ module MultiJson
|
|
|
23
26
|
# @param string [String] JSON string to parse
|
|
24
27
|
# @param options [Hash] parsing options
|
|
25
28
|
# @return [Object] parsed Ruby object
|
|
29
|
+
# @raise [::JSON::ParserError] when input contains invalid bytes that
|
|
30
|
+
# cannot be transcoded to UTF-8
|
|
26
31
|
#
|
|
27
32
|
# @example Parse JSON string
|
|
28
33
|
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
29
34
|
def load(string, options = {})
|
|
30
|
-
|
|
35
|
+
if string.encoding != Encoding::UTF_8
|
|
36
|
+
begin
|
|
37
|
+
string = string.encode(Encoding::UTF_8)
|
|
38
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
|
|
39
|
+
raise ::JSON::ParserError, e.message
|
|
40
|
+
end
|
|
41
|
+
end
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
::JSON.parse(string, options)
|
|
43
|
+
::JSON.parse(string, translate_load_options(options))
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
# Serialize a Ruby object to JSON
|
|
@@ -43,16 +53,32 @@ module MultiJson
|
|
|
43
53
|
# @example Serialize object to JSON
|
|
44
54
|
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
45
55
|
def dump(object, options = {})
|
|
46
|
-
opts = options.except(:adapter)
|
|
47
56
|
json_object = object.respond_to?(:as_json) ? object.as_json : object
|
|
48
|
-
return ::JSON.dump(json_object) if
|
|
57
|
+
return ::JSON.dump(json_object) if options.empty?
|
|
58
|
+
return ::JSON.generate(json_object, options) unless options.key?(:pretty)
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
# Common case: ``pretty: true`` is the only option, so the merge
|
|
61
|
+
# would just produce a copy of PRETTY_STATE_PROTOTYPE.
|
|
62
|
+
return ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE) if options.size == 1
|
|
63
|
+
|
|
64
|
+
::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE.merge(options.except(:pretty)))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Translate ``:symbolize_keys`` into JSON gem's ``:symbolize_names``
|
|
70
|
+
#
|
|
71
|
+
# Returns a new hash without mutating the input. ``options`` is the
|
|
72
|
+
# cached hash returned from {Adapter.merged_load_options}, so in-place
|
|
73
|
+
# edits would pollute the cache and corrupt subsequent calls.
|
|
74
|
+
#
|
|
75
|
+
# @api private
|
|
76
|
+
# @param options [Hash] merged load options
|
|
77
|
+
# @return [Hash] options with ``:symbolize_keys`` translated
|
|
78
|
+
def translate_load_options(options)
|
|
79
|
+
return options unless options[:symbolize_keys]
|
|
54
80
|
|
|
55
|
-
|
|
81
|
+
options.except(:symbolize_keys).merge(symbolize_names: true)
|
|
56
82
|
end
|
|
57
83
|
end
|
|
58
84
|
end
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "oj"
|
|
2
4
|
require_relative "../adapter"
|
|
3
5
|
require_relative "oj_common"
|
|
4
6
|
|
|
5
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
|
|
@@ -11,13 +17,13 @@ module MultiJson
|
|
|
11
17
|
defaults :load, mode: :strict, symbolize_keys: false
|
|
12
18
|
defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
|
|
13
19
|
|
|
14
|
-
# In certain cases
|
|
15
|
-
# of its own class.
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# shouldn't be a problem since
|
|
20
|
-
#
|
|
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.
|
|
21
27
|
class ParseError < ::SyntaxError
|
|
22
28
|
WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].freeze
|
|
23
29
|
private_constant :WRAPPED_CLASSES
|
|
@@ -31,7 +37,7 @@ module MultiJson
|
|
|
31
37
|
# @example Match parse errors in rescue
|
|
32
38
|
# rescue ParseError => e
|
|
33
39
|
def self.===(exception)
|
|
34
|
-
exception.
|
|
40
|
+
exception.class.ancestors.any? { |ancestor| WRAPPED_CLASSES.include?(ancestor.to_s) }
|
|
35
41
|
end
|
|
36
42
|
end
|
|
37
43
|
|
|
@@ -45,8 +51,7 @@ module MultiJson
|
|
|
45
51
|
# @example Parse JSON string
|
|
46
52
|
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
47
53
|
def load(string, options = {})
|
|
48
|
-
|
|
49
|
-
::Oj.load(string, options)
|
|
54
|
+
::Oj.load(string, translate_load_options(options))
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
# Serialize a Ruby object to JSON
|
|
@@ -61,6 +66,24 @@ module MultiJson
|
|
|
61
66
|
def dump(object, options = {})
|
|
62
67
|
::Oj.dump(object, prepare_dump_options(options))
|
|
63
68
|
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Translate ``:symbolize_keys`` 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_keys`` translated
|
|
84
|
+
def translate_load_options(options)
|
|
85
|
+
options.except(:symbolize_keys).merge(symbol_keys: options[:symbolize_keys] == true)
|
|
86
|
+
end
|
|
64
87
|
end
|
|
65
88
|
end
|
|
66
89
|
end
|
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MultiJson
|
|
4
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
|
|
5
11
|
module OjCommon
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
space_before: "",
|
|
16
|
-
object_nl: "\n",
|
|
17
|
-
array_nl: "\n",
|
|
18
|
-
ascii_only: false
|
|
19
|
-
}.freeze
|
|
20
|
-
private_constant :PRETTY_STATE_PROTOTYPE
|
|
21
|
-
end
|
|
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
|
|
22
21
|
|
|
23
22
|
private
|
|
24
23
|
|
|
25
|
-
# Prepare options for Oj.dump
|
|
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.
|
|
26
30
|
#
|
|
27
31
|
# @api private
|
|
28
32
|
# @param options [Hash] serialization options
|
|
@@ -31,16 +35,9 @@ module MultiJson
|
|
|
31
35
|
# @example Prepare dump options
|
|
32
36
|
# prepare_dump_options(pretty: true)
|
|
33
37
|
def prepare_dump_options(options)
|
|
34
|
-
|
|
35
|
-
options[:indent] = 2 if options[:pretty]
|
|
36
|
-
options[:indent] = options[:indent].to_i if options[:indent]
|
|
37
|
-
elsif OJ_V3
|
|
38
|
-
options.merge!(PRETTY_STATE_PROTOTYPE.dup) if options.delete(:pretty)
|
|
39
|
-
else
|
|
40
|
-
raise "Unsupported Oj version: #{::Oj::VERSION}"
|
|
41
|
-
end
|
|
38
|
+
return options unless options.key?(:pretty)
|
|
42
39
|
|
|
43
|
-
options
|
|
40
|
+
options.except(:pretty).merge(PRETTY_STATE_PROTOTYPE)
|
|
44
41
|
end
|
|
45
42
|
end
|
|
46
43
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "yajl"
|
|
2
4
|
require_relative "../adapter"
|
|
3
5
|
|
|
@@ -5,6 +7,7 @@ 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
|
|
|
10
13
|
# Parse a JSON string into a Ruby object
|
|
@@ -17,7 +20,7 @@ module MultiJson
|
|
|
17
20
|
# @example Parse JSON string
|
|
18
21
|
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
19
22
|
def load(string, options = {})
|
|
20
|
-
::Yajl::Parser.new(
|
|
23
|
+
::Yajl::Parser.new(options).parse(string)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
# Serialize a Ruby object to JSON
|
|
@@ -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
|