multi_json 1.20.0-java
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 +7 -0
- data/CHANGELOG.md +309 -0
- data/CONTRIBUTING.md +53 -0
- data/LICENSE.md +20 -0
- data/README.md +201 -0
- data/lib/multi_json/adapter.rb +182 -0
- data/lib/multi_json/adapter_error.rb +42 -0
- data/lib/multi_json/adapter_selector.rb +174 -0
- data/lib/multi_json/adapters/fast_jsonparser.rb +72 -0
- data/lib/multi_json/adapters/gson.rb +55 -0
- data/lib/multi_json/adapters/jr_jackson.rb +43 -0
- data/lib/multi_json/adapters/json_gem.rb +85 -0
- data/lib/multi_json/adapters/oj.rb +89 -0
- data/lib/multi_json/adapters/oj_common.rb +44 -0
- data/lib/multi_json/adapters/yajl.rb +40 -0
- data/lib/multi_json/concurrency.rb +57 -0
- data/lib/multi_json/deprecated.rb +110 -0
- data/lib/multi_json/options.rb +112 -0
- data/lib/multi_json/options_cache/concurrent_store.rb +86 -0
- data/lib/multi_json/options_cache.rb +72 -0
- data/lib/multi_json/parse_error.rb +103 -0
- data/lib/multi_json/version.rb +30 -0
- data/lib/multi_json.rb +268 -0
- metadata +86 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require_relative "options"
|
|
5
|
+
|
|
6
|
+
module MultiJson
|
|
7
|
+
# Base class for JSON adapter implementations
|
|
8
|
+
#
|
|
9
|
+
# Each adapter wraps a specific JSON library (Oj, JSON gem, etc.) and
|
|
10
|
+
# provides a consistent interface. Uses Singleton pattern so each adapter
|
|
11
|
+
# class has exactly one instance.
|
|
12
|
+
#
|
|
13
|
+
# Subclasses must implement:
|
|
14
|
+
# - #load(string, options) -> parsed object
|
|
15
|
+
# - #dump(object, options) -> JSON string
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
class Adapter
|
|
19
|
+
extend Options
|
|
20
|
+
include Singleton
|
|
21
|
+
|
|
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
|
|
65
|
+
def defaults(action, value)
|
|
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)
|
|
68
|
+
|
|
69
|
+
instance_variable_set(:"@default_#{action}_options", value.freeze)
|
|
70
|
+
end
|
|
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
|
|
78
|
+
def load(string, options = {})
|
|
79
|
+
string = string.read if string.respond_to?(:read)
|
|
80
|
+
return nil if blank?(string)
|
|
81
|
+
|
|
82
|
+
instance.load(string, merged_load_options(options))
|
|
83
|
+
end
|
|
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
|
|
91
|
+
def dump(object, options = {})
|
|
92
|
+
instance.dump(object, merged_dump_options(options))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
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
|
|
135
|
+
def blank?(input)
|
|
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)
|
|
140
|
+
end
|
|
141
|
+
|
|
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)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
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)
|
|
163
|
+
end
|
|
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
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJson
|
|
4
|
+
# Raised when an adapter cannot be loaded or is not recognized
|
|
5
|
+
#
|
|
6
|
+
# @api public
|
|
7
|
+
class AdapterError < ArgumentError
|
|
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
|
|
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"))
|
|
34
|
+
def self.build(original_exception)
|
|
35
|
+
new(
|
|
36
|
+
"Did not recognize your adapter specification " \
|
|
37
|
+
"(#{original_exception.class}: #{original_exception.message}).",
|
|
38
|
+
cause: original_exception
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
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"}
|
|
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
|
|
34
|
+
|
|
35
|
+
# Returns the default adapter to use
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
38
|
+
# @return [Symbol] adapter name
|
|
39
|
+
# @example
|
|
40
|
+
# AdapterSelector.default_adapter #=> :oj
|
|
41
|
+
def default_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
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Detects the best available JSON adapter
|
|
68
|
+
#
|
|
69
|
+
# @api private
|
|
70
|
+
# @return [Symbol] adapter name
|
|
71
|
+
def detect_best_adapter
|
|
72
|
+
loaded_adapter || installable_adapter || fallback_adapter
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Finds an already-loaded JSON library
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @param excluding [Symbol, nil] adapter name to skip during detection
|
|
79
|
+
# @return [Symbol, nil] adapter name if found
|
|
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
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Tries to require and use an installable adapter
|
|
89
|
+
#
|
|
90
|
+
# @api private
|
|
91
|
+
# @param excluding [Symbol, nil] adapter name to skip during detection
|
|
92
|
+
# @return [Symbol, nil] adapter name if successfully required
|
|
93
|
+
def installable_adapter(excluding: nil)
|
|
94
|
+
REQUIREMENT_MAP.each_key do |adapter_name|
|
|
95
|
+
next if adapter_name == excluding
|
|
96
|
+
return adapter_name if try_require(adapter_name)
|
|
97
|
+
end
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Attempts to require a JSON library
|
|
102
|
+
#
|
|
103
|
+
# @api private
|
|
104
|
+
# @param adapter_name [Symbol] adapter to require
|
|
105
|
+
# @return [Boolean] true if require succeeded
|
|
106
|
+
def try_require(adapter_name)
|
|
107
|
+
require REQUIREMENT_MAP.fetch(adapter_name)
|
|
108
|
+
true
|
|
109
|
+
rescue ::LoadError
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the fallback adapter when no others available
|
|
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
|
+
#
|
|
120
|
+
# @api private
|
|
121
|
+
# @return [Symbol] the json_gem adapter name
|
|
122
|
+
def fallback_adapter
|
|
123
|
+
warn_about_fallback unless @default_adapter_warning_shown
|
|
124
|
+
@default_adapter_warning_shown = true
|
|
125
|
+
:json_gem
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Warns the user about reaching the last-resort fallback
|
|
129
|
+
#
|
|
130
|
+
# @api private
|
|
131
|
+
# @return [void]
|
|
132
|
+
def warn_about_fallback
|
|
133
|
+
Kernel.warn(
|
|
134
|
+
"[WARNING] MultiJson is falling back to the json_gem adapter " \
|
|
135
|
+
"because no other JSON library could be loaded."
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Loads an adapter from a specification
|
|
140
|
+
#
|
|
141
|
+
# @api private
|
|
142
|
+
# @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
|
|
143
|
+
# @return [Class] the adapter class
|
|
144
|
+
def load_adapter(adapter_spec)
|
|
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
|
|
152
|
+
rescue ::LoadError => e
|
|
153
|
+
raise AdapterError.build(e)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Loads an adapter by its string name
|
|
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
|
+
#
|
|
162
|
+
# @api private
|
|
163
|
+
# @param name [String] adapter name
|
|
164
|
+
# @return [Class] the adapter class
|
|
165
|
+
def load_adapter_by_name(name)
|
|
166
|
+
normalized = name.downcase
|
|
167
|
+
normalized = "jr_jackson" if normalized == "jrjackson"
|
|
168
|
+
require_relative "adapters/#{normalized}"
|
|
169
|
+
|
|
170
|
+
class_name = normalized.split("_").map(&:capitalize).join
|
|
171
|
+
::MultiJson::Adapters.const_get(class_name)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
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_keys: 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
|
+
# 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
|
+
#
|
|
60
|
+
# @api private
|
|
61
|
+
# @param string [String] JSON string to parse
|
|
62
|
+
# @param options [Hash] parsing options (only :symbolize_keys is honored)
|
|
63
|
+
# @return [Object] parsed Ruby object
|
|
64
|
+
#
|
|
65
|
+
# @example Parse JSON string
|
|
66
|
+
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
67
|
+
def load(string, options = {})
|
|
68
|
+
::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_keys])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gson"
|
|
4
|
+
require_relative "../adapter"
|
|
5
|
+
|
|
6
|
+
module MultiJson
|
|
7
|
+
module Adapters
|
|
8
|
+
# Use the gson.rb library to dump/load.
|
|
9
|
+
class Gson < Adapter
|
|
10
|
+
# Exception raised when JSON parsing fails
|
|
11
|
+
ParseError = ::Gson::DecodeError
|
|
12
|
+
|
|
13
|
+
# Pre-allocated zero-options decoder/encoder for the dominant call
|
|
14
|
+
# path. Without an explicit ``defaults :load`` / ``defaults :dump``
|
|
15
|
+
# block on this adapter, ``MultiJson.load(json)`` and
|
|
16
|
+
# ``MultiJson.dump(obj)`` arrive here with the shared frozen empty
|
|
17
|
+
# hash from {Adapter.strip_adapter_key}, so reusing a single
|
|
18
|
+
# decoder/encoder instance avoids the per-call ``Decoder.new`` /
|
|
19
|
+
# ``Encoder.new`` allocation that the previous implementation
|
|
20
|
+
# incurred. Java Gson is documented thread-safe, so sharing the
|
|
21
|
+
# underlying handle across threads is safe.
|
|
22
|
+
DEFAULT_DECODER = ::Gson::Decoder.new({})
|
|
23
|
+
DEFAULT_ENCODER = ::Gson::Encoder.new({})
|
|
24
|
+
private_constant :DEFAULT_DECODER, :DEFAULT_ENCODER
|
|
25
|
+
|
|
26
|
+
# Parse a JSON string into a Ruby object
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
# @param string [String] JSON string to parse
|
|
30
|
+
# @param options [Hash] parsing options
|
|
31
|
+
# @return [Object] parsed Ruby object
|
|
32
|
+
#
|
|
33
|
+
# @example Parse JSON string
|
|
34
|
+
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
35
|
+
def load(string, options = {})
|
|
36
|
+
decoder = options.empty? ? DEFAULT_DECODER : ::Gson::Decoder.new(options)
|
|
37
|
+
decoder.decode(string)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Serialize a Ruby object to JSON
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
# @param object [Object] object to serialize
|
|
44
|
+
# @param options [Hash] serialization options
|
|
45
|
+
# @return [String] JSON string
|
|
46
|
+
#
|
|
47
|
+
# @example Serialize object to JSON
|
|
48
|
+
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
49
|
+
def dump(object, options = {})
|
|
50
|
+
encoder = options.empty? ? DEFAULT_ENCODER : ::Gson::Encoder.new(options)
|
|
51
|
+
encoder.encode(object)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jrjackson" unless defined?(JrJackson)
|
|
4
|
+
require_relative "../adapter"
|
|
5
|
+
|
|
6
|
+
module MultiJson
|
|
7
|
+
module Adapters
|
|
8
|
+
# Use the jrjackson.rb library to dump/load.
|
|
9
|
+
class JrJackson < Adapter
|
|
10
|
+
# Exception raised when JSON parsing fails
|
|
11
|
+
ParseError = ::JrJackson::ParseError
|
|
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"}
|
|
22
|
+
def load(string, options = {})
|
|
23
|
+
::JrJackson::Json.load(string, options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Serialize a Ruby object to JSON
|
|
27
|
+
#
|
|
28
|
+
# Requires JrJackson >= 0.4.18, which accepts an options hash as
|
|
29
|
+
# the second argument to ``Json.dump``.
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
# @param object [Object] object to serialize
|
|
33
|
+
# @param options [Hash] serialization options
|
|
34
|
+
# @return [String] JSON string
|
|
35
|
+
#
|
|
36
|
+
# @example Serialize object to JSON
|
|
37
|
+
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
38
|
+
def dump(object, options = {})
|
|
39
|
+
::JrJackson::Json.dump(object, options)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../adapter"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module MultiJson
|
|
7
|
+
module Adapters
|
|
8
|
+
# Use the JSON gem to dump/load.
|
|
9
|
+
class JsonGem < Adapter
|
|
10
|
+
# Exception raised when JSON parsing fails
|
|
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
|
+
# @api private
|
|
26
|
+
# @param string [String] JSON string to parse
|
|
27
|
+
# @param options [Hash] parsing options
|
|
28
|
+
# @return [Object] parsed Ruby object
|
|
29
|
+
# @raise [::JSON::ParserError] when input contains invalid bytes that
|
|
30
|
+
# cannot be transcoded to UTF-8
|
|
31
|
+
#
|
|
32
|
+
# @example Parse JSON string
|
|
33
|
+
# adapter.load('{"key":"value"}') #=> {"key" => "value"}
|
|
34
|
+
def load(string, options = {})
|
|
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
|
|
42
|
+
|
|
43
|
+
::JSON.parse(string, translate_load_options(options))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Serialize a Ruby object to JSON
|
|
47
|
+
#
|
|
48
|
+
# @api private
|
|
49
|
+
# @param object [Object] object to serialize
|
|
50
|
+
# @param options [Hash] serialization options
|
|
51
|
+
# @return [String] JSON string
|
|
52
|
+
#
|
|
53
|
+
# @example Serialize object to JSON
|
|
54
|
+
# adapter.dump({key: "value"}) #=> '{"key":"value"}'
|
|
55
|
+
def dump(object, options = {})
|
|
56
|
+
json_object = object.respond_to?(:as_json) ? object.as_json : object
|
|
57
|
+
return ::JSON.dump(json_object) if options.empty?
|
|
58
|
+
return ::JSON.generate(json_object, options) unless options.key?(:pretty)
|
|
59
|
+
|
|
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]
|
|
80
|
+
|
|
81
|
+
options.except(:symbolize_keys).merge(symbolize_names: true)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|