multi_json 1.19.1 → 1.21.1

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.
@@ -1,4 +1,6 @@
1
- module MultiJson
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
2
4
  # Handles adapter discovery, loading, and selection
3
5
  #
4
6
  # Adapters can be specified as:
@@ -10,17 +12,45 @@ module MultiJson
10
12
  module AdapterSelector
11
13
  extend self
12
14
 
13
- # Alternate spellings for adapter names
14
- ALIASES = {"jrjackson" => "jr_jackson"}.freeze
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
+ #
21
+ # The hash order is split per platform: on MRI/TruffleRuby the
22
+ # bundled benchmark suite ranks json_gem ahead of fast_jsonparser/
23
+ # oj/yajl on Ruby 3.4+; on JRuby the FFI-vs-pure-Ruby tradeoff
24
+ # hasn't been re-benchmarked yet, so jr_jackson stays first there.
25
+ # CI re-runs the benchmark with ``--verify-preference`` to fail
26
+ # if the observed ranking diverges.
27
+ # :nocov:
28
+ ADAPTERS = if RUBY_ENGINE == "jruby"
29
+ {
30
+ jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
31
+ json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
32
+ gson: {require: "gson", loaded: "Gson"},
33
+ fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
34
+ oj: {require: "oj", loaded: "Oj"},
35
+ yajl: {require: "yajl", loaded: "Yajl"}
36
+ }.freeze
37
+ else
38
+ {
39
+ json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
40
+ fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
41
+ oj: {require: "oj", loaded: "Oj"},
42
+ yajl: {require: "yajl", loaded: "Yajl"},
43
+ jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
44
+ gson: {require: "gson", loaded: "Gson"}
45
+ }.freeze
46
+ end
47
+ # :nocov:
48
+ private_constant :ADAPTERS
15
49
 
16
- # Strategy lambdas for loading adapters based on specification type
17
- LOADERS = {
18
- Module => ->(adapter, _selector) { adapter },
19
- String => ->(name, selector) { selector.send(:load_adapter_by_name, name) },
20
- Symbol => ->(name, selector) { selector.send(:load_adapter_by_name, name.to_s) },
21
- NilClass => ->(_adapter, selector) { selector.send(:load_adapter, selector.default_adapter) },
22
- FalseClass => ->(_adapter, selector) { selector.send(:load_adapter, selector.default_adapter) }
23
- }.freeze
50
+ # Backwards-compatible view of {ADAPTERS} that exposes only the
51
+ # require paths. Tests still poke at this constant to stub or break
52
+ # the require step.
53
+ REQUIREMENT_MAP = ADAPTERS.transform_values { |meta| meta[:require] }.freeze
24
54
 
25
55
  # Returns the default adapter to use
26
56
  #
@@ -29,7 +59,27 @@ module MultiJson
29
59
  # @example
30
60
  # AdapterSelector.default_adapter #=> :oj
31
61
  def default_adapter
32
- @default_adapter ||= detect_best_adapter
62
+ Concurrency.synchronize(:default_adapter) { @default_adapter ||= detect_best_adapter }
63
+ end
64
+
65
+ # Returns the default adapter class, excluding the given adapter name
66
+ #
67
+ # Used by adapters that only implement one direction (e.g.
68
+ # FastJsonparser only parses) so the other direction can be delegated
69
+ # to whichever library MultiJSON would otherwise pick.
70
+ #
71
+ # @api private
72
+ # @param excluded [Symbol] adapter name to skip during detection
73
+ # @return [Class] the adapter class
74
+ # @example
75
+ # AdapterSelector.default_adapter_excluding(:fast_jsonparser) #=> MultiJSON::Adapters::Oj
76
+ def default_adapter_excluding(excluded)
77
+ Concurrency.synchronize(:default_adapter) do
78
+ name = loaded_adapter(excluding: excluded)
79
+ name ||= installable_adapter(excluding: excluded)
80
+ name ||= fallback_adapter
81
+ load_adapter_by_name(name.to_s)
82
+ end
33
83
  end
