multi_json 1.15.0 → 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.
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Deprecated public API kept around for one major release
4
+ #
5
+ # Each method here emits a one-time deprecation warning on first call and
6
+ # delegates to its current-API counterpart. The whole file is loaded by
7
+ # {MultiJSON} so the deprecation surface stays out of the main module
8
+ # definition.
9
+ #
10
+ # @api private
11
+ module MultiJSON
12
+ class << self
13
+ private
14
+
15
+ # Define a deprecated alias that delegates to a new method name
16
+ #
17
+ # The generated singleton method emits a one-time deprecation
18
+ # warning naming the replacement, then forwards all positional and
19
+ # keyword arguments plus any block to ``replacement``. Used for the
20
+ # ``load`` / ``dump`` / ``decode`` / ``encode`` / ``engine*`` /
21
+ # ``with_engine`` / ``default_engine`` aliases that are scheduled
22
+ # for removal in v2.0.
23
+ #
24
+ # @api private
25
+ # @param name [Symbol] deprecated method name
26
+ # @param replacement [Symbol] current-API method to delegate to
27
+ # @return [Symbol] the defined method name
28
+ # @example
29
+ # deprecate_alias :load, :parse
30
+ def deprecate_alias(name, replacement)
31
+ message = "MultiJSON.#{name} is deprecated and will be removed in v2.0. Use MultiJSON.#{replacement} instead."
32
+ define_singleton_method(name) do |*args, **kwargs, &block|
33
+ warn_deprecation_once(name, message)
34
+ public_send(replacement, *args, **kwargs, &block)
35
+ end
36
+ end
37
+
38
+ # Define a deprecated method whose body needs custom delegation
39
+ #
40
+ # Used for the ``default_options`` / ``default_options=`` pair
41
+ # whose body fans out to multiple replacement methods, and for the
42
+ # ``cached_options`` / ``reset_cached_options!`` no-op stubs that
43
+ # have no current-API counterpart at all. The block runs in its
44
+ # own lexical ``self``, which is the ``MultiJSON`` module since
45
+ # every call site sits inside ``module MultiJSON`` below.
46
+ #
47
+ # @api private
48
+ # @param name [Symbol] deprecated method name
49
+ # @param message [String] warning to emit on first call
50
+ # @yield body to evaluate after the warning
51
+ # @return [Symbol] the defined method name
52
+ # @example
53
+ # deprecate_method(:cached_options, "...") { nil }
54
+ def deprecate_method(name, message, &body)
55
+ define_singleton_method(name) do |*args, **kwargs|
56
+ warn_deprecation_once(name, message)
57
+ body.call(*args, **kwargs)
58
+ end
59
+ end
60
+ end
61
+
62
+ deprecate_alias :load, :parse
63
+ deprecate_alias :dump, :generate
64
+ deprecate_alias :decode, :parse
65
+ deprecate_alias :encode, :generate
66
+ deprecate_alias :engine, :adapter
67
+ deprecate_alias :engine=, :adapter=
68
+ deprecate_alias :default_engine, :default_adapter
69
+ deprecate_alias :with_engine, :with_adapter
70
+
71
+ deprecate_method(
72
+ :default_options=,
73
+ "MultiJSON.default_options setter is deprecated\n" \
74
+ "Use MultiJSON.parse_options and MultiJSON.generate_options instead"
75
+ ) { |value| self.parse_options = self.generate_options = value }
76
+
77
+ deprecate_method(
78
+ :default_options,
79
+ "MultiJSON.default_options is deprecated\n" \
80
+ "Use MultiJSON.parse_options or MultiJSON.generate_options instead"
81
+ ) { parse_options }
82
+
83
+ %i[cached_options reset_cached_options!].each do |name|
84
+ deprecate_method(name, "MultiJSON.#{name} method is deprecated and no longer used.") { nil }
85
+ end
86
+
87
+ private
88
+
89
+ # Instance-method delegate for the deprecated default_options setter
90
+ #
91
+ # @api private
92
+ # @deprecated Use {MultiJSON.load_options=} and {MultiJSON.dump_options=} instead
93
+ # @param value [Hash] options hash
94
+ # @return [Hash] the options hash
95
+ # @example
96
+ # class Foo; include MultiJSON; end
97
+ # Foo.new.send(:default_options=, symbolize_keys: true)
98
+ def default_options=(value)
99
+ MultiJSON.default_options = value
100
+ end
101
+
102
+ # Instance-method delegate for the deprecated default_options getter
103
+ #
104
+ # @api private
105
+ # @deprecated Use {MultiJSON.load_options} or {MultiJSON.dump_options} instead
106
+ # @return [Hash] the current load options
107
+ # @example
108
+ # class Foo; include MultiJSON; end
109
+ # Foo.new.send(:default_options)
110
+ def default_options
111
+ MultiJSON.default_options
112
+ end
113
+ end
@@ -1,39 +1,186 @@
1
- module MultiJson
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
4
+ # Mixin providing configurable parse/generate options
5
+ #
6
+ # Supports static hashes or dynamic callables (procs/lambdas).
7
+ # Extended by both MultiJSON (global options) and Adapter classes.
8
+ #
9
+ # @api private
2
10
  module Options
