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.
- checksums.yaml +7 -0
- data/.gitignore +46 -0
- data/CHANGELOG.md +75 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +22 -0
- data/docs/_introduction.md +48 -0
- data/docs/basic_read_api.md +72 -0
- data/docs/basic_write_api.md +144 -0
- data/docs/examples.md +140 -0
- data/docs/options.md +69 -0
- data/ext/smarter_json/extconf.rb +30 -0
- data/ext/smarter_json/smarter_json.c +1424 -0
- data/ext/smarter_json/smarter_json.h +9 -0
- data/ext/smarter_json/vendor/ryu.h +819 -0
- data/ext/smarter_json/vendor/ryu.md +22 -0
- data/lib/smarter_json/errors.rb +28 -0
- data/lib/smarter_json/generator.rb +209 -0
- data/lib/smarter_json/parser.rb +926 -0
- data/lib/smarter_json/version.rb +5 -0
- data/lib/smarter_json.rb +24 -0
- metadata +86 -0
|
@@ -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
|