34
84
 
35
85
  private
@@ -45,23 +95,24 @@ module MultiJson
45
95
  # Finds an already-loaded JSON library
46
96
  #
47
97
  # @api private
98
+ # @param excluding [Symbol, nil] adapter name to skip during detection
48
99
  # @return [Symbol, nil] adapter name if found
49
- def loaded_adapter
50
- return :fast_jsonparser if defined?(::FastJsonparser)
51
- return :oj if defined?(::Oj)
52
- return :yajl if defined?(::Yajl)
53
- return :jr_jackson if defined?(::JrJackson)
54
- return :json_gem if defined?(::JSON::Ext::Parser)
55
-
56
- :gson if defined?(::Gson)
100
+ def loaded_adapter(excluding: nil)
101
+ ADAPTERS.each do |name, meta|
102
+ next if name == excluding
103
+ return name if Object.const_defined?(meta.fetch(:loaded))
104
+ end
105
+ nil
57
106
  end
58
107
 
59
108
  # Tries to require and use an installable adapter
60
109
  #
61
110
  # @api private
111
+ # @param excluding [Symbol, nil] adapter name to skip during detection
62
112
  # @return [Symbol, nil] adapter name if successfully required
63
- def installable_adapter
64
- ::MultiJson::REQUIREMENT_MAP.each_key do |adapter_name|
113
+ def installable_adapter(excluding: nil)
114
+ REQUIREMENT_MAP.each_key do |adapter_name|
115
+ next if adapter_name == excluding
65
116
  return adapter_name if try_require(adapter_name)
66
117
  end
67
118
  nil
@@ -73,7 +124,7 @@ module MultiJson
73
124
  # @param adapter_name [Symbol] adapter to require
74
125
  # @return [Boolean] true if require succeeded
75
126
  def try_require(adapter_name)
76
- require ::MultiJson::REQUIREMENT_MAP.fetch(adapter_name)
127
+ require REQUIREMENT_MAP.fetch(adapter_name)
77
128
  true
78
129
  rescue ::LoadError
79
130
  false
@@ -81,62 +132,83 @@ module MultiJson
81
132
 
82
133
  # Returns the fallback adapter when no others available
83
134
  #
135
+ # The json gem is a Ruby default gem since Ruby 1.9, so in practice
136
+ # the installable-adapter step always succeeds before reaching this
137
+ # fallback on any supported Ruby version. The warning below only
138
+ # fires in tests that deliberately break the require path.
139
+ #
84
140
  # @api private
85
- # @return [Symbol] the ok_json adapter name
141
+ # @return [Symbol] the json_gem adapter name
86
142
  def fallback_adapter
87
143
  warn_about_fallback unless @default_adapter_warning_shown
88
144
  @default_adapter_warning_shown = true
89
- :ok_json
145
+ :json_gem
90
146
  end
91
147
 
92
- # Warns the user about using the slow fallback adapter
148
+ # Warns the user about reaching the last-resort fallback
93
149
  #
94
150
  # @api private
95
151
  # @return [void]
96
152
  def warn_about_fallback
97
153
  Kernel.warn(
98
- "[WARNING] MultiJson is using the default adapter (ok_json). " \
99
- "We recommend loading a different JSON library to improve performance."
154
+ "[WARNING] MultiJSON is falling back to the json_gem adapter " \
155
+ "because no other JSON library could be loaded."
100
156
  )
101
157
  end
102
158
 
103
159
  # Loads an adapter from a specification
104
160
  #
105
161
  # @api private
106
- # @param adapter_spec [Symbol, String, Module, nil] adapter specification
162
+ # @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
107
163
  # @return [Class] the adapter class
108
164
  def load_adapter(adapter_spec)