3
- def load_options=(options)
11
+ # Steep needs an inline `#:` annotation here because `{}.freeze`
12
+ # would be inferred as `Hash[untyped, untyped]` and trip
13
+ # `UnannotatedEmptyCollection`. The annotation requires
14
+ # `Hash.new.freeze` (not the `{}.freeze` rubocop would prefer)
15
+ # because the `#:` cast only applies to method-call results.
16
+ EMPTY_OPTIONS = Hash.new.freeze #: options # rubocop:disable Style/EmptyLiteral
17
+
18
+ # Set options for parse operations
19
+ #
20
+ # @api public
21
+ # @param options [Hash, Proc] options hash or callable
22
+ # @return [Hash, Proc] the options
23
+ # @example
24
+ # MultiJSON.parse_options = {symbolize_keys: true}
25
+ def parse_options=(options)
4
26
  OptionsCache.reset
5
- @load_options = options
27
+ @parse_options = options
6
28
  end
7
29
 
8
- def dump_options=(options)
30
+ # Set options for generate operations
31
+ #
32
+ # @api public
33
+ # @param options [Hash, Proc] options hash or callable
34
+ # @return [Hash, Proc] the options
35
+ # @example
36
+ # MultiJSON.generate_options = {pretty: true}
37
+ def generate_options=(options)
9
38
  OptionsCache.reset
10
- @dump_options = options
39
+ @generate_options = options
40
+ end
41
+
42
+ # Get options for parse operations
43
+ #
44
+ # When `@parse_options` is a callable (proc/lambda), it's invoked
45
+ # with `args` as positional arguments — typically the merged
46
+ # options hash from `Adapter.merged_parse_options`. When it's a
47
+ # plain hash, `args` is ignored.
48
+ #
49
+ # @api public
50
+ # @param args [Array<Object>] forwarded to the callable, ignored otherwise
51
+ # @return [Hash] resolved options hash
52
+ # @example
53
+ # MultiJSON.parse_options #=> {}
54
+ def parse_options(*args)
55
+ resolve_options(@parse_options, *args) || default_parse_options
11
56
  end
12
57
 
