nosj 0.4.1 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/Cargo.lock +1 -1
- data/README.md +30 -14
- data/ext/nosj/Cargo.toml +1 -1
- data/ext/nosj/fuzz/src/prelude.rb +20 -44
- data/ext/nosj/src/gen/opts.rs +76 -141
- data/ext/nosj/src/gen/ruby.rs +5 -0
- data/ext/nosj/src/gen/walker.rs +166 -32
- data/ext/nosj/src/lib.rs +4 -0
- data/ext/nosj/src/locate.rs +162 -0
- data/ext/nosj/src/opt_reader.rs +170 -0
- data/ext/nosj/src/parse.rs +160 -93
- data/ext/nosj/src/reformat.rs +54 -125
- data/ext/nosj/src/sink.rs +180 -44
- data/ext/nosj/src/state.rs +5 -0
- data/ext/nosj/src/stats.rs +33 -30
- data/lib/nosj/json.rb +105 -39
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +34 -21
- data/sig/nosj.rbs +4 -3
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 354e310be9d2d56119d2db1f9ad13052c9a6bc8fba5c8d7b7993b78abf345e1e
|
|
4
|
+
data.tar.gz: b21acc76a56a10a56c62c370ff1dfcaa8dc263a96958fcac6c4f3c213c4bb542
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c672e8653239dc1600ce19b4b38c31ae298dbf40a79449fe12ccf05335144eed7d210a115e87a3a60e8b534b868d3c0080079072ed21a8e67b398e2e8d7e8fc4
|
|
7
|
+
data.tar.gz: 22aadd62a736d86fe310ed099d995fe6006e149103eb95d41d128adc6164e6c2f64c2de49e6652437bccefbef70379b27cc3700d45fcad7ae1e2003ac6f05f41
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
## [0.5.0] - 2026-09-25
|
|
2
|
+
|
|
3
|
+
**json 3.0 compatibility.** nosj now matches the behavior of the json
|
|
4
|
+
gem 3.0, and stays compatible with both the json 2.x and 3.0
|
|
5
|
+
interfaces: `NOSJ.parse`, `NOSJ.generate`, and the rest of nosj's own
|
|
6
|
+
API follow json 3.0 semantics, while the `nosj/json` drop-in follows
|
|
7
|
+
whichever json gem your application has installed, 2.x or 3.0, down to
|
|
8
|
+
its calling conventions and the documents it accepts. Upgrading json
|
|
9
|
+
(or not) is your choice; nosj works with either.
|
|
10
|
+
|
|
11
|
+
Behavior changes in nosj's own API, for input that was accepted
|
|
12
|
+
before:
|
|
13
|
+
|
|
14
|
+
- Duplicate object keys raise `NOSJ::ParserError` (positioned at the
|
|
15
|
+
object repeating the key, like json 3) in `parse`, `load_file`,
|
|
16
|
+
`valid?` (returns false), `minify`/`reformat`, and everything that
|
|
17
|
+
materializes values. `allow_duplicate_key: true` restores the old
|
|
18
|
+
behavior: the last value wins (in `minify`, repeated keys pass
|
|
19
|
+
through).
|
|
20
|
+
- Lone UTF-16 surrogates such as `"\udc00"` raise `NOSJ::ParserError`
|
|
21
|
+
everywhere, trailing ones included (they used to decode to raw
|
|
22
|
+
WTF-8 bytes, and `minify` re-escaped them).
|
|
23
|
+
- `generate` raises `NOSJ::GeneratorError` for keys that render alike
|
|
24
|
+
(`{"a" => 1, :a => 2}`, `{1 => 1, "1" => 2}`), with json 3's exact
|
|
25
|
+
message; `allow_duplicate_key: true` emits them as before. Hashes
|
|
26
|
+
whose keys are all of one kind are never checked. The Rails encoder
|
|
27
|
+
is unchanged.
|
|
28
|
+
- `stats` still describes such documents rather than refusing them.
|
|
29
|
+
- Unknown options raise ArgumentError with json 3's message
|
|
30
|
+
(`unknown keyword: foo`) in every entry point, instead of being
|
|
31
|
+
ignored. json options nosj does not implement (`object_class`,
|
|
32
|
+
`array_class`, `decimal_class`, `on_load`, `create_additions`,
|
|
33
|
+
`allow_comments`, `allow_control_characters`,
|
|
34
|
+
`allow_invalid_escape`, `sort_keys`, `as_json`) raise unless falsy;
|
|
35
|
+
`on_load` and the newer ones used to be silently ignored.
|
|
36
|
+
`escape_slash` is gone, as in json 3: use `script_safe`.
|
|
37
|
+
`quirks_mode` is no longer accepted. `stats` takes only the options
|
|
38
|
+
it documents (`max_nesting`, `allow_nan`, `allow_trailing_comma`).
|
|
39
|
+
|
|
40
|
+
The `nosj/json` drop-in follows the installed json gem:
|
|
41
|
+
|
|
42
|
+
- With json 3.0, `JSON.parse` takes keyword options only, `JSON.dump`
|
|
43
|
+
uses json 3's defaults (nesting capped at 100), and whatever json 3
|
|
44
|
+
refuses (`quirks_mode`, `escape_slash`, `create_additions`, unknown
|
|
45
|
+
options, positional option hashes) raises exactly as json 3 raises.
|
|
46
|
+
- With json 2.x, everything behaves as before, including json 2's
|
|
47
|
+
acceptance of duplicate keys, lone surrogates, and comments, and
|
|
48
|
+
its handling of keys that render alike.
|
|
49
|
+
- Whenever the fast path refuses a call, the installed gem runs it
|
|
50
|
+
again and decides: exceptions are now the gem's own (message,
|
|
51
|
+
`json_path`, `invalid_object`) rather than nosj's messages re-raised
|
|
52
|
+
as JSON classes. The second pass happens on failures only, and a
|
|
53
|
+
`generate` run twice this way calls `to_json` again on the objects
|
|
54
|
+
visited before the refusal.
|
|
55
|
+
- Fixed: `JSON.dump` raised NameError (`_dump_default_options`) with
|
|
56
|
+
json older than 2.11, which includes the json bundled with Ruby 3.3
|
|
57
|
+
and 3.4.
|
|
58
|
+
- Fixed: `JSON.generate` (and `pretty_generate`, `dump`) with both
|
|
59
|
+
`ascii_only` and `script_safe` raised ArgumentError; that combination,
|
|
60
|
+
which nosj does not implement, now goes to the gem.
|
|
61
|
+
|
|
1
62
|
## [0.4.1] - 2026-09-25
|
|
2
63
|
|
|
3
64
|
- Fixed a crash in `NOSJ.generate`: an object whose `to_json` or
|
data/Cargo.lock
CHANGED
data/README.md
CHANGED
|
@@ -65,6 +65,10 @@ through nosj:
|
|
|
65
65
|
require "nosj/json"
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
It works with json 2.x and 3.0 alike and follows whichever your app
|
|
69
|
+
has installed: its calling conventions, its defaults, and the documents
|
|
70
|
+
it accepts.
|
|
71
|
+
|
|
68
72
|
In a Bundler app (Rails included) that can live entirely in the
|
|
69
73
|
Gemfile you can do this:
|
|
70
74
|
|
|
@@ -108,9 +112,11 @@ The `json` gem API, on the `NOSJ` module:
|
|
|
108
112
|
|
|
109
113
|
```ruby
|
|
110
114
|
NOSJ.parse(src, symbolize_names: true) # also: freeze, max_nesting,
|
|
111
|
-
# allow_nan, allow_trailing_comma
|
|
115
|
+
# allow_nan, allow_trailing_comma,
|
|
116
|
+
# allow_duplicate_key
|
|
112
117
|
NOSJ.generate(obj) # indent, space, object_nl, ...,
|
|
113
|
-
NOSJ.pretty_generate(obj) # ascii_only, script_safe, strict
|
|
118
|
+
NOSJ.pretty_generate(obj) # ascii_only, script_safe, strict,
|
|
119
|
+
# allow_duplicate_key
|
|
114
120
|
```
|
|
115
121
|
|
|
116
122
|
### Lazy documents
|
|
@@ -189,10 +195,11 @@ NOSJ.reformat_file("big.json") # straight off a memory map
|
|
|
189
195
|
```
|
|
190
196
|
|
|
191
197
|
Output is exactly `generate(parse(json))`—canonical number spellings,
|
|
192
|
-
normalized escapes, same formatting options—
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`
|
|
198
|
+
normalized escapes, same formatting options—and it accepts exactly what
|
|
199
|
+
`parse` does. Acceptance options apply too:
|
|
200
|
+
`minify(src, allow_trailing_comma: true)` normalizes the commas away,
|
|
201
|
+
and under `allow_duplicate_key: true` repeated keys pass through (a
|
|
202
|
+
reformatter must not silently drop data).
|
|
196
203
|
|
|
197
204
|
### Byte-splicing edits and JSON Patch
|
|
198
205
|
|
|
@@ -403,19 +410,28 @@ Reproduce with `rake bench` (the parity-gated comparison, after a PGO retrain—
|
|
|
403
410
|
|
|
404
411
|
## Switching from the json gem
|
|
405
412
|
|
|
406
|
-
You mostly don't have to do anything.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
+
You mostly don't have to do anything. `NOSJ.*` follows json 3.0,
|
|
414
|
+
whichever json your app runs; the `nosj/json` drop-in follows the
|
|
415
|
+
installed one, 2.x or 3.0. Some differences:
|
|
416
|
+
|
|
417
|
+
- json 3.0 semantics: duplicate keys, lone surrogates, JS comments,
|
|
418
|
+
and invalid UTF-8 are errors (`allow_duplicate_key: true` restores
|
|
419
|
+
last-key-wins), keys that render alike raise in `generate`, and
|
|
420
|
+
unknown options raise ArgumentError. With json 2.x installed, the
|
|
421
|
+
drop-in keeps json 2's leniency: when nosj refuses a call, the
|
|
422
|
+
installed gem runs it and has the last word.
|
|
423
|
+
- The json options nosj doesn't implement (`create_additions`,
|
|
424
|
+
`object_class`, `array_class`, `decimal_class`, `on_load`,
|
|
425
|
+
`allow_comments`, `allow_control_characters`,
|
|
426
|
+
`allow_invalid_escape`, `sort_keys`, `as_json`) raise ArgumentError
|
|
427
|
+
unless falsy; the drop-in passes them to the original gem.
|
|
413
428
|
- Unlike `Array#dig`, negative indices in `NOSJ.dig` return nil (JSON
|
|
414
429
|
Pointer has no equivalent).
|
|
415
430
|
- Parse errors raise `NOSJ::ParserError` (`NOSJ::NestingError` past
|
|
416
431
|
`max_nesting`, like the gem); messages use byte offsets rather than
|
|
417
432
|
the gem's phrasing, and the exception carries `#line`, `#column`,
|
|
418
|
-
and a caret `#snippet`.
|
|
433
|
+
and a caret `#snippet`. Through the drop-in, exceptions are the
|
|
434
|
+
installed gem's own (message, `json_path`, `invalid_object`).
|
|
419
435
|
|
|
420
436
|
Everything else—including the gem's exact float formatting, which is
|
|
421
437
|
not the shortest-round-trip form most libraries emit—matches
|
data/ext/nosj/Cargo.toml
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# required by lib/nosj.rb as "nosj/nosj" (Init_nosj set explicitly in
|
|
6
6
|
# lib.rs). Gem name, module, and API are plain nosj.
|
|
7
7
|
name = "nosj_native"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.5.0"
|
|
9
9
|
edition = "2021"
|
|
10
10
|
authors = ["Yaroslav Markin <yaroslav@markin.net>"]
|
|
11
11
|
license = "MIT"
|
|
@@ -48,7 +48,14 @@ module NOSJFuzz
|
|
|
48
48
|
rescue *PARSE_FAIL
|
|
49
49
|
false
|
|
50
50
|
end
|
|
51
|
-
|
|
51
|
+
# stats describes documents rather than refusing them: it accepts
|
|
52
|
+
# everything parse accepts, plus duplicate keys and lone surrogates
|
|
53
|
+
# (json 3 refusals parse makes on top of the grammar).
|
|
54
|
+
raise "stats refused what parse accepted" if status == :ok && !stats_ok
|
|
55
|
+
if status == :err && stats_ok &&
|
|
56
|
+
try_parse(s, {allow_duplicate_key: true})[0] != :ok && !s.include?("\\u")
|
|
57
|
+
raise "stats accepted what parse refused for a grammar reason"
|
|
58
|
+
end
|
|
52
59
|
|
|
53
60
|
min_status, min = begin
|
|
54
61
|
[:ok, NOSJ.reformat_native(s, nil)]
|
|
@@ -59,24 +66,17 @@ module NOSJFuzz
|
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
if status == :err
|
|
62
|
-
# The pipe may abort with GeneratorError (
|
|
63
|
-
#
|
|
64
|
-
#
|
|
69
|
+
# The pipe may abort with GeneratorError (a non-finite float)
|
|
70
|
+
# before the parser reaches whatever made the whole document
|
|
71
|
+
# unparseable; any refusal is a refusal.
|
|
65
72
|
raise "reformat accepted what parse refused" unless [:err, :generator].include?(min_status)
|
|
66
73
|
return
|
|
67
74
|
end
|
|
68
75
|
if min_status == :generator
|
|
69
76
|
# On a document parse accepts, only an overflow-to-Infinity
|
|
70
|
-
# float (1e999, a 300-digit integer with a small exponent
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
# allow_nan separates them: it lifts the float refusal but not
|
|
74
|
-
# the key refusal, and the key requires a \u escape.
|
|
75
|
-
begin
|
|
76
|
-
NOSJ.reformat_native(s, {allow_nan: true})
|
|
77
|
-
rescue NOSJ::GeneratorError
|
|
78
|
-
raise "GeneratorError without a \\u escape in source" unless s.include?("\\u")
|
|
79
|
-
end
|
|
77
|
+
# float (1e999, a 300-digit integer with a small exponent) may
|
|
78
|
+
# abort the pipe, and allow_nan lifts exactly that refusal.
|
|
79
|
+
stage("reformat with allow_nan") { NOSJ.reformat_native(s, {allow_nan: true}) }
|
|
80
80
|
return
|
|
81
81
|
end
|
|
82
82
|
raise "reformat refused what parse accepted" unless min_status == :ok
|
|
@@ -140,11 +140,9 @@ module NOSJFuzz
|
|
|
140
140
|
raise "yielded values diverge from per-line parses" unless yielded.eql?(reference)
|
|
141
141
|
|
|
142
142
|
if status == :ok && !yielded.empty?
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return # WTF-8 strings (lone surrogates) are not generable
|
|
147
|
-
end
|
|
143
|
+
# Parsed values are always generable: parse refuses lone
|
|
144
|
+
# surrogates, the only source of unencodable strings.
|
|
145
|
+
ndjson = stage("generate_lines") { NOSJ.generate_lines_native(yielded, nil) }
|
|
148
146
|
back = []
|
|
149
147
|
NOSJ.each_line_native(ndjson, nil) { |v| back << v }
|
|
150
148
|
raise "generate_lines does not round-trip" unless back.eql?(yielded)
|
|
@@ -168,16 +166,11 @@ module NOSJFuzz
|
|
|
168
166
|
doc = utf8(doc_bytes)
|
|
169
167
|
spec_status, spec = try_parse(utf8(spec_bytes))
|
|
170
168
|
return unless spec_status == :ok
|
|
171
|
-
# Replacement values with broken encoding (lone surrogates) raise
|
|
172
|
-
# GeneratorError on insertion; the reference cannot mirror that.
|
|
173
|
-
return unless deep_valid_encoding?(spec)
|
|
174
169
|
|
|
170
|
+
# Parsing refuses duplicate keys (raw-byte resolution would see the
|
|
171
|
+
# first occurrence, the tree the last) and lone surrogates, so an
|
|
172
|
+
# accepted tree and spec are free of both.
|
|
175
173
|
tree_status, tree = try_parse(doc)
|
|
176
|
-
# Duplicate keys: raw-byte resolution sees the first occurrence,
|
|
177
|
-
# tree materialization keeps the last; the two sides cannot agree.
|
|
178
|
-
if tree_status == :ok && NOSJ.stats_native(doc, nil)[:keys] != count_keys(tree)
|
|
179
|
-
return
|
|
180
|
-
end
|
|
181
174
|
|
|
182
175
|
case spec
|
|
183
176
|
when Hash
|
|
@@ -380,21 +373,4 @@ module NOSJFuzz
|
|
|
380
373
|
else v
|
|
381
374
|
end
|
|
382
375
|
end
|
|
383
|
-
|
|
384
|
-
def deep_valid_encoding?(v)
|
|
385
|
-
case v
|
|
386
|
-
when String then v.valid_encoding?
|
|
387
|
-
when Array then v.all? { |e| deep_valid_encoding?(e) }
|
|
388
|
-
when Hash then v.all? { |k, e| deep_valid_encoding?(k) && deep_valid_encoding?(e) }
|
|
389
|
-
else true
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def count_keys(v)
|
|
394
|
-
case v
|
|
395
|
-
when Hash then v.size + v.sum { |_, e| count_keys(e) }
|
|
396
|
-
when Array then v.sum { |e| count_keys(e) }
|
|
397
|
-
else 0
|
|
398
|
-
end
|
|
399
|
-
end
|
|
400
376
|
end
|
data/ext/nosj/src/gen/opts.rs
CHANGED
|
@@ -5,6 +5,9 @@ use magnus::value::ReprValue;
|
|
|
5
5
|
use magnus::{Error, RHash, RString, Ruby, Value};
|
|
6
6
|
use nosj::emit::EscapeMode;
|
|
7
7
|
|
|
8
|
+
use crate::opt_reader::{Opt, OptReader};
|
|
9
|
+
use crate::sink::MAX_NESTING;
|
|
10
|
+
|
|
8
11
|
pub(crate) struct GenConfig {
|
|
9
12
|
pub(crate) indent: Vec<u8>,
|
|
10
13
|
pub(crate) space: Vec<u8>,
|
|
@@ -21,195 +24,129 @@ pub(crate) struct GenConfig {
|
|
|
21
24
|
/// null (Float#as_json parity). Set only by the Rails entry, never
|
|
22
25
|
/// from user option hashes.
|
|
23
26
|
pub(super) rails: bool,
|
|
27
|
+
/// json 3: keys that render the same (`"a"` and `:a`) raise unless
|
|
28
|
+
/// this is set. The Rails configs keep ActiveSupport's own handling.
|
|
29
|
+
pub(super) allow_duplicate_key: bool,
|
|
24
30
|
pub(crate) mode: EscapeMode,
|
|
25
31
|
/// Precomputed "any formatting string set": scanning the five
|
|
26
32
|
/// vectors per call was measurable on tiny documents.
|
|
27
33
|
pub(super) pretty: bool,
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/// The json gem's defaults under an escape mode, for the plain walk or
|
|
37
|
+
/// the Rails encoder's (which keeps ActiveSupport's own key handling,
|
|
38
|
+
/// so it allows keys that render alike). A const fn because statics
|
|
39
|
+
/// cannot struct-update a type with `Vec` fields; `Vec::new` is const
|
|
40
|
+
/// and allocation-free.
|
|
41
|
+
const fn defaults(rails: bool, mode: EscapeMode) -> GenConfig {
|
|
42
|
+
GenConfig {
|
|
43
|
+
indent: Vec::new(),
|
|
44
|
+
space: Vec::new(),
|
|
45
|
+
space_before: Vec::new(),
|
|
46
|
+
object_nl: Vec::new(),
|
|
47
|
+
array_nl: Vec::new(),
|
|
48
|
+
max_nesting: MAX_NESTING,
|
|
49
|
+
start_depth: 0,
|
|
50
|
+
allow_nan: false,
|
|
51
|
+
strict: false,
|
|
52
|
+
rails,
|
|
53
|
+
allow_duplicate_key: rails,
|
|
54
|
+
mode,
|
|
55
|
+
pretty: false,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
/// The nil-options configuration, shared instead of rebuilt: stamping
|
|
31
60
|
/// a fresh ~140-byte GenConfig onto the stack per call was measurable
|
|
32
61
|
/// on tiny documents (the json gem likewise reuses a cached State for
|
|
33
|
-
/// the default options). Safe as a static:
|
|
34
|
-
///
|
|
35
|
-
pub(crate) static DEFAULT_CONFIG: GenConfig =
|
|
36
|
-
indent: Vec::new(),
|
|
37
|
-
space: Vec::new(),
|
|
38
|
-
space_before: Vec::new(),
|
|
39
|
-
object_nl: Vec::new(),
|
|
40
|
-
array_nl: Vec::new(),
|
|
41
|
-
max_nesting: 100,
|
|
42
|
-
start_depth: 0,
|
|
43
|
-
allow_nan: false,
|
|
44
|
-
strict: false,
|
|
45
|
-
rails: false,
|
|
46
|
-
mode: EscapeMode::Standard,
|
|
47
|
-
pretty: false,
|
|
48
|
-
};
|
|
62
|
+
/// the default options). Safe as a static: generation only ever
|
|
63
|
+
/// borrows the config.
|
|
64
|
+
pub(crate) static DEFAULT_CONFIG: GenConfig = defaults(false, EscapeMode::Standard);
|
|
49
65
|
|
|
50
66
|
/// The Rails-encoder configuration for ActiveSupport's default escape
|
|
51
67
|
/// flags (HTML entities and JS separators both on, the overwhelmingly
|
|
52
68
|
/// common case): escaping is fused into the crate's HtmlSafe kernels,
|
|
53
69
|
/// one pass, no post-scan.
|
|
54
|
-
pub(super) static RAILS_HTML_SAFE_CONFIG: GenConfig =
|
|
55
|
-
indent: Vec::new(),
|
|
56
|
-
space: Vec::new(),
|
|
57
|
-
space_before: Vec::new(),
|
|
58
|
-
object_nl: Vec::new(),
|
|
59
|
-
array_nl: Vec::new(),
|
|
60
|
-
max_nesting: 100,
|
|
61
|
-
start_depth: 0,
|
|
62
|
-
allow_nan: false,
|
|
63
|
-
strict: false,
|
|
64
|
-
rails: true,
|
|
65
|
-
mode: EscapeMode::HtmlSafe,
|
|
66
|
-
pretty: false,
|
|
67
|
-
};
|
|
70
|
+
pub(super) static RAILS_HTML_SAFE_CONFIG: GenConfig = defaults(true, EscapeMode::HtmlSafe);
|
|
68
71
|
|
|
69
72
|
/// Rails-encoder configuration with HTML entities on and JS separators
|
|
70
73
|
/// off.
|
|
71
|
-
pub(super) static RAILS_HTML_ENTITIES_CONFIG: GenConfig =
|
|
72
|
-
indent: Vec::new(),
|
|
73
|
-
space: Vec::new(),
|
|
74
|
-
space_before: Vec::new(),
|
|
75
|
-
object_nl: Vec::new(),
|
|
76
|
-
array_nl: Vec::new(),
|
|
77
|
-
max_nesting: 100,
|
|
78
|
-
start_depth: 0,
|
|
79
|
-
allow_nan: false,
|
|
80
|
-
strict: false,
|
|
81
|
-
rails: true,
|
|
82
|
-
mode: EscapeMode::HtmlEntities,
|
|
83
|
-
pretty: false,
|
|
84
|
-
};
|
|
74
|
+
pub(super) static RAILS_HTML_ENTITIES_CONFIG: GenConfig = defaults(true, EscapeMode::HtmlEntities);
|
|
85
75
|
|
|
86
76
|
/// Rails-encoder configuration with JS separators on and HTML entities
|
|
87
77
|
/// off.
|
|
88
|
-
pub(super) static RAILS_JS_SEPARATORS_CONFIG: GenConfig =
|
|
89
|
-
indent: Vec::new(),
|
|
90
|
-
space: Vec::new(),
|
|
91
|
-
space_before: Vec::new(),
|
|
92
|
-
object_nl: Vec::new(),
|
|
93
|
-
array_nl: Vec::new(),
|
|
94
|
-
max_nesting: 100,
|
|
95
|
-
start_depth: 0,
|
|
96
|
-
allow_nan: false,
|
|
97
|
-
strict: false,
|
|
98
|
-
rails: true,
|
|
99
|
-
mode: EscapeMode::JsSeparators,
|
|
100
|
-
pretty: false,
|
|
101
|
-
};
|
|
78
|
+
pub(super) static RAILS_JS_SEPARATORS_CONFIG: GenConfig = defaults(true, EscapeMode::JsSeparators);
|
|
102
79
|
|
|
103
80
|
/// The Rails-encoder configuration with every escape flag off
|
|
104
81
|
/// (encode(escape: false)). Mirrors JSONGemEncoder#stringify, which
|
|
105
82
|
/// generates with the json gem's defaults.
|
|
106
|
-
pub(super) static RAILS_CONFIG: GenConfig =
|
|
107
|
-
indent: Vec::new(),
|
|
108
|
-
space: Vec::new(),
|
|
109
|
-
space_before: Vec::new(),
|
|
110
|
-
object_nl: Vec::new(),
|
|
111
|
-
array_nl: Vec::new(),
|
|
112
|
-
max_nesting: 100,
|
|
113
|
-
start_depth: 0,
|
|
114
|
-
allow_nan: false,
|
|
115
|
-
strict: false,
|
|
116
|
-
rails: true,
|
|
117
|
-
mode: EscapeMode::Standard,
|
|
118
|
-
pretty: false,
|
|
119
|
-
};
|
|
83
|
+
pub(super) static RAILS_CONFIG: GenConfig = defaults(true, EscapeMode::Standard);
|
|
120
84
|
|
|
121
85
|
impl Default for GenConfig {
|
|
122
86
|
fn default() -> Self {
|
|
123
|
-
|
|
124
|
-
indent: Vec::new(),
|
|
125
|
-
space: Vec::new(),
|
|
126
|
-
space_before: Vec::new(),
|
|
127
|
-
object_nl: Vec::new(),
|
|
128
|
-
array_nl: Vec::new(),
|
|
129
|
-
max_nesting: 100,
|
|
130
|
-
start_depth: 0,
|
|
131
|
-
allow_nan: false,
|
|
132
|
-
strict: false,
|
|
133
|
-
rails: false,
|
|
134
|
-
mode: EscapeMode::Standard,
|
|
135
|
-
pretty: false,
|
|
136
|
-
}
|
|
87
|
+
defaults(false, EscapeMode::Standard)
|
|
137
88
|
}
|
|
138
89
|
}
|
|
139
90
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return Ok(None);
|
|
146
|
-
}
|
|
91
|
+
/// A formatting string option's bytes; empty when absent or nil.
|
|
92
|
+
fn opt_bytes(r: &mut OptReader, opt: Opt) -> Result<Vec<u8>, Error> {
|
|
93
|
+
let Some(v) = r.get(opt).filter(|v| !v.is_nil()) else {
|
|
94
|
+
return Ok(Vec::new());
|
|
95
|
+
};
|
|
147
96
|
let s = RString::from_value(v).ok_or_else(|| {
|
|
148
97
|
Error::new(
|
|
149
|
-
ruby.exception_type_error(),
|
|
150
|
-
format!("{
|
|
98
|
+
r.ruby().exception_type_error(),
|
|
99
|
+
format!("{} must be a String", opt.name()),
|
|
151
100
|
)
|
|
152
101
|
})?;
|
|
153
|
-
Ok(
|
|
102
|
+
Ok(unsafe { s.as_slice() }.to_vec())
|
|
154
103
|
}
|
|
155
104
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
None
|
|
160
|
-
} else {
|
|
161
|
-
Some(v.to_bool())
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/// Decode a non-nil options hash (nil takes [`DEFAULT_CONFIG`] at the
|
|
166
|
-
/// call site without constructing anything).
|
|
105
|
+
/// Decode a generate options hash (nil takes [`DEFAULT_CONFIG`] at the
|
|
106
|
+
/// call site without constructing anything); keys it does not read
|
|
107
|
+
/// raise ArgumentError, like json 3.
|
|
167
108
|
pub(crate) fn parse_gen_opts(ruby: &Ruby, opts: Value) -> Result<(GenConfig, usize), Error> {
|
|
168
|
-
let mut cfg = GenConfig::default();
|
|
169
|
-
let mut cap_hint = 0usize;
|
|
170
109
|
if opts.is_nil() {
|
|
171
|
-
return Ok((
|
|
110
|
+
return Ok((GenConfig::default(), 0));
|
|
172
111
|
}
|
|
173
112
|
let opts = RHash::from_value(opts)
|
|
174
113
|
.ok_or_else(|| Error::new(ruby.exception_type_error(), "options must be a Hash or nil"))?;
|
|
114
|
+
let mut reader = OptReader::new(ruby, opts);
|
|
115
|
+
let decoded = read_gen_opts(&mut reader)?;
|
|
116
|
+
reader.finish()?;
|
|
117
|
+
Ok(decoded)
|
|
118
|
+
}
|
|
175
119
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if let Some(v) = opt_bool(ruby, opts, "strict") {
|
|
195
|
-
cfg.strict = v;
|
|
196
|
-
}
|
|
197
|
-
let ascii = opt_bool(ruby, opts, "ascii_only").unwrap_or(false);
|
|
198
|
-
let script = opt_bool(ruby, opts, "script_safe").unwrap_or(false)
|
|
199
|
-
|| opt_bool(ruby, opts, "escape_slash").unwrap_or(false);
|
|
120
|
+
/// Read the json 3 generate options and the buffer size hint. sort_keys
|
|
121
|
+
/// and as_json, which NOSJ does not implement, raise unless falsy.
|
|
122
|
+
pub(crate) fn read_gen_opts(r: &mut OptReader) -> Result<(GenConfig, usize), Error> {
|
|
123
|
+
let mut cfg = GenConfig {
|
|
124
|
+
indent: opt_bytes(r, Opt::Indent)?,
|
|
125
|
+
space: opt_bytes(r, Opt::Space)?,
|
|
126
|
+
space_before: opt_bytes(r, Opt::SpaceBefore)?,
|
|
127
|
+
object_nl: opt_bytes(r, Opt::ObjectNl)?,
|
|
128
|
+
array_nl: opt_bytes(r, Opt::ArrayNl)?,
|
|
129
|
+
allow_nan: r.truthy(Opt::AllowNan),
|
|
130
|
+
strict: r.truthy(Opt::Strict),
|
|
131
|
+
allow_duplicate_key: r.truthy(Opt::AllowDuplicateKey),
|
|
132
|
+
..GenConfig::default()
|
|
133
|
+
};
|
|
134
|
+
let mut cap_hint = 0usize;
|
|
135
|
+
r.tolerate(&[Opt::SortKeys, Opt::AsJson]);
|
|
136
|
+
let ascii = r.truthy(Opt::AsciiOnly);
|
|
137
|
+
let script = r.truthy(Opt::ScriptSafe);
|
|
200
138
|
if ascii {
|
|
201
139
|
cfg.mode = EscapeMode::AsciiOnly;
|
|
202
140
|
if script {
|
|
203
141
|
return Err(Error::new(
|
|
204
|
-
ruby.exception_arg_error(),
|
|
142
|
+
r.ruby().exception_arg_error(),
|
|
205
143
|
"NOSJ.generate: ascii_only and script_safe cannot be combined",
|
|
206
144
|
));
|
|
207
145
|
}
|
|
208
146
|
} else if script {
|
|
209
147
|
cfg.mode = EscapeMode::ScriptSafe;
|
|
210
148
|
}
|
|
211
|
-
if let Some(v) =
|
|
212
|
-
let v: Value = v;
|
|
149
|
+
if let Some(v) = r.get(Opt::MaxNesting) {
|
|
213
150
|
// nil/false → unlimited; true → keep the default 100; Integer → limit.
|
|
214
151
|
if !v.to_bool() {
|
|
215
152
|
cfg.max_nesting = 0;
|
|
@@ -217,14 +154,12 @@ pub(crate) fn parse_gen_opts(ruby: &Ruby, opts: Value) -> Result<(GenConfig, usi
|
|
|
217
154
|
cfg.max_nesting = if n <= 0 { 0 } else { n as usize };
|
|
218
155
|
}
|
|
219
156
|
}
|
|
220
|
-
if let Some(v) =
|
|
221
|
-
let v: Value = v;
|
|
157
|
+
if let Some(v) = r.get(Opt::Depth) {
|
|
222
158
|
if let Ok(n) = <i64 as magnus::TryConvert>::try_convert(v) {
|
|
223
159
|
cfg.start_depth = if n <= 0 { 0 } else { n as usize };
|
|
224
160
|
}
|
|
225
161
|
}
|
|
226
|
-
if let Some(v) =
|
|
227
|
-
let v: Value = v;
|
|
162
|
+
if let Some(v) = r.get(Opt::BufferInitialLength) {
|
|
228
163
|
if let Ok(n) = <i64 as magnus::TryConvert>::try_convert(v) {
|
|
229
164
|
if n > 0 {
|
|
230
165
|
cap_hint = n as usize;
|
data/ext/nosj/src/gen/ruby.rs
CHANGED
|
@@ -18,6 +18,11 @@ pub(super) fn protected_to_s(v: VALUE) -> Result<VALUE, Error> {
|
|
|
18
18
|
magnus::rb_sys::protect(|| unsafe { rb_sys::rb_obj_as_string(v) })
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/// `v.inspect`, protected (user code for any element's `inspect`).
|
|
22
|
+
pub(super) fn protected_inspect(v: VALUE) -> Result<VALUE, Error> {
|
|
23
|
+
magnus::rb_sys::protect(|| unsafe { rb_sys::rb_inspect(v) })
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
/// `v.to_json`, protected.
|
|
22
27
|
pub(super) fn protected_to_json(v: VALUE) -> Result<VALUE, Error> {
|
|
23
28
|
magnus::rb_sys::protect(|| unsafe { rb_sys::rb_funcall(v, to_json_id(), 0) })
|