109
- loader = find_loader_for(adapter_spec)
110
- return loader.call(adapter_spec, self) if loader
111
-
112
- raise ::LoadError, adapter_spec
165
+ adapter = case adapter_spec
166
+ when ::String then load_adapter_by_name(adapter_spec)
167
+ when ::Symbol then load_adapter_by_name(adapter_spec.to_s)
168
+ when nil, false then load_adapter(default_adapter)
169
+ when ::Module then adapter_spec
170
+ else raise ::LoadError, "expected adapter to be a Symbol, String, or Module, got #{adapter_spec.inspect}"
171
+ end
172
+ validate_adapter!(adapter)
113
173
  rescue ::LoadError => e
114
174
  raise AdapterError.build(e)
115
175
  end
116
176
 
117
- # Finds the appropriate loader for an adapter specification
118
- #
119
- # @api private
120
- # @param adapter_spec [Object] adapter specification
121
- # @return [Proc, nil] loader proc if found
122
- def find_loader_for(adapter_spec)
123
- klass = adapter_spec.class
124
- return LOADERS.fetch(klass) if LOADERS.key?(klass)
125
-
126
- LOADERS.fetch(Module) if adapter_spec.is_a?(Module)
127
- end
128
-
129
177
  # Loads an adapter by its string name
130
178
  #
179
+ # ``jrjackson`` (the JrJackson gem's name) is normalized to
180
+ # ``jr_jackson`` (the adapter file/class name) for backwards
181
+ # compatibility with the original gem-name alias.
182
+ #
131
183
  # @api private
132
184
  # @param name [String] adapter name
133
185
  # @return [Class] the adapter class
134
186
  def load_adapter_by_name(name)
135
- normalized = ALIASES.fetch(name, name).downcase
187
+ normalized = name.downcase
188
+ normalized = "jr_jackson" if normalized == "jrjackson"
136
189
  require_relative "adapters/#{normalized}"
137
190
 
138
191
  class_name = normalized.split("_").map(&:capitalize).join
139
- ::MultiJson::Adapters.const_get(class_name)
192
+ ::MultiJSON::Adapters.const_get(class_name)
193
+ end
194
+
195
+ # Validate that an adapter satisfies the documented contract
196
+ #
197
+ # Custom adapters are accepted as modules/classes, so fail fast
198
+ # during adapter resolution rather than later on the first load or
199
+ # dump call.
200
+ #
201
+ # @api private
202
+ # @param adapter [Module] adapter class or module
203
+ # @return [Module] the validated adapter
204
+ # @raise [AdapterError] when the adapter is missing a required class method
205
+ # or ParseError constant
206
+ def validate_adapter!(adapter)
207
+ raise AdapterError, "Adapter #{adapter} must respond to .load" unless adapter.respond_to?(:load)
208
+ raise AdapterError, "Adapter #{adapter} must respond to .dump" unless adapter.respond_to?(:dump)
209
+
210
+ MultiJSON.parse_error_class_for(adapter)
211
+ adapter
140
212
  end
141
213
  end
142
214
  end
@@ -1,43 +1,73 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "fast_jsonparser"
2
- require "oj"
3
4
  require_relative "../adapter"
4
- require_relative "oj_common"
5
+ require_relative "../adapter_selector"
5
6
 
6
- module MultiJson
7
+ module MultiJSON
7
8
  module Adapters
8
- # Use the FastJsonparser library to load and Oj to dump.
9
+ # Use the FastJsonparser library to load, and the fastest other
10
+ # available adapter to dump.
11
+ #
12
+ # FastJsonparser only implements parsing, so the ``dump`` side of
13
+ # the adapter is delegated to whichever adapter MultiJSON would
14
+ # pick if FastJsonparser weren't installed (oj → yajl → jr_jackson
15
+ # → json_gem → gson). The delegate is resolved lazily at the first
16
+ # ``dump`` call, not at file load time, so load order doesn't lock
17
+ # in the wrong delegate. Require any preferred dump backend before
18
+ # the first ``dump`` call (typical applications already have ``oj``
19
+ # loaded by then).
9
20
  class FastJsonparser < Adapter