58
+ # Get options for generate operations
59
+ #
60
+ # @api public
61
+ # @param args [Array<Object>] forwarded to the callable, ignored otherwise
62
+ # @return [Hash] resolved options hash
63
+ # @example
64
+ # MultiJSON.generate_options #=> {}
65
+ def generate_options(*args)
66
+ resolve_options(@generate_options, *args) || default_generate_options
67
+ end
68
+
69
+ # Get default parse options
70
+ #
71
+ # @api private
72
+ # @return [Hash] frozen empty hash
73
+ def default_parse_options
74
+ Concurrency.synchronize(:default_options) { @default_parse_options ||= EMPTY_OPTIONS }
75
+ end
76
+
77
+ # Get default generate options
78
+ #
79
+ # @api private
80
+ # @return [Hash] frozen empty hash
81
+ def default_generate_options
82
+ Concurrency.synchronize(:default_options) { @default_generate_options ||= EMPTY_OPTIONS }
83
+ end
84
+
85
+ # Set options for parse operations
86
+ #
87
+ # @api public
88
+ # @deprecated Use {#parse_options=} instead. Will be removed in v2.0.
89
+ # @param options [Hash, Proc] options hash or callable
90
+ # @return [Hash, Proc] the options
91
+ # @example
92
+ # MultiJSON.load_options = {symbolize_keys: true}
93
+ def load_options=(options)
94
+ MultiJSON.warn_deprecation_once(:load_options=,
95
+ "MultiJSON.load_options= is deprecated and will be removed in v2.0. Use MultiJSON.parse_options= instead.")
96
+ self.parse_options = options
97
+ end
98
+
99
+ # Set options for generate operations
100
+ #
101
+ # @api public
102
+ # @deprecated Use {#generate_options=} instead. Will be removed in v2.0.
103
+ # @param options [Hash, Proc] options hash or callable
104
+ # @return [Hash, Proc] the options
105
+ # @example
106
+ # MultiJSON.dump_options = {pretty: true}
107
+ def dump_options=(options)
108
+ MultiJSON.warn_deprecation_once(:dump_options=,
109
+ "MultiJSON.dump_options= is deprecated and will be removed in v2.0. Use MultiJSON.generate_options= instead.")
110
+ self.generate_options = options
111
+ end
112
+
113
+ # Get options for parse operations
114
+ #
115
+ # @api public
116
+ # @deprecated Use {#parse_options} instead. Will be removed in v2.0.
117
+ # @param args [Array<Object>] forwarded to the callable, ignored otherwise
118
+ # @return [Hash] resolved options hash
119
+ # @example
120
+ # MultiJSON.load_options #=> {}
13
121
  def load_options(*args)
14
- defined?(@load_options) && get_options(@load_options, *args) || default_load_options
122
+ MultiJSON.warn_deprecation_once(:load_options,
123
+ "MultiJSON.load_options is deprecated and will be removed in v2.0. Use MultiJSON.parse_options instead.")
124
+ parse_options(*args)
15
125
  end
16
126
 
127
+ # Get options for generate operations
128
+ #
129
+ # @api public
130
+ # @deprecated Use {#generate_options} instead. Will be removed in v2.0.
131
+ # @param args [Array<Object>] forwarded to the callable, ignored otherwise
132
+ # @return [Hash] resolved options hash
133
+ # @example
134
+ # MultiJSON.dump_options #=> {}
17
135
  def dump_options(*args)
18
- defined?(@dump_options) && get_options(@dump_options, *args) || default_dump_options
136
+ MultiJSON.warn_deprecation_once(:dump_options,
137
+ "MultiJSON.dump_options is deprecated and will be removed in v2.0. Use MultiJSON.generate_options instead.")
138
+ generate_options(*args)
19
139
  end
20
140
 
141
+ # Get default parse options
142
+ #
143
+ # @api private
144
+ # @deprecated Use {#default_parse_options} instead. Will be removed in v2.0.
145
+ # @return [Hash] frozen empty hash
21
146
  def default_load_options
22
- @default_load_options ||= {}
147
+ default_parse_options
23
148
  end
24
149
 
150
+ # Get default generate options
151
+ #
152
+ # @api private
153
+ # @deprecated Use {#default_generate_options} instead. Will be removed in v2.0.
154
+ # @return [Hash] frozen empty hash
25
155
  def default_dump_options
26
- @default_dump_options ||= {}
156
+ default_generate_options
27
157
  end
28
158
 
29
- private
159
+ private
30
160
 
31
- def get_options(options, *args)
32
- if options.respond_to?(:call) && options.arity
33
- options.arity == 0 ? options[] : options[*args]
34
- elsif options.respond_to?(:to_hash)
35
- options.to_hash
161
+ # Resolves options from a hash or callable
162
+ #
163
+ # @api private
164
+ # @param options [Hash, Proc, nil] options configuration
165
+ # @param args [Array<Object>] arguments forwarded to a callable provider
166
+ # @return [Hash, nil] resolved options hash
167
+ def resolve_options(options, *args)
168
+ if options.respond_to?(:call)
169
+ # @type var options: options_proc
170
+ return invoke_callable(options, *args)
36
171
  end
172
+
173
+ options.to_hash if options.respond_to?(:to_hash)
174
+ end
175
+
176
+ # Invokes a callable options provider
177
+ #
178
+ # @api private
179
+ # @param callable [Proc] options provider
180
+ # @param args [Array<Object>] arguments forwarded when the callable is non-arity-zero
181
+ # @return [Hash] options returned by the callable
182
+ def invoke_callable(callable, *args)
183
+ callable.arity.zero? ? callable.call : callable.call(*args)
37
184
  end
