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.
data/lib/multi_json.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "multi_json/concurrency"
1
4
  require_relative "multi_json/options"
2
5
  require_relative "multi_json/version"
3
6
  require_relative "multi_json/adapter_error"
@@ -10,6 +13,26 @@ require_relative "multi_json/adapter_selector"
10
13
  # MultiJson allows swapping between JSON backends without changing your code.
11
14
  # It auto-detects available JSON libraries and uses the fastest one available.
12
15
  #
16
+ # ## Method-definition patterns
17
+ #
18
+ # The current public API uses two patterns, each chosen for a specific reason:
19
+ #
20
+ # 1. ``module_function`` creates both a class method and a private instance
21
+ # method from a single ``def``. This is used for the hot-path API
22
+ # (``adapter``, ``use``, ``adapter=``, ``load``, ``dump``,
23
+ # ``current_adapter``) so that both ``MultiJson.load(...)`` and legacy
24
+ # ``Class.new { include MultiJson }.new.send(:load, ...)`` invocations
25
+ # work through the same body. The instance versions are re-publicized
26
+ # below so YARD renders them as part of the public API.
27
+ # 2. ``def self.foo`` creates only a singleton method, giving mutation
28
+ # testing a single canonical definition to target. This is used for
29
+ # {.with_adapter}, which needs precise mutation coverage of its
30
+ # fiber-local save/restore logic.
31
+ #
32
+ # Deprecated public API (``decode``, ``encode``, ``engine``, etc.) lives in
33
+ # {file:lib/multi_json/deprecated.rb} so this file stays focused on the
34
+ # current surface.
35
+ #
13
36
  # @example Basic usage
14
37
  # MultiJson.load('{"foo":"bar"}') #=> {"foo" => "bar"}
15
38
  # MultiJson.dump({foo: "bar"}) #=> '{"foo":"bar"}'
@@ -23,169 +46,165 @@ module MultiJson
23
46
  extend Options
24
47
  extend AdapterSelector
25
48
 
26
- # @!visibility private
27
- module_function
28
-
29
- # @!group Configuration
49
+ # Tracks which deprecation warnings have already been emitted so each one
50
+ # fires at most once per process. Stored as a Set rather than a Hash so
51
+ # presence checks have unambiguous semantics for mutation tests.
52
+ DEPRECATION_WARNINGS_SHOWN = Set.new
53
+ private_constant :DEPRECATION_WARNINGS_SHOWN
30
54
 
31
- # Set default options for both load and dump operations
32
- #
33
- # @api private
34
- # @deprecated Use {.load_options=} and {.dump_options=} instead
35
- # @param value [Hash] options hash
36
- # @return [Hash] the options hash
37
- # @example
38
- # MultiJson.default_options = {symbolize_keys: true}
39
- def default_options=(value)
40
- Kernel.warn "MultiJson.default_options setter is deprecated\n" \
41
- "Use MultiJson.load_options and MultiJson.dump_options instead"
42
- self.load_options = self.dump_options = value
43
- end
55
+ class << self
56
+ private
44
57
 
45
- # Get the default options
46
- #
47
- # @api private
48
- # @deprecated Use {.load_options} or {.dump_options} instead
49
- # @return [Hash] the current load options
50
- # @example
51
- # MultiJson.default_options #=> {}
52
- def default_options
53
- Kernel.warn "MultiJson.default_options is deprecated\n" \
54
- "Use MultiJson.load_options or MultiJson.dump_options instead"
55
- load_options
56
- end
58
+ # Emit a deprecation warning at most once per process for the given key
59
+ #
60
+ # Defined as a singleton method (rather than via module_function) so
61
+ # there is exactly one definition for mutation tests to target. The
62
+ # deprecated method bodies invoke this via ``warn_deprecation_once(...)``
63
+ # (singleton callers) and via the private instance delegates routing
64
+ # through the singleton for legacy ``include MultiJson`` consumers.
65
+ #
66
+ # @api private
67
+ # @param key [Symbol] identifier for the deprecation (typically the method name)
68
+ # @param message [String] warning message to emit on first call
69
+ # @return [void]
70
+ # @example
71
+ # MultiJson.send(:warn_deprecation_once, :foo, "MultiJson.foo is deprecated")
72
+ def warn_deprecation_once(key, message)
73
+ Concurrency.synchronize(:deprecation_warnings) do
74
+ return if DEPRECATION_WARNINGS_SHOWN.include?(key)
57
75
 