10
- include OjCommon
11
-
12
- defaults :load, symbolize_keys: false
13
- defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
21
+ defaults :load, symbolize_names: false
14
22
 
23
+ # Exception raised when JSON parsing fails
15
24
  ParseError = ::FastJsonparser::ParseError
16
25
 
26
+ class << self
27
+ # Serialize a Ruby object to JSON via the lazy delegate
28
+ #
29
+ # @api private
30
+ # @param object [Object] object to serialize
31
+ # @param options [Hash] serialization options
32
+ # @return [String] JSON string
33
+ # @example
34
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
35
+ def dump(object, options = {})
36
+ dump_delegate.dump(object, options)
37
+ end
38
+
39
+ private
40
+
41
+ # Resolve the dump delegate, caching it across calls
42
+ #
43
+ # @api private
44
+ # @return [Class] delegate adapter class
45
+ def dump_delegate
46
+ MultiJSON::Concurrency.synchronize(:dump_delegate) do
47
+ @dump_delegate ||= MultiJSON::AdapterSelector.default_adapter_excluding(:fast_jsonparser)
48
+ end
49
+ end
50
+ end
51
+
17
52
  # Parse a JSON string into a Ruby object
18
53
  #
54
+ # FastJsonparser.parse only accepts ``symbolize_keys`` and raises
55
+ # on unknown keyword arguments, so the adapter explicitly forwards
56
+ # MultiJSON's canonical ``:symbolize_names`` option as
57
+ # FastJsonparser's native ``symbolize_keys:`` kwarg and silently
58
+ # drops the rest. Pass other options through
59
+ # ``MultiJSON.parse_options=`` and they'll apply to whichever
60
+ # adapter MultiJSON selects when fast_jsonparser isn't installed.
61
+ #
19
62
  # @api private
20
63
  # @param string [String] JSON string to parse
21
- # @param options [Hash] parsing options
64
+ # @param options [Hash] parsing options (only :symbolize_names is honored)
22
65
  # @return [Object] parsed Ruby object
23
66
  #
24
67
  # @example Parse JSON string
25
68
  # adapter.load('{"key":"value"}') #=> {"key" => "value"}
26
69
  def load(string, options = {})
27
- ::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_keys])
28
- end
29
-
30
- # Serialize a Ruby object to JSON
31
- #
32
- # @api private
33
- # @param object [Object] object to serialize
34
- # @param options [Hash] serialization options
35
- # @return [String] JSON string
36
- #
37
- # @example Serialize object to JSON
38
- # adapter.dump({key: "value"}) #=> '{"key":"value"}'
39
- def dump(object, options = {})
40
- ::Oj.dump(object, prepare_dump_options(options))
70
+ ::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_names])
41
71
  end
42
72
  end
43
73
  end
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../adapter"
2
4
  require "json"
3
5
 
4
- module MultiJson
6
+ module MultiJSON
5
7
  module Adapters
6
8
  # Use the JSON gem to dump/load.
7
9
  class JsonGem < Adapter
10
+ # Exception raised when JSON parsing fails
8
11
  ParseError = ::JSON::ParserError
9
12
 
10
13
  defaults :load, create_additions: false, quirks_mode: true
@@ -19,17 +22,28 @@ module MultiJson
19
22
 
20
23
  # Parse a JSON string into a Ruby object
21
24
  #
25
+ # Non-UTF-8 strings are re-labeled via ``force_encoding`` (not
26
+ # transcoded) and then validated. This handles the dominant
27
+ # real-world case: Ruby HTTP libraries return response bodies
28
+ # tagged as ``ASCII-8BIT`` even when the bytes are valid UTF-8.
29
+ # ``encode(Encoding::UTF_8)`` would raise on any multi-byte
30
+ # sequence in that scenario because it tries to transcode each
31
+ # byte individually from ASCII-8BIT to UTF-8.
32
+ #
22
33
  # @api private