38
185
  end
39
186
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
4
+ module OptionsCache
5
+ # Thread-safe cache store backed by a Hash guarded by a Mutex
6
+ #
7
+ # Used on MRI and TruffleRuby, where a runtime dependency on
8
+ # concurrent-ruby would be overkill: the GVL on MRI makes single
9
+ # lookups atomic, and locking on both reads and writes keeps the
10
+ # small perf cost predictable across engines without adding a
11
+ # runtime dependency.
12
+ #
13
+ # @api private
14
+ class Store
15
+ # Create a new cache store
16
+ #
17
+ # @api private
18
+ # @return [Store] new store instance
19
+ def initialize
20
+ @cache = {}
21
+ @mutex = Mutex.new
22
+ end
23
+
24
+ # Clear all cached entries
25
+ #
26
+ # Held under the mutex because TruffleRuby (which also uses this
27
+ # backend via the ruby-platform gem) has true parallelism: a
28
+ # concurrent ``fetch`` racing with ``Hash#clear`` could corrupt
29
+ # iteration in a way that MRI's GVL would otherwise prevent.
30
+ #
31
+ # @api private
32
+ # @return [void]
33
+ def reset
34
+ @mutex.synchronize { @cache.clear }
35
+ end
36
+
37
+ # Fetch a value from cache or compute it
38
+ #
39
+ # When called with a block, returns the cached value or computes a
40
+ # new one. When called without a block, returns the cached value or
41
+ # the supplied default if the key is missing. Nil cached values are
42
+ # preserved because ``Hash#fetch`` only falls through to the default
43
+ # block when the key is truly missing. The ``block_given?`` check
44
+ # is hoisted out of the mutex so the no-block read path runs the
45
+ # check once per call instead of once inside the critical section.
46
+ #
47
+ # @api private
48
+ # @param key [Object] cache key
49
+ # @param default [Object] value to return when key is missing and no
50
+ # block is given
51
+ # @yield block to compute value if not cached
52
+ # @return [Object] cached, computed, or default value
53
+ def fetch(key, default = nil)
54
+ return @mutex.synchronize { @cache.fetch(key) { default } } unless block_given?
55
+
56
+ @mutex.synchronize do
57
+ @cache.fetch(key) do
58
+ @cache.shift if @cache.size >= OptionsCache.max_cache_size
59
+ @cache[key] = yield
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,29 +1,86 @@
1
- module MultiJson
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
4
+ # Thread-safe bounded cache for merged options hashes
5
+ #
6
+ # Caches are separated for load and dump operations. Each cache is
7
+ # bounded to prevent unbounded memory growth when options are
8
+ # generated dynamically. The ``Store`` backend is chosen at load time
9
+ # based on ``RUBY_ENGINE``: JRuby uses Concurrent::Map (shipped as a
10
+ # runtime dependency of the java-platform gem); MRI and TruffleRuby
11
+ # use a Hash guarded by a Mutex.
12
+ #
13
+ # @api private
2
14
  module OptionsCache
3
- extend self
15
+ # Default bound on the number of cached entries per store. Applications
16
+ # that dynamically generate many distinct option hashes can raise this
17
+ # via {.max_cache_size=}.
18
+ DEFAULT_MAX_CACHE_SIZE = 1000
4
19
 
5
- def reset
6
- @dump_cache = {}
7
- @load_cache = {}
8
- end
20
+ class << self
21
+ # Get the dump options cache
22
+ #
23
+ # @api private
24
+ # @return [Store] dump cache store
25
+ attr_reader :dump
9
26
 
10
- def fetch(type, key, &block)
11
- cache = instance_variable_get("@#{type}_cache")
12
- cache.key?(key) ? cache[key] : write(cache, key, &block)
13
- end
27
+ # Get the load options cache
28
+ #
29
+ # @api private
30
+ # @return [Store] load cache store
31
+ attr_reader :load
14
32
 