58
- # @deprecated These methods are no longer used
59
- %w[cached_options reset_cached_options!].each do |method_name|
60
- define_method(method_name) do |*|
61
- Kernel.warn "MultiJson.#{method_name} method is deprecated and no longer used."
76
+ Kernel.warn(message)
77
+ DEPRECATION_WARNINGS_SHOWN.add(key)
78
+ end
62
79
  end
63
80
  end
64
81
 
65
- # Legacy alias for adapter name mappings
66
- ALIASES = AdapterSelector::ALIASES
67
-
68
- # Maps adapter symbols to their require paths for auto-loading
69
- REQUIREMENT_MAP = {
70
- fast_jsonparser: "fast_jsonparser",
71
- oj: "oj",
72
- yajl: "yajl",
73
- jr_jackson: "jrjackson",
74
- json_gem: "json",
75
- gson: "gson"
76
- }.freeze
82
+ # Resolve the ``ParseError`` constant for an adapter class
83
+ #
84
+ # The result is memoized on the adapter class itself in a
85
+ # ``@_multi_json_parse_error`` ivar so subsequent ``MultiJson.load``
86
+ # calls skip the constant lookup entirely. The lookup is performed
87
+ # with ``inherit: false`` so a stray top-level ``::ParseError``
88
+ # constant in the host process is correctly ignored on every
89
+ # supported Ruby implementation — TruffleRuby's ``::`` operator
90
+ # walks the ancestor chain and would otherwise pick up the top-level
91
+ # constant. Custom adapters that don't define their own
92
+ # ``ParseError`` get a clear {AdapterError} instead of the bare
93
+ # ``NameError`` Ruby would raise from the rescue clause.
94
+ #
95
+ # @api private
96
+ # @param adapter_class [Class] adapter class to inspect
97
+ # @return [Class] the adapter's ParseError class
98
+ # @raise [AdapterError] when the adapter doesn't define ParseError
99
+ def self.parse_error_class_for(adapter_class)
100
+ cached = adapter_class.instance_variable_get(:@_multi_json_parse_error)
101
+ return cached if cached
77
102
 
78
- class << self
79
- # Returns the default adapter name (alias for default_adapter)
80
- #
81
- # @api public
82
- # @deprecated Use {.default_adapter} instead
83
- # @return [Symbol] the default adapter name
84
- # @example
85
- # MultiJson.default_engine #=> :oj
86
- alias_method :default_engine, :default_adapter
103
+ resolved = adapter_class.const_get(:ParseError, false)
104
+ adapter_class.instance_variable_set(:@_multi_json_parse_error, resolved)
105
+ rescue NameError
106
+ raise AdapterError, "Adapter #{adapter_class} must define a ParseError constant"
87
107
  end
88
108
 
89
- # @!endgroup
109
+ # ===========================================================================
110
+ # Public API (module_function: class + private instance method)
111
+ # ===========================================================================
90
112
 
91
- # @!group Adapter Management
113
+ # @!visibility private
114
+ module_function
92
115
 
93
116
  # Returns the current adapter class
94
117
  #
95
- # @api private
118
+ # Honors a fiber-local override set by {.with_adapter} so concurrent
119
+ # blocks observe their own adapter without clobbering the process-wide
120
+ # default. Falls back to the process default when no override is set.
121
+ #
122
+ # @api public
96
123
  # @return [Class] the current adapter class
97
124
  # @example
98
125
  # MultiJson.adapter #=> MultiJson::Adapters::Oj
99
126
  def adapter
127
+ override = Fiber[:multi_json_adapter]
128
+ return override if override
129
+
100
130
  @adapter ||= use(nil)
101
131
  end
102
132
 
103
- # Returns the current adapter class (alias for adapter)
104
- #
105
- # @api private
106
- # @deprecated Use {.adapter} instead
107
- # @return [Class] the current adapter class
108
- # @example
109
- # MultiJson.engine #=> MultiJson::Adapters::Oj
110
- alias_method :engine, :adapter
111
-
112
133
  # Sets the adapter to use for JSON operations
113
134
  #
114
- # @api private
135
+ # The merged-options cache is only reset when the new adapter loads
136
+ # successfully. A failed ``use(:nonexistent)`` leaves the cache in
137
+ # place so the previously-active adapter keeps its cached entries.
138
+ #
139
+ # @api public
115
140
  # @param new_adapter [Symbol, String, Module, nil] adapter specification