23
34
  # @param string [String] JSON string to parse
24
35
  # @param options [Hash] parsing options
25
36
  # @return [Object] parsed Ruby object
37
+ # @raise [::JSON::ParserError] when the input is not valid UTF-8
26
38
  #
27
39
  # @example Parse JSON string
28
40
  # adapter.load('{"key":"value"}') #=> {"key" => "value"}
29
41
  def load(string, options = {})
30
- string = string.dup.force_encoding(Encoding::UTF_8) if string.encoding != Encoding::UTF_8
42
+ if string.encoding != Encoding::UTF_8
43
+ string = string.dup.force_encoding(Encoding::UTF_8)
44
+ raise ::JSON::ParserError, "Invalid UTF-8 byte sequence in JSON input" unless string.valid_encoding?
45
+ end
31
46
 
32
- options[:symbolize_names] = true if options.delete(:symbolize_keys)
33
47
  ::JSON.parse(string, options)
34
48
  end
35
49
 
@@ -43,16 +57,15 @@ module MultiJson
43
57
  # @example Serialize object to JSON
44
58
  # adapter.dump({key: "value"}) #=> '{"key":"value"}'
45
59
  def dump(object, options = {})
46
- opts = options.except(:adapter)
47
60
  json_object = object.respond_to?(:as_json) ? object.as_json : object
48
- return ::JSON.dump(json_object) if opts.empty?
61
+ return ::JSON.dump(json_object) if options.empty?
62
+ return ::JSON.generate(json_object, options) unless options.key?(:pretty)
49
63
 
50
- if opts.delete(:pretty)
51
- opts = PRETTY_STATE_PROTOTYPE.merge(opts)
52
- return ::JSON.pretty_generate(json_object, opts)
53
- end
64
+ # Common case: ``pretty: true`` is the only option, so the merge
65
+ # would just produce a copy of PRETTY_STATE_PROTOTYPE.
66
+ return ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE) if options.size == 1
54
67
 
55
- ::JSON.generate(json_object, opts)
68
+ ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE.merge(options.except(:pretty)))
56
69
  end
57
70
  end
58
71
  end
@@ -1,23 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oj"
2
4
  require_relative "../adapter"
3
5
  require_relative "oj_common"
4
6
 
5
- module MultiJson
7
+ module MultiJSON
8
+ # Namespace for JSON adapter implementations
9
+ #
10
+ # Each adapter wraps a specific JSON library and provides a consistent
11
+ # interface for loading and dumping JSON data.
6
12
  module Adapters
7
13
  # Use the Oj library to dump/load.
8
14
  class Oj < Adapter
9
15
  include OjCommon
10
16
 
11
- defaults :load, mode: :strict, symbolize_keys: false
17
+ defaults :load, mode: :strict, symbolize_names: false
12
18
  defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
13
19
 
14
- # In certain cases OJ gem may throw JSON::ParserError exception instead
15
- # of its own class. Also, we can't expect ::JSON::ParserError and
16
- # ::Oj::ParseError to always be defined, since it's often not the case.
17
- # Because of this, we can't reference those classes directly and have to
18
- # do string comparison instead. This will not catch subclasses, but it
19
- # shouldn't be a problem since the library is not known to be using it
20
- # (at least for now).
20
+ # In certain cases the Oj gem may throw a ``JSON::ParserError``
21
+ # exception instead of its own class. Neither ``::JSON::ParserError``
22
+ # nor ``::Oj::ParseError`` is guaranteed to be defined, so we can't
23
+ # reference them directly match by walking the exception's
24
+ # ancestry by class name instead. This will not catch subclasses
25
+ # of those classes, which shouldn't be a problem since neither
26
+ # library is known to subclass them.
21
27
  class ParseError < ::SyntaxError
