multi_json 1.18.0 → 1.19.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.
@@ -1,13 +1,33 @@
1
1
  module MultiJson
2
+ # Raised when an adapter cannot be loaded or is not recognized
3
+ #
4
+ # @api public
2
5
  class AdapterError < ArgumentError
6
+ # Create a new AdapterError
7
+ #
8
+ # @api public
9
+ # @param message [String, nil] error message
10
+ # @param cause [Exception, nil] the original exception
11
+ # @return [AdapterError] new error instance
12
+ # @example
13
+ # AdapterError.new("Unknown adapter", cause: original_error)
3
14
  def initialize(message = nil, cause: nil)
4
15
  super(message)
5
16
  set_backtrace(cause.backtrace) if cause
6
17
  end
7
18
 
19
+ # Build an AdapterError from an original exception
20
+ #
21
+ # @api public
22
+ # @param original_exception [Exception] the original load error
23
+ # @return [AdapterError] new error with formatted message
24
+ # @example
25
+ # AdapterError.build(LoadError.new("cannot load such file"))
8
26
  def self.build(original_exception)
9
- message = "Did not recognize your adapter specification (#{original_exception.message})."
10
- new(message, cause: original_exception)
27
+ new(
28
+ "Did not recognize your adapter specification (#{original_exception.message}).",
29
+ cause: original_exception
30
+ )
11
31
  end
12
32
  end
13
33
  end
@@ -0,0 +1,142 @@
1
+ module MultiJson
2
+ # Handles adapter discovery, loading, and selection
3
+ #
4
+ # Adapters can be specified as:
5
+ # - Symbol/String: adapter name (e.g., :oj, "json_gem")
6
+ # - Module: adapter class directly
7
+ # - nil/false: use default adapter
8
+ #
9
+ # @api private
10
+ module AdapterSelector
11
+ extend self
12
+
13
+ # Alternate spellings for adapter names
14
+ ALIASES = {"jrjackson" => "jr_jackson"}.freeze
15
+
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
24
+
25
+ # Returns the default adapter to use
26
+ #
27
+ # @api private
28
+ # @return [Symbol] adapter name
29
+ # @example
30
+ # AdapterSelector.default_adapter #=> :oj
31
+ def default_adapter
32
+ @default_adapter ||= detect_best_adapter
33
+ end
34
+
35
+ private
36
+
37
+ # Detects the best available JSON adapter
38
+ #
39
+ # @api private
40
+ # @return [Symbol] adapter name
41
+ def detect_best_adapter
42
+ loaded_adapter || installable_adapter || fallback_adapter
43
+ end
44
+
45
+ # Finds an already-loaded JSON library
46
+ #
47
+ # @api private
48
+ # @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)
57
+ end
58
+
59
+ # Tries to require and use an installable adapter
60
+ #
61
+ # @api private
62
+ # @return [Symbol, nil] adapter name if successfully required
63
+ def installable_adapter
64
+ ::MultiJson::REQUIREMENT_MAP.each_key do |adapter_name|
65
+ return adapter_name if try_require(adapter_name)
66
+ end
67
+ nil
68
+ end
69
+
70
+ # Attempts to require a JSON library
71
+ #
72
+ # @api private
73
+ # @param adapter_name [Symbol] adapter to require
74
+ # @return [Boolean] true if require succeeded
75
+ def try_require(adapter_name)
76
+ require ::MultiJson::REQUIREMENT_MAP.fetch(adapter_name)
77
+ true
78
+ rescue ::LoadError
79
+ false
80
+ end
81
+
82
+ # Returns the fallback adapter when no others available
83
+ #
84
+ # @api private
85
+ # @return [Symbol] the ok_json adapter name
86
+ def fallback_adapter
87
+ warn_about_fallback unless @default_adapter_warning_shown
88
+ @default_adapter_warning_shown = true
89
+ :ok_json
90
+ end
91
+
92
+ # Warns the user about using the slow fallback adapter
93
+ #
94
+ # @api private
95
+ # @return [void]
96
+ def warn_about_fallback
97
+ Kernel.warn(
98
+ "[WARNING] MultiJson is using the default adapter (ok_json). " \
99
+ "We recommend loading a different JSON library to improve performance."
100
+ )
101
+ end
102
+
103
+ # Loads an adapter from a specification
104
+ #
105
+ # @api private
106
+ # @param adapter_spec [Symbol, String, Module, nil] adapter specification
107
+ # @return [Class] the adapter class
108
+ 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
113
+ rescue ::LoadError => e
114
+ raise AdapterError.build(e)
115
+ end
116
+
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
+ # Loads an adapter by its string name
130
+ #
131
+ # @api private
132
+ # @param name [String] adapter name
133
+ # @return [Class] the adapter class
134
+ def load_adapter_by_name(name)
135
+ normalized = ALIASES.fetch(name, name).downcase
136
+ require_relative "adapters/#{normalized}"
137
+
138
+ class_name = normalized.split("_").map(&:capitalize).join
139
+ ::MultiJson::Adapters.const_get(class_name)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,44 @@
1
+ require "fast_jsonparser"
2
+ require "oj"
3
+ require_relative "../adapter"
4
+ require_relative "oj_common"
5
+
6
+ module MultiJson
7
+ module Adapters
8
+ # Use the FastJsonparser library to load and Oj to dump.
9
+ 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
14
+
15
+ ParseError = ::FastJsonparser::ParseError
16
+
17
+ # Parse a JSON string into a Ruby object
18
+ #
19
+ # @api private
20
+ # @param string [String] JSON string to parse
21
+ # @param options [Hash] parsing options
22
+ # @return [Object] parsed Ruby object
23
+ #
24
+ # @example Parse JSON string
25
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
26
+ 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))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -7,10 +7,28 @@ module MultiJson
7
7
  class Gson < Adapter