116
141
  # @return [Class] the loaded adapter class
117
142
  # @example
118
143
  # MultiJson.use(:oj)
119
144
  def use(new_adapter)
120
- @adapter = load_adapter(new_adapter)
121
- ensure
122
- OptionsCache.reset
145
+ loaded = load_adapter(new_adapter)
146
+ Concurrency.synchronize(:adapter) do
147
+ OptionsCache.reset
148
+ @adapter = loaded
149
+ end
123
150
  end
124
151
 
125
152
  # Sets the adapter to use for JSON operations
126
153
  #
127
- # @api private
154
+ # @api public
128
155
  # @return [Class] the loaded adapter class
129
156
  # @example
130
157
  # MultiJson.adapter = :json_gem
131
158
  alias_method :adapter=, :use
132
-
133
- # Sets the adapter to use for JSON operations
134
- #
135
- # @api private
136
- # @deprecated Use {.adapter=} instead
137
- # @return [Class] the loaded adapter class
138
- # @example
139
- # MultiJson.engine = :json_gem
140
- alias_method :engine=, :use
141
- module_function :adapter=, :engine=
142
-
143
- # @!endgroup
144
-
145
- # @!group JSON Operations
159
+ module_function :adapter=
146
160
 
147
161
  # Parses a JSON string into a Ruby object
148
162
  #
149
- # @api private
163
+ # Returns ``nil`` for ``nil``, empty, and whitespace-only inputs
164
+ # instead of raising. Pass an explicit non-blank string if you want
165
+ # to surface a {ParseError} for empty payloads at the call site.
166
+ #
167
+ # @api public
150
168
  # @param string [String, #read] JSON string or IO-like object
151
169
  # @param options [Hash] parsing options (adapter-specific)
152
- # @return [Object] parsed Ruby object
170
+ # @return [Object, nil] parsed Ruby object, or nil for blank input
153
171
  # @raise [ParseError] if parsing fails
172
+ # @raise [AdapterError] if the adapter doesn't define a ``ParseError`` constant
154
173
  # @example
155
174
  # MultiJson.load('{"foo":"bar"}') #=> {"foo" => "bar"}
175
+ # MultiJson.load("") #=> nil
176
+ # MultiJson.load(" \n") #=> nil
156
177
  def load(string, options = {})
157
178
  adapter_class = current_adapter(options)
158
- adapter_class.load(string, options)
159
- rescue adapter_class::ParseError => e
160
- raise ParseError.build(e, string)
179
+ parse_error_class = MultiJson.parse_error_class_for(adapter_class)
180
+ begin
181
+ adapter_class.load(string, options)
182
+ rescue parse_error_class => e
183
+ raise ParseError.build(e, string)
184
+ end
161
185
  end
162
186
 
163
- # Parses a JSON string into a Ruby object
164
- #
165
- # @api private
166
- # @deprecated Use {.load} instead
167
- # @return [Object] parsed Ruby object
168
- # @example
169
- # MultiJson.decode('{"foo":"bar"}') #=> {"foo" => "bar"}
170
- alias_method :decode, :load
171
- module_function :decode
172
-
173
187
  # Returns the adapter to use for the given options
174
188
  #
175
- # @api private
176
- # @param options [Hash] options that may contain :adapter key
189
+ # ``nil`` is accepted as a no-options sentinel — explicit
190
+ # ``current_adapter(nil)`` calls fall through to the process default
191
+ # adapter without raising.
192
+ #
193
+ # @api public
194
+ # @param options [Hash, nil] options that may contain :adapter key, or
195
+ # nil to use the process default
177
196
  # @return [Class] adapter class
178
197
  # @example
179
198
  # MultiJson.current_adapter(adapter: :oj) #=> MultiJson::Adapters::Oj
180
199
  def current_adapter(options = {})
181
- options ||= {}
200
+ options ||= Options::EMPTY_OPTIONS
182
201
  adapter_override = options[:adapter]
183
202
  adapter_override ? load_adapter(adapter_override) : adapter
184
203
  end
185
204
 
186
205
  # Serializes a Ruby object to a JSON string
187
206
  #
188
- # @api private
207
+ # @api public
189
208
  # @param object [Object] object to serialize
190
209
  # @param options [Hash] serialization options (adapter-specific)
191
210
  # @return [String] JSON string