15
- private
33
+ # Maximum number of entries per cache store
34
+ #
35
+ # Applies to both the dump and load caches. Existing entries are
36
+ # left in place until normal eviction trims them below a lowered
37
+ # limit; call {.reset} if you need to evict immediately.
38
+ #
39
+ # @api public
40
+ # @return [Integer] current cache size limit
41
+ # @example
42
+ # MultiJSON::OptionsCache.max_cache_size = 5000
43
+ # MultiJSON::OptionsCache.max_cache_size #=> 5000
44
+ attr_reader :max_cache_size
16
45
 
17
- # Normally MultiJson is used with a few option sets for both dump/load
18
- # methods. When options are generated dynamically though, every call would
19
- # cause a cache miss and the cache would grow indefinitely. To prevent
20
- # this, we just reset the cache every time the number of keys outgrows
21
- # 1000.
22
- MAX_CACHE_SIZE = 1000
46
+ # Set the maximum number of entries per cache store
47
+ #
48
+ # @api public
49
+ # @param value [Integer] positive entry cap
50
+ # @return [Integer] the validated value
51
+ # @raise [ArgumentError] when value is not a positive Integer
52
+ # @example
53
+ # MultiJSON::OptionsCache.max_cache_size = 5000
54
+ def max_cache_size=(value)
55
+ raise ArgumentError, "max_cache_size must be a positive Integer, got #{value.inspect}" unless Integer === value && value.positive? # rubocop:disable Style/CaseEquality
23
56
 
24
- def write(cache, key)
25
- cache.clear if cache.length >= MAX_CACHE_SIZE
26
- cache[key] = yield
57
+ @max_cache_size = value
58
+ end
59
+
60
+ # Reset both caches
61
+ #
62
+ # @api private
63
+ # @return [void]
64
+ def reset
65
+ @dump = Store.new
66
+ @load = Store.new
67
+ end
27
68
  end
69
+
70
+ self.max_cache_size = DEFAULT_MAX_CACHE_SIZE
28
71
  end
29
72
  end
73
+
74
+ module MultiJSON
75
+ module OptionsCache
76
+ # Dynamic require path so MRI (mutex_store) and JRuby
77
+ # (concurrent_store) execute the same physical line, avoiding a
78
+ # dead-branch ``require_relative`` that would otherwise drop
79
+ # JRuby's line coverage below 100%.
80
+ BACKENDS = {"jruby" => "concurrent_store"}.freeze
81
+ private_constant :BACKENDS
82
+ end
83
+ end
84
+
85
+ require_relative "options_cache/#{MultiJSON::OptionsCache.send(:const_get, :BACKENDS).fetch(RUBY_ENGINE, "mutex_store")}"
86
+ MultiJSON::OptionsCache.reset
@@ -1,17 +1,103 @@
1
- module MultiJson
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
4
+ # Raised when JSON parsing fails
5
+ #
6
+ # Wraps the underlying adapter's parse error with the original input
7
+ # data, plus best-effort line and column extraction from the adapter's
8
+ # error message. Line/column are populated for adapters that include
9
+ # them in their messages (Oj, the json gem) and remain nil for
10
+ # adapters that don't (Yajl, fast_jsonparser).
11
+ #
12
+ # @api public
2
13
  class ParseError < StandardError
3
- attr_reader :data, :cause
14
+ # Regex that matches the "line N[, ]column M" fragment inside an
15
+ # adapter error message. The separator between line and column is
16
+ # permissive — Oj emits ``"line 1, column 3"`` while the json gem
17
+ # emits ``"line 1 column 2"`` — so ``[,\s]+`` covers both. Column
18
+ # is optional so messages like ``"at line 5"`` still yield a line.
19
+ LOCATION_PATTERN = /line\s+(\d+)(?:[,\s]+column\s+(\d+))?/i
20
+ private_constant :LOCATION_PATTERN
21
+
22
+ # The input string that failed to parse
23
+ #
24
+ # @api public
25
+ # @return [String, nil] the original input data
26
+ # @example
27
+ # error.data #=> "{invalid json}"
28
+ attr_reader :data
29
+
30
+ # The 1-based line number reported by the adapter
31
+ #
32
+ # @api public
33
+ # @return [Integer, nil] line number, or nil if the adapter's message
34
+ # did not include one
35
+ # @example
36
+ # error.line #=> 1
37
+ attr_reader :line
38
+
39
+ # The 1-based column reported by the adapter
40
+ #
41
+ # @api public
42
+ # @return [Integer, nil] column number, or nil if the adapter's message
43
+ # did not include one
44
+ # @example
45
+ # error.column #=> 3
46
+ attr_reader :column
47
+
48
+ # Create a new ParseError
49
+ #
50
+ # @api public
51
+ # @param message [String, nil] error message
52
+ # @param data [String, nil] the input that failed to parse
53
+ # @param cause [Exception, nil] the original exception
54
+ # @return [ParseError] new error instance
55
+ # @example
56
+ # ParseError.new("unexpected token at line 1 column 2", data: "{}")
57
+ def initialize(message = nil, data: nil, cause: nil)
58
+ super(message)
59
+ @data = data
60
+ match = location_match(message)
61
+ @line = match && Integer(match[1])
62
+ @column = match && match[2] && Integer(match[2])
63
+ set_backtrace(cause.backtrace) if cause
64
+ end
4
65
 
