smarter_json 0.5.2

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.

Potentially problematic release.


This version of smarter_json might be problematic. Click here for more details.

@@ -0,0 +1,22 @@
1
+ # Ryū, by Ulf Adams
2
+
3
+ - The algorithm is Ryū, by Ulf Adams (Copyright 2018), Apache-2.0 / Boost. Upstream: https://github.com/ulfjack/ryu
4
+ - The actual file you have was vendored from the ruby/json gem, v2.19.7 — path ext/json/ext/vendor/ryu.h. Repo: https://github.com/ruby/json
5
+
6
+ ## Update from ruby/json, not from upstream Ryū.
7
+
8
+ The function smarter_json calls — `ryu_s2d_from_parts(m10, m10digits, e10, signedM)` (line 754) — is not in stock upstream Ryū (upstream exposes `s2d/s2d_n`, which take a string). It's ruby/json's adaptation that takes a pre-extracted mantissa/exponent, which is exactly what our fj_decimal_value produces. Pull upstream and you won't have that entry point.
9
+
10
+ ## To refresh it:
11
+
12
+ ### from the raw file on GitHub (master, or pin to a json release tag):
13
+ `curl -L https://raw.githubusercontent.com/ruby/json/master/ext/json/ext/vendor/ryu.h -o ext/smarter_json/vendor/ryu.h`
14
+
15
+ ### or from your local json gem:
16
+ `cp "$(gem contents json | grep ext/json/ext/vendor/ryu.h)" ext/smarter_json/vendor/ryu.h`
17
+
18
+ It was vendored from `ruby/json`:
19
+ - origin: Ryū (`ulfjack/ryu`), adapted by `ruby/json`
20
+ - vendored from: json 2.19.7
21
+ - update command above
22
+ - license: Apache-2.0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmarterJSON
4
+ # Single base for everything this gem raises — `rescue SmarterJSON::Error` catches
5
+ # both read (process / process_file) and write (generate) failures. This file is
6
+ # required before parser.rb and generator.rb so the subclasses below can inherit
7
+ # from Error at load time.
8
+ class Error < StandardError; end
9
+
10
+ # Raised by process / process_file on genuinely unparseable input (unterminated
11
+ # string, mismatched bracket, …). Carries the line and column when known.
12
+ class ParseError < Error
13
+ attr_reader :line, :col
14
+
15
+ def initialize(message, line = nil, col = nil)
16
+ @line = line
17
+ @col = col
18
+ super(line && col ? "#{message} at line #{line}, col #{col}" : message)
19
+ end
20
+ end
21
+
22
+ # Raised when input bytes are invalid for the claimed encoding.
23
+ class EncodingError < ParseError; end
24
+
25
+ # Raised by generate when a value cannot be written as strict JSON (an unsupported
26
+ # type, or a non-finite Float / BigDecimal).
27
+ class GenerateError < Error; end
28
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module SmarterJSON
6
+ module_function
7
+
8
+ # SmarterJSON.generate(obj, options = {}) — write a Ruby value as JSON.
9
+ #
10
+ # options[:format]:
11
+ # :json (default) — standard JSON. Hash -> object, Array -> array,
12
+ # scalar -> scalar. Always valid, interoperable JSON.
13
+ # :ndjson — newline-delimited JSON. An Array writes one element per
14
+ # line; any other value writes as a single line. The
15
+ # inverse of process reading NDJSON back into an Array.
16
+ #
17
+ # options[:indent]: spaces per nesting level for pretty-printing (Integer, default
18
+ # 0 = compact). Empty objects/arrays stay inline. Not allowed with :ndjson (a
19
+ # record must be a single line) — combining them raises ArgumentError.
20
+ #
21
+ # Symbol keys/values are emitted as strings; BigDecimal as a JSON number.
22
+ # Unsupported types (Time, custom objects) and non-finite Floats raise
23
+ # SmarterJSON::GenerateError. Returns a String.
24
+ def generate(obj, options = {})
25
+ Generator.new(options).generate(obj)
26
+ end
27
+
28
+ class Generator
29
+ ESCAPE = {
30
+ '"' => '\\"', "\\" => "\\\\", "\b" => "\\b", "\f" => "\\f",
31
+ "\n" => "\\n", "\r" => "\\r", "\t" => "\\t"
32
+ }.freeze
33
+ # ", backslash, and control chars 0x00-0x1F must be escaped; everything else
34
+ # (including multi-byte UTF-8) is emitted raw — valid JSON.
35
+ ESCAPE_RE = /["\\\x00-\x1f]/.freeze
36
+
37
+ def initialize(options = {})
38
+ @format = options.fetch(:format, :json)
39
+ unless %i[json ndjson].include?(@format)
40
+ raise ArgumentError, "unknown writer format: #{@format.inspect} (expected :json or :ndjson)"
41
+ end
42
+
43
+ @indent = options.fetch(:indent, 0) # spaces per nesting level; 0 = compact (default)
44
+ unless @indent.is_a?(Integer) && @indent >= 0
45
+ raise ArgumentError, "indent must be a non-negative Integer, got #{@indent.inspect}"
46
+ end
47
+ if @indent > 0 && @format == :ndjson
48
+ raise ArgumentError, "indent is not compatible with format: :ndjson (each record must be a single line)"
49
+ end
50
+
51
+ @pretty = @indent > 0
52
+
53
+ @ascii_only = options.fetch(:ascii_only, false) # escape non-ASCII as \uXXXX
54
+ @script_safe = options.fetch(:script_safe, false) # escape </ and U+2028 / U+2029
55
+ @sort_keys = options.fetch(:sort_keys, false) # emit object keys in sorted order
56
+ @coerce = options.fetch(:coerce, false) # convert unknown types via as_json / to_json
57
+ @escape_re = build_escape_re
58
+ end
59
+
60
+ def generate(obj)
61
+ buf = +""
62
+ if @format == :ndjson
63
+ if obj.is_a?(Array)
64
+ obj.each do |v|
65
+ emit(v, buf)
66
+ buf << "\n"
67
+ end
68
+ else
69
+ emit(obj, buf)
70
+ buf << "\n"
71
+ end
72
+ else
73
+ emit(obj, buf)
74
+ end
75
+ buf
76
+ end
77
+
78
+ private
79
+
80
+ def emit(obj, buf, level = 0)
81
+ case obj
82
+ when nil then buf << "null"
83
+ when true then buf << "true"
84
+ when false then buf << "false"
85
+ when String then emit_string(obj, buf)
86
+ when Symbol then emit_string(obj.to_s, buf)
87
+ when Integer then buf << obj.to_s
88
+ when Float then emit_float(obj, buf)
89
+ when BigDecimal then emit_bigdecimal(obj, buf)
90
+ when Array then emit_array(obj, buf, level)
91
+ when Hash then emit_hash(obj, buf, level)
92
+ else
93
+ return emit_coerced(obj, buf, level) if @coerce
94
+
95
+ raise SmarterJSON::GenerateError, "SmarterJSON.generate cannot serialize #{obj.class}"
96
+ end
97
+ end
98
+
99
+ # coerce: true — let a value that isn't natively supported convert itself.
100
+ # Prefer as_json (its result is re-emitted through the normal pipeline, so the
101
+ # escaping/format options still apply); fall back to to_json (spliced as-is, so
102
+ # ascii_only / script_safe do not reach inside it). Raise if it defines neither.
103
+ def emit_coerced(obj, buf, level)
104
+ if obj.respond_to?(:as_json)
105
+ emit(obj.as_json, buf, level)
106
+ elsif obj.respond_to?(:to_json)
107
+ buf << obj.to_json
108
+ else
109
+ raise SmarterJSON::GenerateError, "SmarterJSON.generate cannot serialize #{obj.class} (no as_json or to_json)"
110
+ end
111
+ end
112
+
113
+ def emit_array(arr, buf, level)
114
+ return buf << "[]" if arr.empty? # empty stays inline, even in pretty mode
115
+
116
+ if @pretty
117
+ pad = " " * (@indent * (level + 1))
118
+ buf << "[\n"
119
+ arr.each_with_index do |v, i|
120
+ buf << ",\n" unless i.zero?
121
+ buf << pad
122
+ emit(v, buf, level + 1)
123
+ end
124
+ buf << "\n" << (" " * (@indent * level)) << "]"
125
+ else
126
+ buf << "["
127
+ arr.each_with_index do |v, i|
128
+ buf << "," unless i.zero?
129
+ emit(v, buf, level)
130
+ end
131
+ buf << "]"
132
+ end
133
+ end
134
+
135
+ def emit_hash(hash, buf, level)
136
+ return buf << "{}" if hash.empty? # empty stays inline, even in pretty mode
137
+
138
+ pairs = @sort_keys ? hash.sort_by { |k, _| k.is_a?(String) ? k : k.to_s } : hash
139
+
140
+ if @pretty
141
+ pad = " " * (@indent * (level + 1))
142
+ buf << "{\n"
143
+ first = true
144
+ pairs.each do |k, v|
145
+ buf << ",\n" unless first
146
+ first = false
147
+ buf << pad
148
+ emit_string(k.is_a?(String) ? k : k.to_s, buf) # Symbol/other keys -> string
149
+ buf << ": "
150
+ emit(v, buf, level + 1)
151
+ end
152
+ buf << "\n" << (" " * (@indent * level)) << "}"
153
+ else
154
+ buf << "{"
155
+ first = true
156
+ pairs.each do |k, v|
157
+ buf << "," unless first
158
+ first = false
159
+ emit_string(k.is_a?(String) ? k : k.to_s, buf) # Symbol/other keys -> string
160
+ buf << ":"
161
+ emit(v, buf, level)
162
+ end
163
+ buf << "}"
164
+ end
165
+ end
166
+
167
+ def emit_string(str, buf)
168
+ buf << '"' << str.gsub(@escape_re) { |m| escape_match(m) } << '"'
169
+ end
170
+
171
+ # Per-instance escape regex from the active options. Always: ", backslash, C0
172
+ # controls. script_safe adds the slash in </ and the JS line separators
173
+ # U+2028/U+2029. ascii_only adds every non-ASCII char.
174
+ def build_escape_re
175
+ res = [ESCAPE_RE]
176
+ res.unshift(%r{</}) if @script_safe
177
+ res << Regexp.new("[#{[0x2028, 0x2029].pack('U*')}]") if @script_safe
178
+ res << /[^\x00-\x7f]/ if @ascii_only
179
+ Regexp.union(*res)
180
+ end
181
+
182
+ def escape_match(m)
183
+ return "<\\/" if m == "</" # <\/ — stops </ from closing a <script> tag
184
+
185
+ ESCAPE[m] || unicode_escape(m)
186
+ end
187
+
188
+ # \uXXXX for a BMP char; a UTF-16 surrogate pair for astral (> U+FFFF) chars.
189
+ def unicode_escape(char)
190
+ cp = char.ord
191
+ return format("\\u%04x", cp) if cp <= 0xffff
192
+
193
+ cp -= 0x10000
194
+ format("\\u%04x\\u%04x", 0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff))
195
+ end
196
+
197
+ def emit_float(flt, buf)
198
+ raise SmarterJSON::GenerateError, "SmarterJSON.generate cannot serialize non-finite Float #{flt}" unless flt.finite?
199
+
200
+ buf << flt.to_s # Ruby's Float#to_s is shortest round-trippable; e-notation is valid JSON
201
+ end
202
+
203
+ def emit_bigdecimal(num, buf)
204
+ raise SmarterJSON::GenerateError, "SmarterJSON.generate cannot serialize non-finite BigDecimal" unless num.finite?
205
+
206
+ buf << num.to_s("F") # plain decimal notation (BigDecimal's default "0.1e1" is not valid JSON)
207
+ end
208
+ end
209
+ end