@@ -195,41 +214,55 @@ module MultiJson
195
214
  current_adapter(options).dump(object, options)
196
215
  end
197
216
 
198
- # Serializes a Ruby object to a JSON string
199
- #
200
- # @api private
201
- # @deprecated Use {.dump} instead
202
- # @return [String] JSON string
203
- # @example
204
- # MultiJson.encode({foo: "bar"}) #=> '{"foo":"bar"}'
205
- alias_method :encode, :dump
206
- module_function :encode
217
+ # Re-publicize the instance versions of the module_function methods so
218
+ # YARD/yardstick render them as part of the public API and legacy
219
+ # ``include MultiJson`` consumers can call them without ``.send``.
220
+ public :adapter, :use, :adapter=, :load, :current_adapter, :dump
221
+
222
+ # ===========================================================================
223
+ # Public API (def self.foo: singleton-only, for mutation-test precision)
224
+ # ===========================================================================
207
225
 
208
226
  # Executes a block using the specified adapter
209
227
  #
210
- # @api private
228
+ # Defined as a singleton method so mutation testing has exactly one
229
+ # definition to target. The override is stored in fiber-local storage
230
+ # so concurrent fibers and threads each see their own adapter without
231
+ # racing on a shared module variable; nested calls save and restore
232
+ # the previous fiber-local value.
233
+ #
234
+ # @api public
211
235
  # @param new_adapter [Symbol, String, Module] adapter to use
212
236
  # @yield block to execute with the temporary adapter
213
237
  # @return [Object] result of the block
214
238
  # @example
215
239
  # MultiJson.with_adapter(:json_gem) { MultiJson.dump({}) }
216
- def with_adapter(new_adapter)
217
- previous_adapter = adapter
218
- self.adapter = new_adapter
240
+ def self.with_adapter(new_adapter)
241
+ previous_override = Fiber[:multi_json_adapter]
242
+ Fiber[:multi_json_adapter] = load_adapter(new_adapter)
219
243
  yield
220
244
  ensure
221
- self.adapter = previous_adapter
245
+ Fiber[:multi_json_adapter] = previous_override
222
246
  end
223
247
 
224
- # Executes a block using the specified adapter
248
+ # ===========================================================================
249
+ # Private instance-method delegates for the singleton-only methods above
250
+ # ===========================================================================
251
+
252
+ private
253
+
254
+ # Instance-method delegate for {MultiJson.with_adapter}
225
255
  #
226
256
  # @api private
227
- # @deprecated Use {.with_adapter} instead
257
+ # @param new_adapter [Symbol, String, Module] adapter to use
258
+ # @yield block to execute with the temporary adapter
228
259
  # @return [Object] result of the block
229
260
  # @example
230
- # MultiJson.with_engine(:json_gem) { MultiJson.dump({}) }
231
- alias_method :with_engine, :with_adapter
232
- module_function :with_engine
233
-
234
- # @!endgroup
261
+ # class Foo; include MultiJson; end
262
+ # Foo.new.send(:with_adapter, :json_gem) { ... }
263
+ def with_adapter(new_adapter, &)
264
+ MultiJson.with_adapter(new_adapter, &)
265
+ end
235
266
  end
267
+
268
+ require_relative "multi_json/deprecated"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.1
4
+ version: 1.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Bleigh
@@ -13,7 +13,7 @@ cert_chain: []
13
13
  date: 1980-01-02 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: A common interface to multiple JSON libraries, including fast_jsonparser,
16
- Oj, Yajl, the JSON gem (with C-extensions), gson, JrJackson, and OkJson.
16
+ Oj, Yajl, and the JSON gem.
17
17
  email:
18
18
  - sferik@gmail.com
19
19
  executables: []
@@ -29,28 +29,26 @@ files:
29
29
  - lib/multi_json/adapter_error.rb
30
30
  - lib/multi_json/adapter_selector.rb
31
31
  - lib/multi_json/adapters/fast_jsonparser.rb
32
- - lib/multi_json/adapters/gson.rb
33
- - lib/multi_json/adapters/jr_jackson.rb
34
32
  - lib/multi_json/adapters/json_gem.rb
35
33
  - lib/multi_json/adapters/oj.rb
36
34
  - lib/multi_json/adapters/oj_common.rb
37
- - lib/multi_json/adapters/ok_json.rb
38
35
  - lib/multi_json/adapters/yajl.rb