22
28
  WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].freeze
23
29
  private_constant :WRAPPED_CLASSES
@@ -31,7 +37,7 @@ module MultiJson
31
37
  # @example Match parse errors in rescue
32
38
  # rescue ParseError => e
33
39
  def self.===(exception)
34
- exception.is_a?(::SyntaxError) || WRAPPED_CLASSES.include?(exception.class.to_s)
40
+ exception.class.ancestors.any? { |ancestor| WRAPPED_CLASSES.include?(ancestor.to_s) }
35
41
  end
36
42
  end
37
43
 
@@ -45,8 +51,7 @@ module MultiJson
45
51
  # @example Parse JSON string
46
52
  # adapter.load('{"key":"value"}') #=> {"key" => "value"}
47
53
  def load(string, options = {})
48
- options[:symbol_keys] = options[:symbolize_keys]
49
- ::Oj.load(string, options)
54
+ ::Oj.load(string, translate_load_options(options))
50
55
  end
51
56
 
52
57
  # Serialize a Ruby object to JSON
@@ -61,6 +66,24 @@ module MultiJson
61
66
  def dump(object, options = {})
62
67
  ::Oj.dump(object, prepare_dump_options(options))
63
68
  end
69
+
70
+ private
71
+
72
+ # Translate ``:symbolize_names`` 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_names`` translated
84
+ def translate_load_options(options)
85
+ options.except(:symbolize_names).merge(symbol_keys: options[:symbolize_names] == true)
86
+ end
64
87
  end
65
88
  end
66
89
  end
@@ -1,28 +1,32 @@
1
- require "rubygems/version"
1
+ # frozen_string_literal: true
2
2
 
3
- module MultiJson
3
+ module MultiJSON
4
4
  module Adapters
5
+ # Shared functionality for the Oj adapter
6
+ #
7
+ # Provides option preparation for Oj.dump. Targets Oj 3.x; Oj 2.x is
8
+ # no longer supported.
9
+ #
10
+ # @api private
5
11
  module OjCommon
6
- OJ_VERSION = Gem::Version.new(::Oj::VERSION)
7
- OJ_V2 = OJ_VERSION.segments.first == 2
8
- OJ_V3 = OJ_VERSION.segments.first == 3
9
- private_constant :OJ_VERSION, :OJ_V2, :OJ_V3
10
-
11
- if OJ_V3
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
- end
12
+ PRETTY_STATE_PROTOTYPE = {
13
+ indent: " ",
14
+ space: " ",
15
+ space_before: "",
16
+ object_nl: "\n",
17
+ array_nl: "\n",
18
+ ascii_only: false
19
+ }.freeze
20
+ private_constant :PRETTY_STATE_PROTOTYPE
22
21
 
23
22
  private
24
23
 
25
- # Prepare options for Oj.dump based on Oj version
24
+ # Prepare options for Oj.dump
25
+ #
26
+ # Returns a fresh hash; never mutates the input. The input is the
27
+ # cached options hash returned from Adapter.merged_dump_options, so
28
+ # in-place mutation would pollute the cache and corrupt subsequent
29
+ # dump calls.
26
30
  #
27
31
  # @api private
28
32
  # @param options [Hash] serialization options
@@ -31,16 +35,9 @@ module MultiJson
31
35
  # @example Prepare dump options
32
36
  # prepare_dump_options(pretty: true)
33
37
  def prepare_dump_options(options)
34
- if OJ_V2
35
- options[:indent] = 2 if options[:pretty]
36
- options[:indent] = options[:indent].to_i if options[:indent]
37
- elsif OJ_V3
38
- options.merge!(PRETTY_STATE_PROTOTYPE.dup) if options.delete(:pretty)
39
- else
40
- raise "Unsupported Oj version: #{::Oj::VERSION}"
41
- end
38
+ return options unless options.key?(:pretty)
42
39
 