8
8
  ParseError = ::Gson::DecodeError
9
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"}
10
19
  def load(string, options = {})
11
20
  ::Gson::Decoder.new(options).decode(string)
12
21
  end
13
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"}'
14
32
  def dump(object, options = {})
15
33
  ::Gson::Encoder.new(options).encode(object)
16
34
  end
@@ -7,15 +7,42 @@ module MultiJson
7
7
  class JrJackson < Adapter
8
8
  ParseError = ::JrJackson::ParseError
9
9
 
10
- def load(string, options = {}) # :nodoc:
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 = {})
11
20
  ::JrJackson::Json.load(string, options)
12
21
  end
13
22
 
14
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"}'
15
33
  def dump(object, _)
16
34
  ::JrJackson::Json.dump(object)
17
35
  end
18
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"}'
19
46
  def dump(object, options = {})
20
47
  ::JrJackson::Json.dump(object, options)
21
48
  end
@@ -17,6 +17,15 @@ module MultiJson
17
17
  }.freeze
18
18
  private_constant :PRETTY_STATE_PROTOTYPE
19
19
 
20
+ # Parse a JSON string into a Ruby object
21
+ #
22
+ # @api private
23
+ # @param string [String] JSON string to parse
24
+ # @param options [Hash] parsing options
25
+ # @return [Object] parsed Ruby object
26
+ #
27
+ # @example Parse JSON string
28
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
20
29
  def load(string, options = {})
21
30
  string = string.dup.force_encoding(Encoding::UTF_8) if string.encoding != Encoding::UTF_8
22
31
 
@@ -24,15 +33,26 @@ module MultiJson
24
33
  ::JSON.parse(string, options)
25
34
  end
26
35
 
36
+ # Serialize a Ruby object to JSON
37
+ #
38
+ # @api private
39
+ # @param object [Object] object to serialize
40
+ # @param options [Hash] serialization options
41
+ # @return [String] JSON string
42
+ #
43
+ # @example Serialize object to JSON
44
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
27
45
  def dump(object, options = {})