39
- - lib/multi_json/convertible_hash_keys.rb
36
+ - lib/multi_json/concurrency.rb
37
+ - lib/multi_json/deprecated.rb
40
38
  - lib/multi_json/options.rb
41
39
  - lib/multi_json/options_cache.rb
40
+ - lib/multi_json/options_cache/mutex_store.rb
42
41
  - lib/multi_json/parse_error.rb
43
- - lib/multi_json/vendor/okjson.rb
44
42
  - lib/multi_json/version.rb
45
43
  homepage: https://github.com/sferik/multi_json
46
44
  licenses:
47
45
  - MIT
48
46
  metadata:
49
47
  bug_tracker_uri: https://github.com/sferik/multi_json/issues
50
- changelog_uri: https://github.com/sferik/multi_json/blob/v1.19.1/CHANGELOG.md
51
- documentation_uri: https://www.rubydoc.info/gems/multi_json/1.19.1
48
+ changelog_uri: https://github.com/sferik/multi_json/blob/v1.20.0/CHANGELOG.md
49
+ documentation_uri: https://www.rubydoc.info/gems/multi_json/1.20.0
52
50
  rubygems_mfa_required: 'true'
53
- source_code_uri: https://github.com/sferik/multi_json/tree/v1.19.1
51
+ source_code_uri: https://github.com/sferik/multi_json/tree/v1.20.0
54
52
  wiki_uri: https://github.com/sferik/multi_json/wiki
55
53
  rdoc_options: []
56
54
  require_paths:
@@ -59,14 +57,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
59
57
  requirements:
60
58
  - - ">="
61
59
  - !ruby/object:Gem::Version
62
- version: '3.0'
60
+ version: '3.2'
63
61
  required_rubygems_version: !ruby/object:Gem::Requirement
64
62
  requirements:
65
63
  - - ">="
66
64
  - !ruby/object:Gem::Version
67
65
  version: '0'
68
66
  requirements: []
69
- rubygems_version: 4.0.3
67
+ rubygems_version: 4.0.10
70
68
  specification_version: 4
71
69
  summary: A common interface to multiple JSON libraries.
72
70
  test_files: []
