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,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require_relative "options"
5
+
6
+ module MultiJson
7
+ # Base class for JSON adapter implementations
8
+ #
9
+ # Each adapter wraps a specific JSON library (Oj, JSON gem, etc.) and
10
+ # provides a consistent interface. Uses Singleton pattern so each adapter
11
+ # class has exactly one instance.
12
+ #
13
+ # Subclasses must implement:
14
+ # - #load(string, options) -> parsed object
15
+ # - #dump(object, options) -> JSON string
16
+ #
17
+ # @api private
18
+ class Adapter
19
+ extend Options
20
+ include Singleton
21
+
22
+ class << self
23
+ BLANK_PATTERN = /\A\s*\z/
24
+ VALID_DEFAULTS_ACTIONS = %i[load dump].freeze
25
+ private_constant :BLANK_PATTERN, :VALID_DEFAULTS_ACTIONS
26
+
27
+ # Get default load options, walking the superclass chain
28
+ #
29
+ # Returns the closest ancestor's `@default_load_options` ivar so a
30
+ # parent class calling {.defaults} after a subclass has been
31
+ # defined still propagates to the subclass. Falls back to the
32
+ # shared frozen empty hash when no ancestor has defaults set.
33
+ #
34
+ # @api private
35
+ # @return [Hash] frozen options hash
36
+ def default_load_options
37
+ walk_default_options(:@default_load_options)
38
+ end
39
+
40
+ # Get default dump options, walking the superclass chain
41
+ #
42
+ # @api private
43
+ # @return [Hash] frozen options hash
44
+ def default_dump_options
45
+ walk_default_options(:@default_dump_options)
46
+ end
47
+
48
+ # DSL for setting adapter-specific default options
49
+ #
50
+ # ``action`` must be ``:load`` or ``:dump``; ``value`` must be a
51
+ # Hash. Both arguments are validated up front so a typo at the
52
+ # adapter's class definition fails fast instead of producing a
53
+ # silent no-op default that crashes later in the merge path.
54
+ #
55
+ # @api private
56
+ # @param action [Symbol] :load or :dump
57
+ # @param value [Hash] default options for the action
58
+ # @return [Hash] the frozen options hash
59
+ # @raise [ArgumentError] when action is anything other than :load
60
+ # or :dump, or when value isn't a Hash
61
+ # @example Set load defaults for an adapter
62
+ # class MyAdapter < MultiJson::Adapter
63
+ # defaults :load, symbolize_keys: false
64
+ # end
65
+ def defaults(action, value)
66
+ raise ArgumentError, "expected action to be :load or :dump, got #{action.inspect}" unless VALID_DEFAULTS_ACTIONS.include?(action)
67
+ raise ArgumentError, "expected value to be a Hash, got #{value.class}" unless value.is_a?(Hash)
68
+
69
+ instance_variable_set(:"@default_#{action}_options", value.freeze)
70
+ end
71
+
72
+ # Parse a JSON string into a Ruby object
73
+ #
74
+ # @api private
75
+ # @param string [String, #read] JSON string or IO-like object
76
+ # @param options [Hash] parsing options
77
+ # @return [Object, nil] parsed object or nil for blank input
78
+ def load(string, options = {})
79
+ string = string.read if string.respond_to?(:read)
80
+ return nil if blank?(string)
81
+
82
+ instance.load(string, merged_load_options(options))
83
+ end
84
+
85
+ # Serialize a Ruby object to JSON
86
+ #
87
+ # @api private
88
+ # @param object [Object] object to serialize
89
+ # @param options [Hash] serialization options
90
+ # @return [String] JSON string
91
+ def dump(object, options = {})
92
+ instance.dump(object, merged_dump_options(options))
93
+ end
94
+
95
+ private
96
+
97
+ # Walk the superclass chain looking for a default options ivar
98
+ #
99
+ # Stops at the first ancestor whose ``ivar`` is set and returns
100
+ # that value. Returns {Options::EMPTY_OPTIONS} when no ancestor
101
+ # has the ivar set, so adapters without defaults always observe a
102
+ # frozen empty hash instead of nil.
103
+ #
104
+ # @api private
105
+ # @param ivar [Symbol] ivar name (`:@default_load_options` or `:@default_dump_options`)
106
+ # @return [Hash] frozen options hash
107
+ def walk_default_options(ivar)
108
+ # @type var klass: Class?
109
+ klass = self
110
+ while klass
111
+ return klass.instance_variable_get(ivar) if klass.instance_variable_defined?(ivar)
112
+
113
+ klass = klass.superclass
114
+ end
115
+ Options::EMPTY_OPTIONS
116
+ end
117
+
118
+ # Checks if the input is blank (nil, empty, or whitespace-only)
119
+ #
120
+ # The dominant call path arrives with a non-blank string starting
121
+ # with ``{`` or ``[`` (the JSON object/array sigils), so a
122
+ # ``start_with?`` short-circuit skips the regex entirely on the
123
+ # hot path. Falls through to the full check for everything else
124
+ # — strings, numbers, booleans, ``null``, whitespace-prefixed
125
+ # input — at which point ``String#scrub`` is only invoked when
126
+ # the input has invalid encoding so the common valid-UTF-8 path
127
+ # doesn't allocate a scrubbed copy on every call. Scrubbing
128
+ # replaces invalid bytes with U+FFFD before the regex runs so a
129
+ # string with bad bytes is still treated as non-blank without a
130
+ # broad rescue.
131
+ #
132
+ # @api private
133
+ # @param input [String, nil] input to check
134
+ # @return [Boolean] true if input is blank
135
+ def blank?(input)
136
+ return true if input.nil? || input.empty?
137
+ return false if input.start_with?("{", "[")
138
+
139
+ BLANK_PATTERN.match?(input.valid_encoding? ? input : input.scrub)
140
+ end
141
+
142
+ # Merges dump options from adapter, global, and call-site
143
+ #
144
+ # @api private
145
+ # @param options [Hash] call-site options
146
+ # @return [Hash] merged options hash
147
+ def merged_dump_options(options)
148
+ cache_key = strip_adapter_key(options)
149
+ OptionsCache.dump.fetch(cache_key) do
150
+ dump_options(cache_key).merge(MultiJson.dump_options(cache_key)).merge!(cache_key)
151
+ end
152
+ end
153
+
154
+ # Merges load options from adapter, global, and call-site
155
+ #
156
+ # @api private
157
+ # @param options [Hash] call-site options
158
+ # @return [Hash] merged options hash
159
+ def merged_load_options(options)
160
+ cache_key = strip_adapter_key(options)
161
+ OptionsCache.load.fetch(cache_key) do
162
+ load_options(cache_key).merge(MultiJson.load_options(cache_key)).merge!(cache_key)
163
+ end
164
+ end
165
+
166
+ # Removes the :adapter key from options for cache key
167
+ #
168
+ # Returns a shared frozen empty hash for the common no-options call
169
+ # path so the hot path avoids allocating a fresh hash on every call.
170
+ #
171
+ # @api private
172
+ # @param options [Hash, #to_h] original options (may be JSON::State or similar)
173
+ # @return [Hash] frozen options without :adapter key
174
+ def strip_adapter_key(options)
175
+ options = options.to_h unless options.is_a?(Hash)
176
+ return Options::EMPTY_OPTIONS if options.empty? || (options.size == 1 && options.key?(:adapter))
177
+
178
+ options.except(:adapter).freeze
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJson
4
+ # Raised when an adapter cannot be loaded or is not recognized
5
+ #
6
+ # @api public
7
+ class AdapterError < ArgumentError
8
+ # Create a new AdapterError
9
+ #
10
+ # @api public
11
+ # @param message [String, nil] error message
12
+ # @param cause [Exception, nil] the original exception
13
+ # @return [AdapterError] new error instance
14
+ # @example
15
+ # AdapterError.new("Unknown adapter", cause: original_error)
16
+ def initialize(message = nil, cause: nil)
17
+ super(message)
18
+ set_backtrace(cause.backtrace) if cause
19
+ end
20
+
21
+ # Build an AdapterError from an original exception
22
+ #
23
+ # The original exception's class name is included in the message
24
+ # so a downstream consumer reading just the AdapterError can tell
25
+ # whether the underlying failure was a `LoadError`, an
26
+ # `ArgumentError` from the spec validator, or some other class
27
+ # without having to look at `error.cause` separately.
28
+ #
29
+ # @api public
30
+ # @param original_exception [Exception] the original load error
31
+ # @return [AdapterError] new error with formatted message
32
+ # @example
33
+ # AdapterError.build(LoadError.new("cannot load such file"))
34
+ def self.build(original_exception)
35
+ new(
36
+ "Did not recognize your adapter specification " \
37
+ "(#{original_exception.class}: #{original_exception.message}).",
38
+ cause: original_exception
39
+ )
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJson
4
+ # Handles adapter discovery, loading, and selection
5
+ #
6
+ # Adapters can be specified as:
7
+ # - Symbol/String: adapter name (e.g., :oj, "json_gem")
8
+ # - Module: adapter class directly
9
+ # - nil/false: use default adapter
10
+ #
11
+ # @api private
12
+ module AdapterSelector
13
+ extend self
14
+
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
+ ADAPTERS = {
21
+ fast_jsonparser: {require: "fast_jsonparser", loaded: "FastJsonparser"},
22
+ oj: {require: "oj", loaded: "Oj"},
23
+ yajl: {require: "yajl", loaded: "Yajl"},
24
+ jr_jackson: {require: "jrjackson", loaded: "JrJackson"},
25
+ json_gem: {require: "json", loaded: "JSON::Ext::Parser"},
26
+ gson: {require: "gson", loaded: "Gson"}
27
+ }.freeze
28
+ private_constant :ADAPTERS
29
+
30
+ # Backwards-compatible view of {ADAPTERS} that exposes only the
31
+ # require paths. Tests still poke at this constant to stub or break
32
+ # the require step.
33
+ REQUIREMENT_MAP = ADAPTERS.transform_values { |meta| meta[:require] }.freeze
34
+
35
+ # Returns the default adapter to use
36
+ #
37
+ # @api private
38
+ # @return [Symbol] adapter name
39
+ # @example
40
+ # AdapterSelector.default_adapter #=> :oj
41
+ def default_adapter
42
+ Concurrency.synchronize(:default_adapter) { @default_adapter ||= detect_best_adapter }
43
+ end
44
+
45
+ # Returns the default adapter class, excluding the given adapter name
46
+ #
47
+ # Used by adapters that only implement one direction (e.g.
48
+ # FastJsonparser only parses) so the other direction can be delegated
49
+ # to whichever library MultiJson would otherwise pick.
50
+ #
51
+ # @api private
52
+ # @param excluded [Symbol] adapter name to skip during detection
53
+ # @return [Class] the adapter class
54
+ # @example
55
+ # AdapterSelector.default_adapter_excluding(:fast_jsonparser) #=> MultiJson::Adapters::Oj
56
+ def default_adapter_excluding(excluded)
57
+ Concurrency.synchronize(:default_adapter) do
58
+ name = loaded_adapter(excluding: excluded)
59
+ name ||= installable_adapter(excluding: excluded)
60
+ name ||= fallback_adapter
61
+ load_adapter_by_name(name.to_s)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ # Detects the best available JSON adapter
68
+ #
69
+ # @api private
70
+ # @return [Symbol] adapter name
71
+ def detect_best_adapter
72
+ loaded_adapter || installable_adapter || fallback_adapter
73
+ end
74
+
75
+ # Finds an already-loaded JSON library
76
+ #
77
+ # @api private
78
+ # @param excluding [Symbol, nil] adapter name to skip during detection
79
+ # @return [Symbol, nil] adapter name if found
80
+ def loaded_adapter(excluding: nil)
81
+ ADAPTERS.each do |name, meta|
82
+ next if name == excluding
83
+ return name if Object.const_defined?(meta.fetch(:loaded))
84
+ end
85
+ nil
86
+ end
87
+
88
+ # Tries to require and use an installable adapter
89
+ #
90
+ # @api private
91
+ # @param excluding [Symbol, nil] adapter name to skip during detection
92
+ # @return [Symbol, nil] adapter name if successfully required
93
+ def installable_adapter(excluding: nil)
94
+ REQUIREMENT_MAP.each_key do |adapter_name|
95
+ next if adapter_name == excluding
96
+ return adapter_name if try_require(adapter_name)
97
+ end
98
+ nil
99
+ end
100
+
101
+ # Attempts to require a JSON library
102
+ #
103
+ # @api private
104
+ # @param adapter_name [Symbol] adapter to require
105
+ # @return [Boolean] true if require succeeded
106
+ def try_require(adapter_name)
107
+ require REQUIREMENT_MAP.fetch(adapter_name)
108
+ true
109
+ rescue ::LoadError
110
+ false
111
+ end
112
+
113
+ # Returns the fallback adapter when no others available
114
+ #
115
+ # The json gem is a Ruby default gem since Ruby 1.9, so in practice
116
+ # the installable-adapter step always succeeds before reaching this
117
+ # fallback on any supported Ruby version. The warning below only
118
+ # fires in tests that deliberately break the require path.
119
+ #
120
+ # @api private
121
+ # @return [Symbol] the json_gem adapter name
122
+ def fallback_adapter
123
+ warn_about_fallback unless @default_adapter_warning_shown
124
+ @default_adapter_warning_shown = true
125
+ :json_gem
126
+ end
127
+
128
+ # Warns the user about reaching the last-resort fallback
129
+ #
130
+ # @api private
131
+ # @return [void]
132
+ def warn_about_fallback
133
+ Kernel.warn(
134
+ "[WARNING] MultiJson is falling back to the json_gem adapter " \
135
+ "because no other JSON library could be loaded."
136
+ )
137
+ end
138
+
139
+ # Loads an adapter from a specification
140
+ #
141
+ # @api private
142
+ # @param adapter_spec [Symbol, String, Module, nil, false] adapter specification
143
+ # @return [Class] the adapter class
144
+ def load_adapter(adapter_spec)
145
+ case adapter_spec
146
+ when ::String then load_adapter_by_name(adapter_spec)
147
+ when ::Symbol then load_adapter_by_name(adapter_spec.to_s)
148
+ when nil, false then load_adapter(default_adapter)
149
+ when ::Module then adapter_spec
150
+ else raise ::LoadError, "expected adapter to be a Symbol, String, or Module, got #{adapter_spec.inspect}"
151
+ end
152
+ rescue ::LoadError => e
153
+ raise AdapterError.build(e)
154
+ end
155
+
156
+ # Loads an adapter by its string name
157
+ #
158
+ # ``jrjackson`` (the JrJackson gem's name) is normalized to
159
+ # ``jr_jackson`` (the adapter file/class name) for backwards
160
+ # compatibility with the original gem-name alias.
161
+ #
162
+ # @api private
163
+ # @param name [String] adapter name
164
+ # @return [Class] the adapter class
165
+ def load_adapter_by_name(name)
166
+ normalized = name.downcase
167
+ normalized = "jr_jackson" if normalized == "jrjackson"
168
+ require_relative "adapters/#{normalized}"
169
+
170
+ class_name = normalized.split("_").map(&:capitalize).join
171
+ ::MultiJson::Adapters.const_get(class_name)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast_jsonparser"
4
+ require_relative "../adapter"
5
+ require_relative "../adapter_selector"
6
+
7
+ module MultiJson
8
+ module Adapters
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).
20
+ class FastJsonparser < Adapter
21
+ defaults :load, symbolize_keys: false
22
+
23
+ # Exception raised when JSON parsing fails
24
+ ParseError = ::FastJsonparser::ParseError
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
+
52
+ # Parse a JSON string into a Ruby object
53
+ #
54
+ # FastJsonparser.parse only accepts ``symbolize_keys`` and raises
55
+ # on unknown keyword arguments, so the adapter explicitly forwards
56
+ # only that option and silently drops the rest. Pass other options
57
+ # through ``MultiJson.load_options=`` and they'll apply to whichever
58
+ # adapter MultiJson selects when fast_jsonparser isn't installed.
59
+ #
60
+ # @api private
61
+ # @param string [String] JSON string to parse
62
+ # @param options [Hash] parsing options (only :symbolize_keys is honored)
63
+ # @return [Object] parsed Ruby object
64
+ #
65
+ # @example Parse JSON string
66
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
67
+ def load(string, options = {})
68
+ ::FastJsonparser.parse(string, symbolize_keys: options[:symbolize_keys])
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gson"
4
+ require_relative "../adapter"
5
+
6
+ module MultiJson
7
+ module Adapters
8
+ # Use the gson.rb library to dump/load.
9
+ class Gson < Adapter
10
+ # Exception raised when JSON parsing fails
11
+ ParseError = ::Gson::DecodeError
12
+
13
+ # Pre-allocated zero-options decoder/encoder for the dominant call
14
+ # path. Without an explicit ``defaults :load`` / ``defaults :dump``
15
+ # block on this adapter, ``MultiJson.load(json)`` and
16
+ # ``MultiJson.dump(obj)`` arrive here with the shared frozen empty
17
+ # hash from {Adapter.strip_adapter_key}, so reusing a single
18
+ # decoder/encoder instance avoids the per-call ``Decoder.new`` /
19
+ # ``Encoder.new`` allocation that the previous implementation
20
+ # incurred. Java Gson is documented thread-safe, so sharing the
21
+ # underlying handle across threads is safe.
22
+ DEFAULT_DECODER = ::Gson::Decoder.new({})
23
+ DEFAULT_ENCODER = ::Gson::Encoder.new({})
24
+ private_constant :DEFAULT_DECODER, :DEFAULT_ENCODER
25
+
26
+ # Parse a JSON string into a Ruby object
27
+ #
28
+ # @api private
29
+ # @param string [String] JSON string to parse
30
+ # @param options [Hash] parsing options
31
+ # @return [Object] parsed Ruby object
32
+ #
33
+ # @example Parse JSON string
34
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
35
+ def load(string, options = {})
36
+ decoder = options.empty? ? DEFAULT_DECODER : ::Gson::Decoder.new(options)
37
+ decoder.decode(string)
38
+ end
39
+
40
+ # Serialize a Ruby object to JSON
41
+ #
42
+ # @api private
43
+ # @param object [Object] object to serialize
44
+ # @param options [Hash] serialization options
45
+ # @return [String] JSON string
46
+ #
47
+ # @example Serialize object to JSON
48
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
49
+ def dump(object, options = {})
50
+ encoder = options.empty? ? DEFAULT_ENCODER : ::Gson::Encoder.new(options)
51
+ encoder.encode(object)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jrjackson" unless defined?(JrJackson)
4
+ require_relative "../adapter"
5
+
6
+ module MultiJson
7
+ module Adapters
8
+ # Use the jrjackson.rb library to dump/load.
9
+ class JrJackson < Adapter
10
+ # Exception raised when JSON parsing fails
11
+ ParseError = ::JrJackson::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
+ ::JrJackson::Json.load(string, options)
24
+ end
25
+
26
+ # Serialize a Ruby object to JSON
27
+ #
28
+ # Requires JrJackson >= 0.4.18, which accepts an options hash as
29
+ # the second argument to ``Json.dump``.
30
+ #
31
+ # @api private
32
+ # @param object [Object] object to serialize
33
+ # @param options [Hash] serialization options
34
+ # @return [String] JSON string
35
+ #
36
+ # @example Serialize object to JSON
37
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
38
+ def dump(object, options = {})
39
+ ::JrJackson::Json.dump(object, options)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../adapter"
4
+ require "json"
5
+
6
+ module MultiJson
7
+ module Adapters
8
+ # Use the JSON gem to dump/load.
9
+ class JsonGem < Adapter
10
+ # Exception raised when JSON parsing fails
11
+ ParseError = ::JSON::ParserError
12
+
13
+ defaults :load, create_additions: false, quirks_mode: true
14
+
15
+ PRETTY_STATE_PROTOTYPE = {
16
+ indent: " ",
17
+ space: " ",
18
+ object_nl: "\n",
19
+ array_nl: "\n"
20
+ }.freeze
21
+ private_constant :PRETTY_STATE_PROTOTYPE
22
+
23
+ # Parse a JSON string into a Ruby object
24
+ #
25
+ # @api private
26
+ # @param string [String] JSON string to parse
27
+ # @param options [Hash] parsing options
28
+ # @return [Object] parsed Ruby object
29
+ # @raise [::JSON::ParserError] when input contains invalid bytes that
30
+ # cannot be transcoded to UTF-8
31
+ #
32
+ # @example Parse JSON string
33
+ # adapter.load('{"key":"value"}') #=> {"key" => "value"}
34
+ def load(string, options = {})
35
+ if string.encoding != Encoding::UTF_8
36
+ begin
37
+ string = string.encode(Encoding::UTF_8)
38
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
39
+ raise ::JSON::ParserError, e.message
40
+ end
41
+ end
42
+
43
+ ::JSON.parse(string, translate_load_options(options))
44
+ end
45
+
46
+ # Serialize a Ruby object to JSON
47
+ #
48
+ # @api private
49
+ # @param object [Object] object to serialize
50
+ # @param options [Hash] serialization options
51
+ # @return [String] JSON string
52
+ #
53
+ # @example Serialize object to JSON
54
+ # adapter.dump({key: "value"}) #=> '{"key":"value"}'
55
+ def dump(object, options = {})
56
+ json_object = object.respond_to?(:as_json) ? object.as_json : object
57
+ return ::JSON.dump(json_object) if options.empty?
58
+ return ::JSON.generate(json_object, options) unless options.key?(:pretty)
59
+
60
+ # Common case: ``pretty: true`` is the only option, so the merge
61
+ # would just produce a copy of PRETTY_STATE_PROTOTYPE.
62
+ return ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE) if options.size == 1
63
+
64
+ ::JSON.pretty_generate(json_object, PRETTY_STATE_PROTOTYPE.merge(options.except(:pretty)))
65
+ end
66
+
67
+ private
68
+
69
+ # Translate ``:symbolize_keys`` into JSON gem's ``:symbolize_names``
70
+ #
71
+ # Returns a new hash without mutating the input. ``options`` is the
72
+ # cached hash returned from {Adapter.merged_load_options}, so in-place
73
+ # edits would pollute the cache and corrupt subsequent calls.
74
+ #
75
+ # @api private
76
+ # @param options [Hash] merged load options
77
+ # @return [Hash] options with ``:symbolize_keys`` translated
78
+ def translate_load_options(options)
79
+ return options unless options[:symbolize_keys]
80
+
81
+ options.except(:symbolize_keys).merge(symbolize_names: true)
82
+ end
83
+ end
84
+ end
85
+ end