28
- opts = options.dup
46
+ opts = options.except(:adapter)
47
+ json_object = object.respond_to?(:as_json) ? object.as_json : object
48
+ return ::JSON.dump(json_object) if opts.empty?
29
49
 
30
50
  if opts.delete(:pretty)
31
51
  opts = PRETTY_STATE_PROTOTYPE.merge(opts)
32
- return ::JSON.pretty_generate(object, opts)
52
+ return ::JSON.pretty_generate(json_object, opts)
33
53
  end
34
54
 
35
- ::JSON.generate(object, opts)
55
+ ::JSON.generate(json_object, opts)
36
56
  end
37
57
  end
38
58
  end
@@ -1,10 +1,13 @@
1
1
  require "oj"
2
2
  require_relative "../adapter"
3
+ require_relative "oj_common"
3
4
 
4
5
  module MultiJson
5
6
  module Adapters
6
7
  # Use the Oj library to dump/load.
7
8
  class Oj < Adapter
9
+ include OjCommon
10
+
8
11
  defaults :load, mode: :strict, symbolize_keys: false
9
12
  defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
10
13
 
@@ -19,44 +22,44 @@ module MultiJson
19
22
  WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].freeze
20
23
  private_constant :WRAPPED_CLASSES
21
24
 
25
+ # Case equality for exception matching in rescue clauses
26
+ #
27
+ # @api private
28
+ # @param exception [Exception] exception to check
29
+ # @return [Boolean] true if exception is a parse error
30
+ #
31
+ # @example Match parse errors in rescue
32
+ # rescue ParseError => e
22
33
  def self.===(exception)
23
34
  exception.is_a?(::SyntaxError) || WRAPPED_CLASSES.include?(exception.class.to_s)
24
35
  end
25
36
  end
26
37
 
38
+ # Parse a JSON string into a Ruby object
39
+ #
40
+ # @api private
41
+ # @param string [String] JSON string to parse
42
+ # @param options [Hash] parsing options
43
+ # @return [Object] parsed Ruby object
44
+ #
45
+ # @example Parse JSON string
46
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
27
47
  def load(string, options = {})
28
48
  options[:symbol_keys] = options[:symbolize_keys]
29
49
  ::Oj.load(string, options)
30
50
  end
31
51
 
32
- OJ_VERSION = ::Oj::VERSION
33
- OJ_V2 = OJ_VERSION.start_with?("2.")
34
- OJ_V3 = OJ_VERSION.start_with?("3.")
35
- private_constant :OJ_VERSION, :OJ_V2, :OJ_V3
36
-
37
- if OJ_V3
38
- PRETTY_STATE_PROTOTYPE = {
39
- indent: " ",
40
- space: " ",
41
- space_before: "",
42
- object_nl: "\n",
43
- array_nl: "\n",
44
- ascii_only: false
45
- }.freeze
46
- private_constant :PRETTY_STATE_PROTOTYPE
47
- end
48
-
52
+ # Serialize a Ruby object to JSON
53
+ #
54
+ # @api private
55
+ # @param object [Object] object to serialize
56
+ # @param options [Hash] serialization options
57
+ # @return [String] JSON string
58
+ #
59
+ # @example Serialize object to JSON
60
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
49
61
  def dump(object, options = {})
50
- if OJ_V2
51
- options[:indent] = 2 if options[:pretty]
52
- options[:indent] = options[:indent].to_i if options[:indent]
53
- elsif OJ_V3
54
- options.merge!(PRETTY_STATE_PROTOTYPE.dup) if options.delete(:pretty)
55
- else
56
- raise "Unsupported Oj version: #{::Oj::VERSION}"
57
- end
58
-
59
- ::Oj.dump(object, options)
62
+ ::Oj.dump(object, prepare_dump_options(options))
60
63
  end
61
64
  end
62
65
  end
