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,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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.
|
|
12
|
+
module Adapters
|
|
13
|
+
# Use the Oj library to dump/load.
|
|
14
|
+
class Oj < Adapter
|
|
15
|
+
include OjCommon
|
|
16
|
+
|
|
17
|
+
defaults :load, mode: :strict, symbolize_keys: 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.
|
|
27
|
+
class ParseError < ::SyntaxError
|
|
28
|
+
WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].freeze
|
|
29
|
+
private_constant :WRAPPED_CLASSES
|
|
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
|
|
39
|
+
def self.===(exception)
|
|
40
|
+
exception.class.ancestors.any? { |ancestor| WRAPPED_CLASSES.include?(ancestor.to_s) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
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"}
|
|
53
|
+
def load(string, options = {})
|
|
54
|
+
::Oj.load(string, translate_load_options(options))
|
|
55
|
+
end
|
|
56
|
+
|
|
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_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
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yajl"
|
|
4
|
+
require_relative "../adapter"
|
|
5
|
+
|
|
6
|
+
module MultiJson
|
|
7
|
+
module Adapters
|
|
8
|
+
# Use the Yajl-Ruby library to dump/load.
|
|
9
|
+
class Yajl < Adapter
|
|
10
|
+
# Exception raised when JSON parsing fails
|
|
11
|
+
ParseError = ::Yajl::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
|
+
::Yajl::Parser.new(options).parse(string)
|
|
24
|
+
end
|
|
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"}'
|
|
35
|
+
def dump(object, options = {})
|
|
36
|
+
::Yajl::Encoder.encode(object, options)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
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
|
|
@@ -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
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJson
|
|
4
|
+
# Mixin providing configurable load/dump options
|
|
5
|
+
#
|
|
6
|
+
# Supports static hashes or dynamic callables (procs/lambdas).
|
|
7
|
+
# Extended by both MultiJson (global options) and Adapter classes.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Options
|
|
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
|
|
17
|
+
|
|
18
|
+
# Set options for load operations
|
|
19
|
+
#
|
|
20
|
+
# @api public
|
|
21
|
+
# @param options [Hash, Proc] options hash or callable
|
|
22
|
+
# @return [Hash, Proc] the options
|
|
23
|
+
# @example
|
|
24
|
+
# MultiJson.load_options = {symbolize_keys: true}
|
|
25
|
+
def load_options=(options)
|
|
26
|
+
OptionsCache.reset
|
|
27
|
+
@load_options = options
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Set options for dump operations
|
|
31
|
+
#
|
|
32
|
+
# @api public
|
|
33
|
+
# @param options [Hash, Proc] options hash or callable
|
|
34
|
+
# @return [Hash, Proc] the options
|
|
35
|
+
# @example
|
|
36
|
+
# MultiJson.dump_options = {pretty: true}
|
|
37
|
+
def dump_options=(options)
|
|
38
|
+
OptionsCache.reset
|
|
39
|
+
@dump_options = options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get options for load operations
|
|
43
|
+
#
|
|
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
|
|
51
|
+
# @return [Hash] resolved options hash
|
|
52
|
+
# @example
|
|
53
|
+
# MultiJson.load_options #=> {}
|
|
54
|
+
def load_options(*args)
|
|
55
|
+
resolve_options(@load_options, *args) || default_load_options
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get options for dump operations
|
|
59
|
+
#
|
|
60
|
+
# @api public
|
|
61
|
+
# @param args [Array<Object>] forwarded to the callable, ignored otherwise
|
|
62
|
+
# @return [Hash] resolved options hash
|
|
63
|
+
# @example
|
|
64
|
+
# MultiJson.dump_options #=> {}
|
|
65
|
+
def dump_options(*args)
|
|
66
|
+
resolve_options(@dump_options, *args) || default_dump_options
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get default load options
|
|
70
|
+
#
|
|
71
|
+
# @api private
|
|
72
|
+
# @return [Hash] frozen empty hash
|
|
73
|
+
def default_load_options
|
|
74
|
+
Concurrency.synchronize(:default_options) { @default_load_options ||= EMPTY_OPTIONS }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get default dump options
|
|
78
|
+
#
|
|
79
|
+
# @api private
|
|
80
|
+
# @return [Hash] frozen empty hash
|
|
81
|
+
def default_dump_options
|
|
82
|
+
Concurrency.synchronize(:default_options) { @default_dump_options ||= EMPTY_OPTIONS }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Resolves options from a hash or callable
|
|
88
|
+
#
|
|
89
|
+
# @api private
|
|
90
|
+
# @param options [Hash, Proc, nil] options configuration
|
|
91
|
+
# @param args [Array<Object>] arguments forwarded to a callable provider
|
|
92
|
+
# @return [Hash, nil] resolved options hash
|
|
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
|
|
98
|
+
|
|
99
|
+
options.to_hash if options.respond_to?(:to_hash)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Invokes a callable options provider
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
# @param callable [Proc] options provider
|
|
106
|
+
# @param args [Array<Object>] arguments forwarded when the callable is non-arity-zero
|
|
107
|
+
# @return [Hash] options returned by the callable
|
|
108
|
+
def invoke_callable(callable, *args)
|
|
109
|
+
callable.arity.zero? ? callable.call : callable.call(*args)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/map"
|
|
4
|
+
|
|
5
|
+
module MultiJson
|
|
6
|
+
module OptionsCache
|
|
7
|
+
# Thread-safe cache store backed by Concurrent::Map
|
|
8
|
+
#
|
|
9
|
+
# Used on JRuby (via the java-platform gem's concurrent-ruby runtime
|
|
10
|
+
# dependency). JRuby has true parallelism, so the plain Hash + Mutex
|
|
11
|
+
# backend on MRI/TruffleRuby is replaced here with Concurrent::Map,
|
|
12
|
+
# which provides lock-free reads and atomic compute_if_absent without
|
|
13
|
+
# needing to serialize the entire fetch path.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
class Store
|
|
17
|
+
# Create a new cache store
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
# @return [Store] new store instance
|
|
21
|
+
def initialize
|
|
22
|
+
@cache = Concurrent::Map.new
|
|
23
|
+
@eviction_mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Clear all cached entries
|
|
27
|
+
#
|
|
28
|
+
# Held under {@eviction_mutex} so a concurrent JRuby thread inside
|
|
29
|
+
# the {fetch} miss path cannot interleave its evict-then-insert
|
|
30
|
+
# sequence with a clear and leave the cache in a partially
|
|
31
|
+
# populated state. Mirrors {MutexStore#reset}'s mutex usage.
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
# @return [void]
|
|
35
|
+
def reset
|
|
36
|
+
@eviction_mutex.synchronize { @cache.clear }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Fetch a value from cache or compute it
|
|
40
|
+
#
|
|
41
|
+
# When called with a block, returns the cached value or computes a
|
|
42
|
+
# new one. When called without a block, returns the cached value or
|
|
43
|
+
# the supplied default if the key is missing.
|
|
44
|
+
#
|
|
45
|
+
# The fast path (cache hit) is lock-free. The miss path takes a small
|
|
46
|
+
# mutex around the evict-then-insert sequence so concurrent inserts
|
|
47
|
+
# cannot both pass the size check and exceed ``max_cache_size``.
|
|
48
|
+
#
|
|
49
|
+
# @api private
|
|
50
|
+
# @param key [Object] cache key
|
|
51
|
+
# @param default [Object] value to return when key is missing and no
|
|
52
|
+
# block is given
|
|
53
|
+
# @yield block to compute value if not cached
|
|
54
|
+
# @return [Object] cached, computed, or default value
|
|
55
|
+
def fetch(key, default = nil, &block)
|
|
56
|
+
return @cache[key] || default unless block
|
|
57
|
+
|
|
58
|
+
cached = @cache[key]
|
|
59
|
+
return cached if cached
|
|
60
|
+
|
|
61
|
+
@eviction_mutex.synchronize do
|
|
62
|
+
evict_one_if_full
|
|
63
|
+
@cache.compute_if_absent(key, &block)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Drop a single arbitrary entry when the cache is at capacity
|
|
70
|
+
#
|
|
71
|
+
# Concurrent::Map has no built-in size cap. We approximate LRU by
|
|
72
|
+
# evicting whichever key Map#keys surfaces first; deterministic
|
|
73
|
+
# ordering is not required, only memory bounding. Iteration must
|
|
74
|
+
# happen outside ``compute_if_absent`` because that block holds the
|
|
75
|
+
# internal cache mutex.
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @return [void]
|
|
79
|
+
def evict_one_if_full
|
|
80
|
+
return if @cache.size < OptionsCache.max_cache_size
|
|
81
|
+
|
|
82
|
+
@cache.delete(@cache.keys.first)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiJson
|
|
4
|
+
# Thread-safe bounded cache for merged options hashes
|
|
5
|
+
#
|
|
6
|
+
# Caches are separated for load and dump operations. Each cache is
|
|
7
|
+
# bounded to prevent unbounded memory growth when options are
|
|
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.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module OptionsCache
|
|
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
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Get the dump options cache
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
# @return [Store] dump cache store
|
|
25
|
+
attr_reader :dump
|
|
26
|
+
|
|
27
|
+
# Get the load options cache
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
# @return [Store] load cache store
|
|
31
|
+
attr_reader :load
|
|
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
|
+
|
|
46
|
+
# Reset both caches
|
|
47
|
+
#
|
|
48
|
+
# @api private
|
|
49
|
+
# @return [void]
|
|
50
|
+
def reset
|
|
51
|
+
@dump = Store.new
|
|
52
|
+
@load = Store.new
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
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
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
require_relative "options_cache/#{MultiJson::OptionsCache.send(:const_get, :BACKENDS).fetch(RUBY_ENGINE, "mutex_store")}"
|
|
72
|
+
MultiJson::OptionsCache.reset
|