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.
@@ -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