66
+ # Build a ParseError from an original exception
67
+ #
68
+ # @api public
69
+ # @param original_exception [Exception] the adapter's parse error
70
+ # @param data [String] the input that failed to parse
71
+ # @return [ParseError] new error with formatted message
72
+ # @example
73
+ # ParseError.build(JSON::ParserError.new("..."), "{bad json}")
5
74
  def self.build(original_exception, data)
6
- new(original_exception.message).tap do |exception|
7
- exception.instance_eval do
8
- @cause = original_exception
9
- set_backtrace original_exception.backtrace
10
- @data = data
11
- end
12
- end
75
+ new(original_exception.message, data: data, cause: original_exception)
76
+ end
77
+
78
+ private
79
+
80
+ # Match an adapter error message against the line/column pattern
81
+ #
82
+ # Adapter error messages sometimes embed bytes from the failing
83
+ # input (e.g., the json gem's ``"invalid byte sequence in UTF-8"``
84
+ # error). The pattern is pure ASCII so it's compatible with any
85
+ # encoding, but a UTF-8 string with invalid bytes still trips the
86
+ # regex engine — ``String#scrub`` replaces those bytes so the
87
+ # match can proceed. Strings in binary (ASCII-8BIT) or any valid
88
+ # encoding pass through scrub untouched.
89
+ #
90
+ # @api private
91
+ # @param message [String, nil] the adapter's error message
92
+ # @return [MatchData, nil] the regex match, or nil if no message or
93
+ # no location fragment was found
94
+ def location_match(message)
95
+ return unless message
96
+
97
+ LOCATION_PATTERN.match(message.scrub)
13
98
  end
14
99
  end
15
100
 
16
- DecodeError = LoadError = ParseError # Legacy support
101
+ # Legacy aliases for backward compatibility
102
+ DecodeError = LoadError = ParseError
17
103
  end
@@ -1,17 +1,30 @@
1
- module MultiJson
1
+ # frozen_string_literal: true
2
+
3
+ module MultiJSON
4
+ # Version information for MultiJSON
5
+ #
6
+ # @api private
2
7
  class Version
3
- MAJOR = 1 unless defined? MultiJson::Version::MAJOR
4
- MINOR = 15 unless defined? MultiJson::Version::MINOR
5
- PATCH = 0 unless defined? MultiJson::Version::PATCH
6
- PRE = nil unless defined? MultiJson::Version::PRE
8
+ # Major version number
9
+ MAJOR = 1 unless defined? MultiJSON::Version::MAJOR
10
+ # Minor version number
11
+ MINOR = 21 unless defined? MultiJSON::Version::MINOR
12
+ # Patch version number
13
+ PATCH = 1 unless defined? MultiJSON::Version::PATCH
14
+ # Pre-release version suffix
15
+ PRE = nil unless defined? MultiJSON::Version::PRE
7
16
 
8
17
  class << self
9
- # @return [String]
18
+ # Return the version string
19
+ #
20
+ # @api private
21
+ # @return [String] version in semver format
10
22
  def to_s
11
- [MAJOR, MINOR, PATCH, PRE].compact.join('.')
23
+ [MAJOR, MINOR, PATCH, PRE].compact.join(".")
12
24
  end
13
25
  end
14
26
  end
15
27
 
28
+ # Current version string in semver format
16
29
  VERSION = Version.to_s.freeze
17
30
  end