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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Deprecated public API kept around for one major release
|
|
4
|
+
#
|
|
5
|
+
# Each method here emits a one-time deprecation warning on first call and
|
|
6
|
+
# delegates to its current-API counterpart. The whole file is loaded by
|
|
7
|
+
# {MultiJson} so the deprecation surface stays out of the main module
|
|
8
|
+
# definition.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module MultiJson
|
|
12
|
+
class << self
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# Define a deprecated alias that delegates to a new method name
|
|
16
|
+
#
|
|
17
|
+
# The generated singleton method emits a one-time deprecation
|
|
18
|
+
# warning naming the replacement, then forwards all positional and
|
|
19
|
+
# keyword arguments plus any block to ``replacement``. Used for the
|
|
20
|
+
# ``decode`` / ``encode`` / ``engine*`` / ``with_engine`` /
|
|
21
|
+
# ``default_engine`` aliases that are scheduled for removal in v2.0.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
# @param name [Symbol] deprecated method name
|
|
25
|
+
# @param replacement [Symbol] current-API method to delegate to
|
|
26
|
+
# @return [Symbol] the defined method name
|
|
27
|
+
# @example
|
|
28
|
+
# deprecate_alias :decode, :load
|
|
29
|
+
def deprecate_alias(name, replacement)
|
|
30
|
+
message = "MultiJson.#{name} is deprecated and will be removed in v2.0. Use MultiJson.#{replacement} instead."
|
|
31
|
+
define_singleton_method(name) do |*args, **kwargs, &block|
|
|
32
|
+
warn_deprecation_once(name, message)
|
|
33
|
+
public_send(replacement, *args, **kwargs, &block)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Define a deprecated method whose body needs custom delegation
|
|
38
|
+
#
|
|
39
|
+
# Used for the ``default_options`` / ``default_options=`` pair
|
|
40
|
+
# whose body fans out to multiple replacement methods, and for the
|
|
41
|
+
# ``cached_options`` / ``reset_cached_options!`` no-op stubs that
|
|
42
|
+
# have no current-API counterpart at all. The block runs in its
|
|
43
|
+
# own lexical ``self``, which is the ``MultiJson`` module since
|
|
44
|
+
# every call site sits inside ``module MultiJson`` below.
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
47
|
+
# @param name [Symbol] deprecated method name
|
|
48
|
+
# @param message [String] warning to emit on first call
|
|
49
|
+
# @yield body to evaluate after the warning
|
|
50
|
+
# @return [Symbol] the defined method name
|
|
51
|
+
# @example
|
|
52
|
+
# deprecate_method(:cached_options, "...") { nil }
|
|
53
|
+
def deprecate_method(name, message, &body)
|
|
54
|
+
define_singleton_method(name) do |*args, **kwargs|
|
|
55
|
+
warn_deprecation_once(name, message)
|
|
56
|
+
body.call(*args, **kwargs)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
deprecate_alias :decode, :load
|
|
62
|
+
deprecate_alias :encode, :dump
|
|
63
|
+
deprecate_alias :engine, :adapter
|
|
64
|
+
deprecate_alias :engine=, :adapter=
|
|
65
|
+
deprecate_alias :default_engine, :default_adapter
|
|
66
|
+
deprecate_alias :with_engine, :with_adapter
|
|
67
|
+
|
|
68
|
+
deprecate_method(
|
|
69
|
+
:default_options=,
|
|
70
|
+
"MultiJson.default_options setter is deprecated\n" \
|
|
71
|
+
"Use MultiJson.load_options and MultiJson.dump_options instead"
|
|
72
|
+
) { |value| self.load_options = self.dump_options = value }
|
|
73
|
+
|
|
74
|
+
deprecate_method(
|
|
75
|
+
:default_options,
|
|
76
|
+
"MultiJson.default_options is deprecated\n" \
|
|
77
|
+
"Use MultiJson.load_options or MultiJson.dump_options instead"
|
|
78
|
+
) { load_options }
|
|
79
|
+
|
|
80
|
+
%i[cached_options reset_cached_options!].each do |name|
|
|
81
|
+
deprecate_method(name, "MultiJson.#{name} method is deprecated and no longer used.") { nil }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Instance-method delegate for the deprecated default_options setter
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @deprecated Use {MultiJson.load_options=} and {MultiJson.dump_options=} instead
|
|
90
|
+
# @param value [Hash] options hash
|
|
91
|
+
# @return [Hash] the options hash
|
|
92
|
+
# @example
|
|
93
|
+
# class Foo; include MultiJson; end
|
|
94
|
+
# Foo.new.send(:default_options=, symbolize_keys: true)
|
|
95
|
+
def default_options=(value)
|
|
96
|
+
MultiJson.default_options = value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Instance-method delegate for the deprecated default_options getter
|
|
100
|
+
#
|
|
101
|
+
# @api private
|
|
102
|
+
# @deprecated Use {MultiJson.load_options} or {MultiJson.dump_options} instead
|
|
103
|
+
# @return [Hash] the current load options
|
|
104
|
+
# @example
|
|
105
|
+
# class Foo; include MultiJson; end
|
|
106
|
+
# Foo.new.send(:default_options)
|
|
107
|
+
def default_options
|
|
108
|
+
MultiJson.default_options
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/multi_json/options.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MultiJson
|
|
2
4
|
# Mixin providing configurable load/dump options
|
|
3
5
|
#
|
|
@@ -6,14 +8,20 @@ module MultiJson
|
|
|
6
8
|
#
|
|
7
9
|
# @api private
|
|
8
10
|
module Options
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
# Steep needs an inline `#:` annotation here because `{}.freeze`
|
|
12
|
+
# would be inferred as `Hash[untyped, untyped]` and trip
|
|
13
|
+
# `UnannotatedEmptyCollection`. The annotation requires
|
|
14
|
+
# `Hash.new.freeze` (not the `{}.freeze` rubocop would prefer)
|
|
15
|
+
# because the `#:` cast only applies to method-call results.
|
|
16
|
+
EMPTY_OPTIONS = Hash.new.freeze #: options # rubocop:disable Style/EmptyLiteral
|
|
11
17
|
|
|
12
18
|
# Set options for load operations
|
|
13
19
|
#
|
|
14
|
-
# @api
|
|
20
|
+
# @api public
|
|
15
21
|
# @param options [Hash, Proc] options hash or callable
|
|
16
22
|
# @return [Hash, Proc] the options
|
|
23
|
+
# @example
|
|
24
|
+
# MultiJson.load_options = {symbolize_keys: true}
|
|
17
25
|
def load_options=(options)
|
|
18
26
|
OptionsCache.reset
|
|
19
27
|
@load_options = options
|
|
@@ -21,9 +29,11 @@ module MultiJson
|
|
|
21
29
|
|
|
22
30
|
# Set options for dump operations
|
|
23
31
|
#
|
|
24
|
-
# @api
|
|
32
|
+
# @api public
|
|
25
33
|
# @param options [Hash, Proc] options hash or callable
|
|
26
34
|
# @return [Hash, Proc] the options
|
|
35
|
+
# @example
|
|
36
|
+
# MultiJson.dump_options = {pretty: true}
|
|
27
37
|
def dump_options=(options)
|
|
28
38
|
OptionsCache.reset
|
|
29
39
|
@dump_options = options
|
|
@@ -31,18 +41,29 @@ module MultiJson
|
|
|
31
41
|
|
|
32
42
|
# Get options for load operations
|
|
33
43
|
#
|
|
34
|
-
#
|
|
44
|
+
# When `@load_options` is a callable (proc/lambda), it's invoked
|
|
45
|
+
# with `args` as positional arguments — typically the merged
|
|
46
|
+
# options hash from `Adapter.merged_load_options`. When it's a
|
|
47
|
+
# plain hash, `args` is ignored.
|
|
48
|
+
#
|
|
49
|
+
# @api public
|
|
50
|
+
# @param args [Array<Object>] forwarded to the callable, ignored otherwise
|
|
35
51
|
# @return [Hash] resolved options hash
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
# @example
|
|
53
|
+
# MultiJson.load_options #=> {}
|
|
54
|
+
def load_options(*args)
|
|
55
|
+
resolve_options(@load_options, *args) || default_load_options
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
# Get options for dump operations
|
|
41
59
|
#
|
|
42
|
-
# @api
|
|
60
|
+
# @api public
|
|
61
|
+
# @param args [Array<Object>] forwarded to the callable, ignored otherwise
|
|
43
62
|
# @return [Hash] resolved options hash
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
# @example
|
|
64
|
+
# MultiJson.dump_options #=> {}
|
|
65
|
+
def dump_options(*args)
|
|
66
|
+
resolve_options(@dump_options, *args) || default_dump_options
|
|
46
67
|
end
|
|
47
68
|
|
|
48
69
|
# Get default load options
|
|
@@ -50,7 +71,7 @@ module MultiJson
|
|
|
50
71
|
# @api private
|
|
51
72
|
# @return [Hash] frozen empty hash
|
|
52
73
|
def default_load_options
|
|
53
|
-
@default_load_options ||= EMPTY_OPTIONS
|
|
74
|
+
Concurrency.synchronize(:default_options) { @default_load_options ||= EMPTY_OPTIONS }
|
|
54
75
|
end
|
|
55
76
|
|
|
56
77
|
# Get default dump options
|
|
@@ -58,7 +79,7 @@ module MultiJson
|
|
|
58
79
|
# @api private
|
|
59
80
|
# @return [Hash] frozen empty hash
|
|
60
81
|
def default_dump_options
|
|
61
|
-
@default_dump_options ||= EMPTY_OPTIONS
|
|
82
|
+
Concurrency.synchronize(:default_options) { @default_dump_options ||= EMPTY_OPTIONS }
|
|
62
83
|
end
|
|
63
84
|
|
|
64
85
|
private
|
|
@@ -67,9 +88,13 @@ module MultiJson
|
|
|
67
88
|
#
|
|
68
89
|
# @api private
|
|
69
90
|
# @param options [Hash, Proc, nil] options configuration
|
|
91
|
+
# @param args [Array<Object>] arguments forwarded to a callable provider
|
|
70
92
|
# @return [Hash, nil] resolved options hash
|
|
71
|
-
def resolve_options(options,
|
|
72
|
-
|
|
93
|
+
def resolve_options(options, *args)
|
|
94
|
+
if options.respond_to?(:call)
|
|
95
|
+
# @type var options: options_proc
|
|
96
|
+
return invoke_callable(options, *args)
|
|
97
|
+
end
|
|
73
98
|
|
|
74
99
|
options.to_hash if options.respond_to?(:to_hash)
|
|
75
100
|
end
|
|
@@ -78,9 +103,10 @@ module MultiJson
|
|
|
78
103
|
#
|
|
79
104
|
# @api private
|
|
80
105
|
# @param callable [Proc] options provider
|
|
106
|
+
# @param args [Array<Object>] arguments forwarded when the callable is non-arity-zero
|
|
81
107
|
# @return [Hash] options returned by the callable
|
|
82
|
-
def invoke_callable(callable,
|
|
83
|
-
callable.arity.zero? ? callable.call : callable.call(
|
|
108
|
+
def invoke_callable(callable, *args)
|
|
109
|
+
callable.arity.zero? ? callable.call : callable.call(*args)
|
|
84
110
|
end
|
|
85
111
|
end
|
|
86
112
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJson
|
|
4
|
+
module OptionsCache
|
|
5
|
+
# Thread-safe cache store backed by a Hash guarded by a Mutex
|
|
6
|
+
#
|
|
7
|
+
# Used on MRI and TruffleRuby, where a runtime dependency on
|
|
8
|
+
# concurrent-ruby would be overkill: the GVL on MRI makes single
|
|
9
|
+
# lookups atomic, and locking on both reads and writes keeps the
|
|
10
|
+
# small perf cost predictable across engines without adding a
|
|
11
|
+
# runtime dependency.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class Store
|
|
15
|
+
# Create a new cache store
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
# @return [Store] new store instance
|
|
19
|
+
def initialize
|
|
20
|
+
@cache = {}
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Clear all cached entries
|
|
25
|
+
#
|
|
26
|
+
# Held under the mutex because TruffleRuby (which also uses this
|
|
27
|
+
# backend via the ruby-platform gem) has true parallelism: a
|
|
28
|
+
# concurrent ``fetch`` racing with ``Hash#clear`` could corrupt
|
|
29
|
+
# iteration in a way that MRI's GVL would otherwise prevent.
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
# @return [void]
|
|
33
|
+
def reset
|
|
34
|
+
@mutex.synchronize { @cache.clear }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Fetch a value from cache or compute it
|
|
38
|
+
#
|
|
39
|
+
# When called with a block, returns the cached value or computes a
|
|
40
|
+
# new one. When called without a block, returns the cached value or
|
|
41
|
+
# the supplied default if the key is missing. Nil cached values are
|
|
42
|
+
# preserved because ``Hash#fetch`` only falls through to the default
|
|
43
|
+
# block when the key is truly missing. The ``block_given?`` check
|
|
44
|
+
# is hoisted out of the mutex so the no-block read path runs the
|
|
45
|
+
# check once per call instead of once inside the critical section.
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
# @param key [Object] cache key
|
|
49
|
+
# @param default [Object] value to return when key is missing and no
|
|
50
|
+
# block is given
|
|
51
|
+
# @yield block to compute value if not cached
|
|
52
|
+
# @return [Object] cached, computed, or default value
|
|
53
|
+
def fetch(key, default = nil)
|
|
54
|
+
return @mutex.synchronize { @cache.fetch(key) { default } } unless block_given?
|
|
55
|
+
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
@cache.fetch(key) do
|
|
58
|
+
@cache.shift if @cache.size >= OptionsCache.max_cache_size
|
|
59
|
+
@cache[key] = yield
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -1,74 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MultiJson
|
|
2
|
-
# Thread-safe
|
|
4
|
+
# Thread-safe bounded cache for merged options hashes
|
|
3
5
|
#
|
|
4
6
|
# Caches are separated for load and dump operations. Each cache is
|
|
5
7
|
# bounded to prevent unbounded memory growth when options are
|
|
6
|
-
# generated dynamically.
|
|
8
|
+
# generated dynamically. The ``Store`` backend is chosen at load time
|
|
9
|
+
# based on ``RUBY_ENGINE``: JRuby uses Concurrent::Map (shipped as a
|
|
10
|
+
# runtime dependency of the java-platform gem); MRI and TruffleRuby
|
|
11
|
+
# use a Hash guarded by a Mutex.
|
|
7
12
|
#
|
|
8
13
|
# @api private
|
|
9
14
|
module OptionsCache
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
# @api private
|
|
16
|
-
class Store
|
|
17
|
-
# Sentinel value to detect cache misses (unique object identity)
|
|
18
|
-
NOT_FOUND = Object.new
|
|
19
|
-
|
|
20
|
-
# Create a new cache store
|
|
21
|
-
#
|
|
22
|
-
# @api private
|
|
23
|
-
# @return [Store] new store instance
|
|
24
|
-
def initialize
|
|
25
|
-
@cache = {}
|
|
26
|
-
@mutex = Mutex.new
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Clear all cached entries
|
|
30
|
-
#
|
|
31
|
-
# @api private
|
|
32
|
-
# @return [void]
|
|
33
|
-
def reset
|
|
34
|
-
@mutex.synchronize { @cache.clear }
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Fetch a value from cache or compute it
|
|
38
|
-
#
|
|
39
|
-
# @api private
|
|
40
|
-
# @param key [Object] cache key
|
|
41
|
-
# @param default [Object] default value if key not found
|
|
42
|
-
# @yield block to compute value if not cached
|
|
43
|
-
# @return [Object] cached or computed value
|
|
44
|
-
def fetch(key, default = nil)
|
|
45
|
-
# Fast path: check cache without lock (safe for reads)
|
|
46
|
-
value = @cache.fetch(key, NOT_FOUND)
|
|
47
|
-
return value unless value.equal?(NOT_FOUND)
|
|
48
|
-
|
|
49
|
-
# Slow path: acquire lock and compute value
|
|
50
|
-
@mutex.synchronize do
|
|
51
|
-
@cache.fetch(key) { block_given? ? store(key, yield) : default }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
# Stores a value in the cache with LRU eviction
|
|
58
|
-
#
|
|
59
|
-
# @api private
|
|
60
|
-
# @param key [Object] cache key
|
|
61
|
-
# @param value [Object] value to store
|
|
62
|
-
# @return [Object] the stored value
|
|
63
|
-
def store(key, value)
|
|
64
|
-
# Double-check in case another thread computed while we waited
|
|
65
|
-
@cache.fetch(key) do
|
|
66
|
-
# Evict oldest entry if at capacity (Hash maintains insertion order)
|
|
67
|
-
@cache.shift if @cache.size >= MAX_CACHE_SIZE
|
|
68
|
-
@cache[key] = value
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
15
|
+
# Default bound on the number of cached entries per store. Applications
|
|
16
|
+
# that dynamically generate many distinct option hashes can raise this
|
|
17
|
+
# via {.max_cache_size=}.
|
|
18
|
+
DEFAULT_MAX_CACHE_SIZE = 1000
|
|
72
19
|
|
|
73
20
|
class << self
|
|
74
21
|
# Get the dump options cache
|
|
@@ -83,6 +30,19 @@ module MultiJson
|
|
|
83
30
|
# @return [Store] load cache store
|
|
84
31
|
attr_reader :load
|
|
85
32
|
|
|
33
|
+
# Maximum number of entries per cache store
|
|
34
|
+
#
|
|
35
|
+
# Applies to both the dump and load caches. Existing entries are
|
|
36
|
+
# left in place until normal eviction trims them below a lowered
|
|
37
|
+
# limit; call {.reset} if you need to evict immediately.
|
|
38
|
+
#
|
|
39
|
+
# @api public
|
|
40
|
+
# @return [Integer] current cache size limit
|
|
41
|
+
# @example
|
|
42
|
+
# MultiJson::OptionsCache.max_cache_size = 5000
|
|
43
|
+
# MultiJson::OptionsCache.max_cache_size #=> 5000
|
|
44
|
+
attr_accessor :max_cache_size
|
|
45
|
+
|
|
86
46
|
# Reset both caches
|
|
87
47
|
#
|
|
88
48
|
# @api private
|
|
@@ -93,6 +53,20 @@ module MultiJson
|
|
|
93
53
|
end
|
|
94
54
|
end
|
|
95
55
|
|
|
96
|
-
|
|
56
|
+
self.max_cache_size = DEFAULT_MAX_CACHE_SIZE
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module MultiJson
|
|
61
|
+
module OptionsCache
|
|
62
|
+
# Dynamic require path so MRI (mutex_store) and JRuby
|
|
63
|
+
# (concurrent_store) execute the same physical line, avoiding a
|
|
64
|
+
# dead-branch ``require_relative`` that would otherwise drop
|
|
65
|
+
# JRuby's line coverage below 100%.
|
|
66
|
+
BACKENDS = {"jruby" => "concurrent_store"}.freeze
|
|
67
|
+
private_constant :BACKENDS
|
|
97
68
|
end
|
|
98
69
|
end
|
|
70
|
+
|
|
71
|
+
require_relative "options_cache/#{MultiJson::OptionsCache.send(:const_get, :BACKENDS).fetch(RUBY_ENGINE, "mutex_store")}"
|
|
72
|
+
MultiJson::OptionsCache.reset
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MultiJson
|
|
2
4
|
# Raised when JSON parsing fails
|
|
3
5
|
#
|
|
4
|
-
# Wraps the underlying adapter's parse error with the original input
|
|
6
|
+
# Wraps the underlying adapter's parse error with the original input
|
|
7
|
+
# data, plus best-effort line and column extraction from the adapter's
|
|
8
|
+
# error message. Line/column are populated for adapters that include
|
|
9
|
+
# them in their messages (Oj, the json gem) and remain nil for
|
|
10
|
+
# adapters that don't (Yajl, fast_jsonparser).
|
|
5
11
|
#
|
|
6
12
|
# @api public
|
|
7
13
|
class ParseError < StandardError
|
|
14
|
+
# Regex that matches the "line N[, ]column M" fragment inside an
|
|
15
|
+
# adapter error message. The separator between line and column is
|
|
16
|
+
# permissive — Oj emits ``"line 1, column 3"`` while the json gem
|
|
17
|
+
# emits ``"line 1 column 2"`` — so ``[,\s]+`` covers both. Column
|
|
18
|
+
# is optional so messages like ``"at line 5"`` still yield a line.
|
|
19
|
+
LOCATION_PATTERN = /line\s+(\d+)(?:[,\s]+column\s+(\d+))?/i
|
|
20
|
+
private_constant :LOCATION_PATTERN
|
|
21
|
+
|
|
8
22
|
# The input string that failed to parse
|
|
9
23
|
#
|
|
10
24
|
# @api public
|
|
@@ -13,6 +27,24 @@ module MultiJson
|
|
|
13
27
|
# error.data #=> "{invalid json}"
|
|
14
28
|
attr_reader :data
|
|
15
29
|
|
|
30
|
+
# The 1-based line number reported by the adapter
|
|
31
|
+
#
|
|
32
|
+
# @api public
|
|
33
|
+
# @return [Integer, nil] line number, or nil if the adapter's message
|
|
34
|
+
# did not include one
|
|
35
|
+
# @example
|
|
36
|
+
# error.line #=> 1
|
|
37
|
+
attr_reader :line
|
|
38
|
+
|
|
39
|
+
# The 1-based column reported by the adapter
|
|
40
|
+
#
|
|
41
|
+
# @api public
|
|
42
|
+
# @return [Integer, nil] column number, or nil if the adapter's message
|
|
43
|
+
# did not include one
|
|
44
|
+
# @example
|
|
45
|
+
# error.column #=> 3
|
|
46
|
+
attr_reader :column
|
|
47
|
+
|
|
16
48
|
# Create a new ParseError
|
|
17
49
|
#
|
|
18
50
|
# @api public
|
|
@@ -21,10 +53,13 @@ module MultiJson
|
|
|
21
53
|
# @param cause [Exception, nil] the original exception
|
|
22
54
|
# @return [ParseError] new error instance
|
|
23
55
|
# @example
|
|
24
|
-
# ParseError.new("unexpected token", data: "{
|
|
56
|
+
# ParseError.new("unexpected token at line 1 column 2", data: "{}")
|
|
25
57
|
def initialize(message = nil, data: nil, cause: nil)
|
|
26
58
|
super(message)
|
|
27
59
|
@data = data
|
|
60
|
+
match = location_match(message)
|
|
61
|
+
@line = match && Integer(match[1])
|
|
62
|
+
@column = match && match[2] && Integer(match[2])
|
|
28
63
|
set_backtrace(cause.backtrace) if cause
|
|
29
64
|
end
|
|
30
65
|
|
|
@@ -39,6 +74,28 @@ module MultiJson
|
|
|
39
74
|
def self.build(original_exception, data)
|
|
40
75
|
new(original_exception.message, data: data, cause: original_exception)
|
|
41
76
|
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Match an adapter error message against the line/column pattern
|
|
81
|
+
#
|
|
82
|
+
# Adapter error messages sometimes embed bytes from the failing
|
|
83
|
+
# input (e.g., the json gem's ``"invalid byte sequence in UTF-8"``
|
|
84
|
+
# error). The pattern is pure ASCII so it's compatible with any
|
|
85
|
+
# encoding, but a UTF-8 string with invalid bytes still trips the
|
|
86
|
+
# regex engine — ``String#scrub`` replaces those bytes so the
|
|
87
|
+
# match can proceed. Strings in binary (ASCII-8BIT) or any valid
|
|
88
|
+
# encoding pass through scrub untouched.
|
|
89
|
+
#
|
|
90
|
+
# @api private
|
|
91
|
+
# @param message [String, nil] the adapter's error message
|
|
92
|
+
# @return [MatchData, nil] the regex match, or nil if no message or
|
|
93
|
+
# no location fragment was found
|
|
94
|
+
def location_match(message)
|
|
95
|
+
return unless message
|
|
96
|
+
|
|
97
|
+
LOCATION_PATTERN.match(message.scrub)
|
|
98
|
+
end
|
|
42
99
|
end
|
|
43
100
|
|
|
44
101
|
# Legacy aliases for backward compatibility
|
data/lib/multi_json/version.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MultiJson
|
|
2
4
|
# Version information for MultiJson
|
|
3
5
|
#
|
|
@@ -6,9 +8,9 @@ module MultiJson
|
|
|
6
8
|
# Major version number
|
|
7
9
|
MAJOR = 1 unless defined? MultiJson::Version::MAJOR
|
|
8
10
|
# Minor version number
|
|
9
|
-
MINOR =
|
|
11
|
+
MINOR = 20 unless defined? MultiJson::Version::MINOR
|
|
10
12
|
# Patch version number
|
|
11
|
-
PATCH =
|
|
13
|
+
PATCH = 0 unless defined? MultiJson::Version::PATCH
|
|
12
14
|
# Pre-release version suffix
|
|
13
15
|
PRE = nil unless defined? MultiJson::Version::PRE
|
|
14
16
|
|