43
- options
40
+ options.except(:pretty).merge(PRETTY_STATE_PROTOTYPE)
44
41
  end
45
42
  end
46
43
  end
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "yajl"
2
4
  require_relative "../adapter"
3
5
 
4
- module MultiJson
6
+ module MultiJSON
5
7
  module Adapters
6
8
  # Use the Yajl-Ruby library to dump/load.
7
9
  class Yajl < Adapter
10
+ # Exception raised when JSON parsing fails
8
11
  ParseError = ::Yajl::ParseError
9
12
 
10
13
  # Parse a JSON string into a Ruby object
@@ -17,7 +20,7 @@ module MultiJson
17
20
  # @example Parse JSON string
18
21
  # adapter.load('{"key":"value"}') #=> {"key" => "value"}
19
22
  def load(string, options = {})
20
- ::Yajl::Parser.new(symbolize_keys: options[:symbolize_keys]).parse(string)
23
+ ::Yajl::Parser.new(options).parse(string)
21
24
  end
22
25
 
23
26
  # Serialize a Ruby object to JSON
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
4
+ # Catalog of process-wide mutexes used to serialize MultiJSON's lazy
5
+ # initializers and adapter swaps. Each mutex protects a distinct
6
+ # piece of mutable state. Callers go through {.synchronize} rather
7
+ # than touching the mutex constants directly so the constants
8
+ # themselves can stay {.private_constant} and the surface of the
9
+ # module is documented in one place.
10
+ #
11
+ # @api private
12
+ module Concurrency
13
+ # Catalog of mutexes keyed by symbolic name. Each entry maps the
14
+ # public name passed to {.synchronize} to the underlying mutex
15
+ # instance. The names are documented inline so callers can find
16
+ # what each mutex protects without leaving this file.
17
+ MUTEXES = {
18
+ # Guards the {DEPRECATION_WARNINGS_SHOWN} set in `MultiJSON` so the
19
+ # check-then-add pair in `warn_deprecation_once` doesn't race.
20
+ deprecation_warnings: Mutex.new,
21
+ # Guards the process-wide `@adapter` swap in `MultiJSON.use` so two
22
+ # threads can't interleave their `OptionsCache.reset` and adapter
23
+ # assignment.
24
+ adapter: Mutex.new,
25
+ # Guards the lazy `@default_adapter` initializer and the
26
+ # `default_adapter_excluding` detection chain in `AdapterSelector`,
27
+ # so the chain runs at most once and `fallback_adapter`'s one-time
28
+ # warning fires at most once.
29
+ default_adapter: Mutex.new,
30
+ # Guards the lazy `default_load_options` / `default_dump_options`
31
+ # initializers in `MultiJSON::Options`.
32
+ default_options: Mutex.new,
33
+ # Guards the lazy dump-delegate resolution in
34
+ # `MultiJSON::Adapters::FastJsonparser`.
35
+ dump_delegate: Mutex.new
36
+ }.freeze
37
+ private_constant :MUTEXES
38
+
39
+ # Run a block while holding the named mutex
40
+ #
41
+ # The ``name`` symbol must be one of the keys in the internal
42
+ # ``MUTEXES`` table; an unknown name raises ``KeyError`` so a
43
+ # typo at the call site fails fast instead of silently dropping
44
+ # synchronization on the floor.
45
+ #
46
+ # @api private
47
+ # @param name [Symbol] mutex identifier
48
+ # @yield block to execute while holding the mutex
49
+ # @return [Object] the block's return value
50
+ # @raise [KeyError] when ``name`` does not match a known mutex
51
+ # @example
52
+ # MultiJSON::Concurrency.synchronize(:adapter) { ... }
53
+ def self.synchronize(name, &)
54
+ MUTEXES.fetch(name).synchronize(&)
55
+ end
56
+ end
57
+ end