@@ -1,37 +0,0 @@
1
- require "gson"
2
- require_relative "../adapter"
3
-
4
- module MultiJson
5
- module Adapters
6
- # Use the gson.rb library to dump/load.
7
- class Gson < Adapter
8
- ParseError = ::Gson::DecodeError
9
-
10
- # Parse a JSON string into a Ruby object
11
- #
12
- # @api private
13
- # @param string [String] JSON string to parse
14
- # @param options [Hash] parsing options
15
- # @return [Object] parsed Ruby object
16
- #
17
- # @example Parse JSON string
18
- # adapter.load('{"key":"value"}') #=> {"key" => "value"}
19
- def load(string, options = {})
20
- ::Gson::Decoder.new(options).decode(string)
21
- end
22
-
23
- # Serialize a Ruby object to JSON
24
- #
25
- # @api private
26
- # @param object [Object] object to serialize
27
- # @param options [Hash] serialization options
28
- # @return [String] JSON string
29
- #
30
- # @example Serialize object to JSON
31
- # adapter.dump({key: "value"}) #=> '{"key":"value"}'
32
- def dump(object, options = {})
33
- ::Gson::Encoder.new(options).encode(object)
34
- end
35
- end
36
- end
37
- end
@@ -1,52 +0,0 @@
1
- require "jrjackson" unless defined?(JrJackson)
2
- require_relative "../adapter"
3
-
4
- module MultiJson
5
- module Adapters
6
- # Use the jrjackson.rb library to dump/load.
7
- class JrJackson < Adapter
8
- ParseError = ::JrJackson::ParseError
9
-
10
- # Parse a JSON string into a Ruby object
11
- #
12
- # @api private
13
- # @param string [String] JSON string to parse
14
- # @param options [Hash] parsing options
15
- # @return [Object] parsed Ruby object
16
- #
17
- # @example Parse JSON string
18
- # adapter.load('{"key":"value"}') #=> {"key" => "value"}
19
- def load(string, options = {})
20
- ::JrJackson::Json.load(string, options)
21
- end
22
-
23
- if ::JrJackson::Json.method(:dump).arity == 1
24
- # Serialize a Ruby object to JSON
25
- #
26
- # @api private
27
- # @param object [Object] object to serialize
28
- # @param _ [Hash] serialization options (unused in this version)
29
- # @return [String] JSON string
30
- #
31
- # @example Serialize object to JSON
32
- # adapter.dump({key: "value"}) #=> '{"key":"value"}'
33
- def dump(object, _)
34
- ::JrJackson::Json.dump(object)
35
- end
36
- else
37
- # Serialize a Ruby object to JSON
38
- #
39
- # @api private
40
- # @param object [Object] object to serialize
41
- # @param options [Hash] serialization options
42
- # @return [String] JSON string
43
- #
44
- # @example Serialize object to JSON
45
- # adapter.dump({key: "value"}) #=> '{"key":"value"}'
46
- def dump(object, options = {})
47
- ::JrJackson::Json.dump(object, options)
48
- end
49
- end
50
- end
51
- end
52
- end
@@ -1,43 +0,0 @@
1
- require_relative "../adapter"
2
- require_relative "../convertible_hash_keys"
3
- require_relative "../vendor/okjson"
4
-
5
- module MultiJson
6
- module Adapters
7
- # Use the vendored OkJson library to dump/load.
8
- class OkJson < Adapter
9
- include ConvertibleHashKeys
10
-
11
- ParseError = ::MultiJson::OkJson::Error
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
- result = ::MultiJson::OkJson.decode("[#{string}]").first
24
- options[:symbolize_keys] ? symbolize_keys(result) : result
25
- rescue ArgumentError # invalid byte sequence in UTF-8
26
- raise ParseError
27
- end
28
-
29
- # Serialize a Ruby object to JSON
30
- #
31
- # @api private
32
- # @param object [Object] object to serialize
33
- # @param _ [Hash] serialization options (unused)
34
- # @return [String] JSON string
35
- #
36
- # @example Serialize object to JSON
37
- # adapter.dump({key: "value"}) #=> '{"key":"value"}'
38
- def dump(object, _ = {})
39
- ::MultiJson::OkJson.valenc(stringify_keys(object))
40
- end
41
- end
42
- end
43
- end
@@ -1,66 +0,0 @@
1
- module MultiJson
2
- # Mixin for converting hash keys between symbols and strings
3
- #
4
- # @api private
5
- module ConvertibleHashKeys
6
- SIMPLE_OBJECT_CLASSES = [String, Numeric, TrueClass, FalseClass, NilClass].freeze
7
- private_constant :SIMPLE_OBJECT_CLASSES
8
-
9
- private
10
-
11
- # Converts hash keys to symbols recursively
12
- #
13
- # @api private
14
- # @param value [Object] value to convert
15
- # @return [Object] value with symbolized keys
16
- def symbolize_keys(value)
17
- convert_hash_keys(value) { |key| key.respond_to?(:to_sym) ? key.to_sym : key }
18
- end
19
-
20
- # Converts hash keys to strings recursively
21
- #
22
- # @api private
23
- # @param value [Object] value to convert
24
- # @return [Object] value with stringified keys
25
- def stringify_keys(value)
26
- convert_hash_keys(value) { |key| key.respond_to?(:to_s) ? key.to_s : key }
27
- end
28
-
29
- # Recursively converts hash keys using the given block
30
- #
31
- # @api private
32
- # @param value [Object] value to convert
33
- # @yield [key] block to transform each key
34
- # @return [Object] converted value
35
- def convert_hash_keys(value, &key_modifier)
36
- case value
37
- when Hash
38
- value.to_h { |k, v| [key_modifier.call(k), convert_hash_keys(v, &key_modifier)] }
39
- when Array
40
- value.map { |v| convert_hash_keys(v, &key_modifier) }
41
- else
42
- convert_simple_object(value)
43
- end
44
- end
45
-
46
- # Converts non-hash objects to a JSON-safe format
47
- #
48
- # @api private
49
- # @param obj [Object] object to convert
50
- # @return [Object] converted object
51
- def convert_simple_object(obj)
52
- return obj if simple_object?(obj) || obj.respond_to?(:to_json)
53
-
54
- obj.respond_to?(:to_s) ? obj.to_s : obj
55
- end
56
-
57
- # Checks if an object is a simple JSON-safe type
58
- #
59
- # @api private
60
- # @param obj [Object] object to check
61
- # @return [Boolean] true if object is a simple type
62
- def simple_object?(obj)
63
- SIMPLE_OBJECT_CLASSES.any? { |klass| obj.is_a?(klass) }
64
- end
65
- end
66
- end