@@ -0,0 +1,47 @@
1
+ require "rubygems/version"
2
+
3
+ module MultiJson
4
+ module Adapters
5
+ 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
22
+
23
+ private
24
+
25
+ # Prepare options for Oj.dump based on Oj version
26
+ #
27
+ # @api private
28
+ # @param options [Hash] serialization options
29
+ # @return [Hash] processed options for Oj.dump
30
+ #
31
+ # @example Prepare dump options
32
+ # prepare_dump_options(pretty: true)
33
+ 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
42
+
43
+ options
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,11 +4,21 @@ require_relative "../vendor/okjson"
4
4
 
5
5
  module MultiJson
6
6
  module Adapters
7
+ # Use the vendored OkJson library to dump/load.
7
8
  class OkJson < Adapter
8
9
  include ConvertibleHashKeys
9
10
 
10
11
  ParseError = ::MultiJson::OkJson::Error
11
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"}
12
22
  def load(string, options = {})
13
23
  result = ::MultiJson::OkJson.decode("[#{string}]").first
14
24
  options[:symbolize_keys] ? symbolize_keys(result) : result
@@ -16,6 +26,15 @@ module MultiJson
16
26
  raise ParseError
17
27
  end
18
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"}'
19
38
  def dump(object, _ = {})
20
39
  ::MultiJson::OkJson.valenc(stringify_keys(object))
21
40
  end
@@ -7,10 +7,28 @@ module MultiJson
7
7
  class Yajl < Adapter
8
8
  ParseError = ::Yajl::ParseError
9
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"}
10
19
  def load(string, options = {})
11
20
  ::Yajl::Parser.new(symbolize_keys: options[:symbolize_keys]).parse(string)
12
21
  end
13
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"}'
14
32
  def dump(object, options = {})
15
33
  ::Yajl::Encoder.encode(object, options)
16
34
  end
@@ -1,49 +1,64 @@
1
1
  module MultiJson
2
+ # Mixin for converting hash keys between symbols and strings
3
+ #
4
+ # @api private
2
5
  module ConvertibleHashKeys
3
6
  SIMPLE_OBJECT_CLASSES = [String, Numeric, TrueClass, FalseClass, NilClass].freeze
4
7
  private_constant :SIMPLE_OBJECT_CLASSES
5
8
 
6
9
  private
7
10
 
8
- def symbolize_keys(hash)
9
- prepare_hash(hash) do |key|
10
- key.respond_to?(:to_sym) ? key.to_sym : key
11
- end
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 }
12
18
  end
13
19
 
14
- def stringify_keys(hash)
15
- prepare_hash(hash) do |key|
16
- key.respond_to?(:to_s) ? key.to_s : key
17
- end
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 }
18
27
  end
19
28
 
20
- def prepare_hash(value, &block)
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)
21
36
  case value
22
- when Array
23
- handle_array(value, &block)
24
37
  when Hash
25
- handle_hash(value, &block)
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) }
26
41
  else
27
- handle_simple_objects(value)
42
+ convert_simple_object(value)
28
43
  end
29
44
  end
30
45
 
31
- def handle_simple_objects(obj)
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)
32
52
  return obj if simple_object?(obj) || obj.respond_to?(:to_json)
33
53
 
34
54
  obj.respond_to?(:to_s) ? obj.to_s : obj
35
55
  end
36
56
 
37
- def handle_array(array, &key_modifier)
38
- array.map { |value| prepare_hash(value, &key_modifier) }
39
- end
40
-
41
- def handle_hash(original_hash, &key_modifier)
42
- original_hash.each_with_object({}) do |(key, value), result|
43
- result[key_modifier.call(key)] = prepare_hash(value, &key_modifier)
44
- end
45
- end
46
-
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
47
62
  def simple_object?(obj)
48
63
  SIMPLE_OBJECT_CLASSES.any? { |klass| obj.is_a?(klass) }
49
64
  end