nosj 0.3.2 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/Cargo.lock +41 -41
- data/README.md +19 -1
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- data/ext/nosj/src/errors.rs +3 -1
- data/ext/nosj/src/files.rs +21 -20
- data/ext/nosj/src/gen/errors.rs +12 -10
- data/ext/nosj/src/gen/mod.rs +50 -49
- data/ext/nosj/src/gen/ruby.rs +54 -22
- data/ext/nosj/src/gen/walker.rs +56 -73
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lib.rs +17 -0
- data/ext/nosj/src/lines.rs +27 -17
- data/ext/nosj/src/parse.rs +10 -6
- data/ext/nosj/src/patch.rs +25 -15
- data/ext/nosj/src/pointer.rs +9 -11
- data/ext/nosj/src/reformat.rs +39 -25
- data/ext/nosj/src/sink.rs +13 -16
- data/ext/nosj/src/state.rs +60 -14
- data/ext/nosj/src/stats.rs +2 -3
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +2 -1
- metadata +2 -2
data/ext/nosj/src/gen/mod.rs
CHANGED
|
@@ -29,20 +29,25 @@ mod walker;
|
|
|
29
29
|
|
|
30
30
|
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
31
31
|
use magnus::{Error, RString, Ruby, Value};
|
|
32
|
-
use std::cell::
|
|
32
|
+
use std::cell::Cell;
|
|
33
33
|
|
|
34
|
+
use crate::state::with_taken;
|
|
34
35
|
use errors::raise_fail;
|
|
35
36
|
use keys::GenKeyCache;
|
|
37
|
+
pub(crate) use ruby::warm_up;
|
|
36
38
|
use walker::Gen;
|
|
37
39
|
|
|
38
40
|
/// Per-thread generate scratch: the pooled output buffer (capacity
|
|
39
|
-
/// survives across calls) and the pre-escaped key
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
///
|
|
43
|
-
///
|
|
44
|
-
///
|
|
45
|
-
///
|
|
41
|
+
/// survives across calls) and the pre-escaped key caches. A call takes
|
|
42
|
+
/// the scratch OUT of the thread-local and stores it back afterwards:
|
|
43
|
+
/// off the main Ractor, Ruby threads run M:N over a pool of native
|
|
44
|
+
/// threads, so a thread resuming after a `to_json` / `as_json` callback
|
|
45
|
+
/// may be on another native thread, where a reference into the old
|
|
46
|
+
/// thread's cell would alias whatever that thread is running. An empty
|
|
47
|
+
/// cell (a recursive generate holds the scratch, or a thread hop left
|
|
48
|
+
/// it elsewhere) means a fresh one. The Box moves as one pointer; the
|
|
49
|
+
/// cost over a plain borrow is one thread-local address lookup per
|
|
50
|
+
/// call, measured at parity (feature/ractor, 2026-09).
|
|
46
51
|
struct GenScratch {
|
|
47
52
|
buf: Vec<u8>,
|
|
48
53
|
keys: GenKeyCache,
|
|
@@ -52,12 +57,28 @@ struct GenScratch {
|
|
|
52
57
|
html_keys: GenKeyCache,
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
impl GenScratch {
|
|
61
|
+
#[cold]
|
|
62
|
+
fn fresh() -> Box<Self> {
|
|
63
|
+
Box::new(GenScratch {
|
|
64
|
+
buf: Vec::new(),
|
|
65
|
+
keys: GenKeyCache::with_capacity(256),
|
|
66
|
+
html_keys: GenKeyCache::with_capacity(64),
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
thread_local! {
|
|
56
|
-
static GEN_SCRATCH:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
static GEN_SCRATCH: Cell<Option<Box<GenScratch>>> = const { Cell::new(None) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Run `f` on this thread's scratch (see [`with_taken`]). A recursive
|
|
76
|
+
/// generate stores its own fresh scratch meanwhile; the outermost call's
|
|
77
|
+
/// warm one replaces it (the replaced key cache keeps its shadow).
|
|
78
|
+
fn with_scratch<R>(f: impl FnOnce(&mut GenScratch) -> R) -> R {
|
|
79
|
+
with_taken(&GEN_SCRATCH, |slot| {
|
|
80
|
+
f(slot.get_or_insert_with(GenScratch::fresh))
|
|
81
|
+
})
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
/// `NOSJ.generate(obj, opts = nil)`, registered as a variadic native
|
|
@@ -174,27 +195,18 @@ fn generate_scratched_into<R>(
|
|
|
174
195
|
cap_hint: usize,
|
|
175
196
|
finish: impl FnOnce(&Ruby, &[u8]) -> Result<R, Error>,
|
|
176
197
|
) -> Result<R, Error> {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
keys
|
|
190
|
-
};
|
|
191
|
-
generate_with(ruby, obj, cfg, cap_hint, buf, keys, finish)
|
|
192
|
-
}
|
|
193
|
-
Err(_) => {
|
|
194
|
-
let mut buf = Vec::new();
|
|
195
|
-
let mut keys = GenKeyCache::default();
|
|
196
|
-
generate_with(ruby, obj, cfg, cap_hint, &mut buf, &mut keys, finish)
|
|
197
|
-
}
|
|
198
|
+
with_scratch(|scratch| {
|
|
199
|
+
let GenScratch {
|
|
200
|
+
buf,
|
|
201
|
+
keys,
|
|
202
|
+
html_keys,
|
|
203
|
+
} = scratch;
|
|
204
|
+
let keys = if cfg.mode == nosj::emit::EscapeMode::HtmlSafe {
|
|
205
|
+
html_keys
|
|
206
|
+
} else {
|
|
207
|
+
keys
|
|
208
|
+
};
|
|
209
|
+
generate_with(ruby, obj, cfg, cap_hint, buf, keys, finish)
|
|
198
210
|
})
|
|
199
211
|
}
|
|
200
212
|
|
|
@@ -258,10 +270,7 @@ pub(crate) fn emit_into(
|
|
|
258
270
|
cfg: &opts::GenConfig,
|
|
259
271
|
out: &mut Vec<u8>,
|
|
260
272
|
) -> Result<(), Error> {
|
|
261
|
-
|
|
262
|
-
Ok(mut scratch) => emit_one(ruby, obj, cfg, out, &mut scratch.keys),
|
|
263
|
-
Err(_) => emit_one(ruby, obj, cfg, out, &mut GenKeyCache::default()),
|
|
264
|
-
})
|
|
273
|
+
with_scratch(|scratch| emit_one(ruby, obj, cfg, out, &mut scratch.keys))
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
/// Formatting strings holding a newline (or carriage return) would
|
|
@@ -313,17 +322,9 @@ pub(crate) fn generate_lines_bytes_into<R>(
|
|
|
313
322
|
"formatting options containing newlines would break JSON Lines framing",
|
|
314
323
|
));
|
|
315
324
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
let GenScratch { buf, keys, .. } = scratch;
|
|
320
|
-
generate_lines_with(ruby, values, cfg, cap_hint, buf, keys, finish)
|
|
321
|
-
}
|
|
322
|
-
Err(_) => {
|
|
323
|
-
let mut buf = Vec::new();
|
|
324
|
-
let mut keys = GenKeyCache::default();
|
|
325
|
-
generate_lines_with(ruby, values, cfg, cap_hint, &mut buf, &mut keys, finish)
|
|
326
|
-
}
|
|
325
|
+
with_scratch(|scratch| {
|
|
326
|
+
let GenScratch { buf, keys, .. } = scratch;
|
|
327
|
+
generate_lines_with(ruby, values, cfg, cap_hint, buf, keys, finish)
|
|
327
328
|
})
|
|
328
329
|
}
|
|
329
330
|
|
data/ext/nosj/src/gen/ruby.rs
CHANGED
|
@@ -23,6 +23,21 @@ pub(super) fn protected_to_json(v: VALUE) -> Result<VALUE, Error> {
|
|
|
23
23
|
magnus::rb_sys::protect(|| unsafe { rb_sys::rb_funcall(v, to_json_id(), 0) })
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/// `v.to_json` if `v.respond_to?(:to_json)`, else `None`, under ONE
|
|
27
|
+
/// protect: `rb_respond_to` dispatches to a user-defined `respond_to?` /
|
|
28
|
+
/// `respond_to_missing?`, which may raise just like `to_json` itself.
|
|
29
|
+
pub(super) fn protected_to_json_if_responds(v: VALUE) -> Result<Option<VALUE>, Error> {
|
|
30
|
+
const QUNDEF: VALUE = ruby_special_consts::RUBY_Qundef as VALUE;
|
|
31
|
+
let json = magnus::rb_sys::protect(|| unsafe {
|
|
32
|
+
if rb_sys::rb_respond_to(v, to_json_id()) != 0 {
|
|
33
|
+
rb_sys::rb_funcall(v, to_json_id(), 0)
|
|
34
|
+
} else {
|
|
35
|
+
QUNDEF
|
|
36
|
+
}
|
|
37
|
+
})?;
|
|
38
|
+
Ok((json != QUNDEF).then_some(json))
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
/// `v.as_json`, protected. Argument-less on purpose: ActiveSupport's
|
|
27
42
|
/// JSONGemEncoder#jsonify recursion also calls as_json without
|
|
28
43
|
/// options (only the top-level value receives them).
|
|
@@ -37,6 +52,15 @@ fn as_json_id() -> rb_sys::ID {
|
|
|
37
52
|
as rb_sys::ID
|
|
38
53
|
}
|
|
39
54
|
|
|
55
|
+
/// Resolve the `OnceLock`s here now (init, main Ractor): an initializer
|
|
56
|
+
/// that enters the VM must never race between Ractors (see lib.rs), and
|
|
57
|
+
/// an initialized lock never blocks again.
|
|
58
|
+
pub(crate) fn warm_up() {
|
|
59
|
+
as_json_id();
|
|
60
|
+
to_json_id();
|
|
61
|
+
utf8_encindexes();
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
/// Whether `v` is a `JSON::Fragment` (pre-rendered JSON to splice
|
|
41
65
|
/// verbatim: the gem accepts fragments even under `strict`, and
|
|
42
66
|
/// ActiveSupport's encoder passes them through). The class is resolved
|
|
@@ -44,33 +68,37 @@ fn as_json_id() -> rb_sys::ID {
|
|
|
44
68
|
/// first generate is still found; a fragment instance existing implies
|
|
45
69
|
/// its class does. The cached VALUE is a constant of the JSON module,
|
|
46
70
|
/// so it can never be collected.
|
|
47
|
-
pub(super) fn is_json_fragment(v: VALUE) -> bool {
|
|
71
|
+
pub(super) fn is_json_fragment(v: VALUE) -> Result<bool, Error> {
|
|
48
72
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
49
73
|
static FRAGMENT: AtomicUsize = AtomicUsize::new(0);
|
|
50
|
-
let mut cls = FRAGMENT.load(Ordering::Relaxed);
|
|
74
|
+
let mut cls = FRAGMENT.load(Ordering::Relaxed) as VALUE;
|
|
51
75
|
if cls == 0 {
|
|
52
|
-
cls = resolve_json_fragment()
|
|
76
|
+
cls = resolve_json_fragment()?;
|
|
53
77
|
if cls == 0 {
|
|
54
|
-
return false;
|
|
78
|
+
return Ok(false);
|
|
55
79
|
}
|
|
56
|
-
FRAGMENT.store(cls, Ordering::Relaxed);
|
|
80
|
+
FRAGMENT.store(cls as usize, Ordering::Relaxed);
|
|
57
81
|
}
|
|
58
|
-
unsafe { rb_sys::rb_obj_is_kind_of(v, cls
|
|
82
|
+
Ok(unsafe { rb_sys::rb_obj_is_kind_of(v, cls) != QFALSE })
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
/// `JSON::Fragment`, or 0 while undefined. Only the `rb_const_get`s are
|
|
86
|
+
/// protected: fetching a constant can run its pending autoload (user
|
|
87
|
+
/// code, whose raise propagates as any constant reference's would),
|
|
88
|
+
/// while `rb_const_defined` never loads anything.
|
|
89
|
+
fn resolve_json_fragment() -> Result<VALUE, Error> {
|
|
62
90
|
unsafe {
|
|
63
91
|
let object = rb_sys::rb_cObject;
|
|
64
92
|
let json_id = rb_sys::rb_intern(c"JSON".as_ptr());
|
|
65
93
|
if rb_sys::rb_const_defined(object, json_id) == 0 {
|
|
66
|
-
return 0;
|
|
94
|
+
return Ok(0);
|
|
67
95
|
}
|
|
68
|
-
let json = rb_sys::rb_const_get(object, json_id)
|
|
96
|
+
let json = magnus::rb_sys::protect(|| rb_sys::rb_const_get(object, json_id))?;
|
|
69
97
|
let fragment_id = rb_sys::rb_intern(c"Fragment".as_ptr());
|
|
70
98
|
if rb_sys::rb_const_defined(json, fragment_id) == 0 {
|
|
71
|
-
return 0;
|
|
99
|
+
return Ok(0);
|
|
72
100
|
}
|
|
73
|
-
rb_sys::rb_const_get(json, fragment_id)
|
|
101
|
+
magnus::rb_sys::protect(|| rb_sys::rb_const_get(json, fragment_id))
|
|
74
102
|
}
|
|
75
103
|
}
|
|
76
104
|
|
|
@@ -86,7 +114,7 @@ pub(super) fn protected_encode_utf8(v: VALUE) -> Result<VALUE, Error> {
|
|
|
86
114
|
}
|
|
87
115
|
|
|
88
116
|
/// Interned `to_json` method ID, resolved once per process.
|
|
89
|
-
|
|
117
|
+
fn to_json_id() -> rb_sys::ID {
|
|
90
118
|
static TO_JSON: OnceLock<usize> = OnceLock::new();
|
|
91
119
|
*TO_JSON.get_or_init(|| unsafe { rb_sys::rb_intern(c"to_json".as_ptr()) } as usize)
|
|
92
120
|
as rb_sys::ID
|
|
@@ -100,12 +128,16 @@ pub(super) fn utf8_encindexes() -> (c_int, c_int) {
|
|
|
100
128
|
// Coderange and encoding index live in RBasic flags (public ABI); reading
|
|
101
129
|
// them inline instead of calling rb_enc_str_coderange / rb_enc_get_index is
|
|
102
130
|
// how the gem avoids two C calls per string (RB_ENC_CODERANGE,
|
|
103
|
-
// RB_ENCODING_GET_INLINED).
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
131
|
+
// RB_ENCODING_GET_INLINED). The bit layout comes from rb-sys's bindings,
|
|
132
|
+
// generated from the headers of the Ruby being built against, so it
|
|
133
|
+
// follows any layout change instead of silently misreading flags.
|
|
134
|
+
const CR_MASK: u64 = rb_sys::ruby_coderange_type::RUBY_ENC_CODERANGE_MASK as u64;
|
|
135
|
+
pub(super) const CR_7BIT: u64 = rb_sys::ruby_coderange_type::RUBY_ENC_CODERANGE_7BIT as u64;
|
|
136
|
+
pub(super) const CR_VALID: u64 = rb_sys::ruby_coderange_type::RUBY_ENC_CODERANGE_VALID as u64;
|
|
137
|
+
const ENC_SHIFT: u64 = rb_sys::ruby_encoding_consts::RUBY_ENCODING_SHIFT as u64;
|
|
138
|
+
const ENC_MASK: u64 = rb_sys::ruby_encoding_consts::RUBY_ENCODING_MASK as u64;
|
|
139
|
+
/// Inline encoding-index sentinel: the real index is stored out of line.
|
|
140
|
+
const ENC_INLINE_MAX: c_int = rb_sys::ruby_encoding_consts::RUBY_ENCODING_INLINE_MAX as c_int;
|
|
109
141
|
|
|
110
142
|
#[inline(always)]
|
|
111
143
|
pub(super) fn str_coderange(s: VALUE) -> u64 {
|
|
@@ -122,18 +154,18 @@ pub(super) fn str_coderange(s: VALUE) -> u64 {
|
|
|
122
154
|
pub(super) fn str_enc_index(s: VALUE) -> c_int {
|
|
123
155
|
let flags = unsafe { (*(s as *const rb_sys::RBasic)).flags };
|
|
124
156
|
let idx = ((flags & ENC_MASK) >> ENC_SHIFT) as c_int;
|
|
125
|
-
if idx ==
|
|
126
|
-
// RUBY_ENCODING_INLINE_MAX sentinel: index stored out of line.
|
|
157
|
+
if idx == ENC_INLINE_MAX {
|
|
127
158
|
unsafe { rb_sys::rb_enc_get_index(s) }
|
|
128
159
|
} else {
|
|
129
160
|
idx
|
|
130
161
|
}
|
|
131
162
|
}
|
|
132
163
|
|
|
164
|
+
/// `RB_SPECIAL_CONST_P` (immediates plus Qnil/Qfalse), through rb-sys's
|
|
165
|
+
/// inline versioned stable API rather than a hand-copied bit test.
|
|
133
166
|
#[inline(always)]
|
|
134
167
|
pub(super) fn is_special_const(v: VALUE) -> bool {
|
|
135
|
-
|
|
136
|
-
(v & (ruby_special_consts::RUBY_IMMEDIATE_MASK as VALUE)) != 0 || v == QNIL || v == QFALSE
|
|
168
|
+
rb_sys::macros::SPECIAL_CONST_P(v)
|
|
137
169
|
}
|
|
138
170
|
|
|
139
171
|
/// Borrow a Ruby String's bytes.
|
data/ext/nosj/src/gen/walker.rs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//! Compact and pretty modes are one const-generic body, so the compact
|
|
4
4
|
//! hot path carries no formatting branches.
|
|
5
5
|
|
|
6
|
+
use magnus::Error;
|
|
6
7
|
use nosj::emit::{self, copy_short_raw, EscapeMode};
|
|
7
8
|
use rb_sys::macros::{
|
|
8
9
|
FIX2LONG, FIXNUM_P, FLONUM_P, RARRAY_CONST_PTR, RARRAY_LEN, RB_BUILTIN_TYPE, RHASH_SIZE,
|
|
@@ -15,8 +16,8 @@ use super::keys::GenKeyCache;
|
|
|
15
16
|
use super::opts::GenConfig;
|
|
16
17
|
use super::ruby::{
|
|
17
18
|
is_json_fragment, is_special_const, protected_as_json, protected_encode_utf8,
|
|
18
|
-
protected_to_json, protected_to_s, rstring_bytes, str_coderange,
|
|
19
|
-
utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL, QTRUE,
|
|
19
|
+
protected_to_json, protected_to_json_if_responds, protected_to_s, rstring_bytes, str_coderange,
|
|
20
|
+
str_enc_index, utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL, QTRUE,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
/// Whether keys escaped under `mode` may be cached: the cached bytes
|
|
@@ -265,13 +266,15 @@ impl Gen<'_> {
|
|
|
265
266
|
let s = unsafe { rb_sys::rb_sym2str(k) };
|
|
266
267
|
return self.emit_rstring_quoted(s);
|
|
267
268
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
let s = self.reraise(protected_to_s(k))?;
|
|
270
|
+
self.emit_rstring_quoted(s)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// A protected Ruby call's outcome inside the walk: a raised
|
|
274
|
+
/// exception becomes this walk's failure, re-raised unchanged once
|
|
275
|
+
/// the walk has unwound.
|
|
276
|
+
fn reraise<T>(&mut self, r: Result<T, Error>) -> Result<T, ()> {
|
|
277
|
+
r.map_err(|exc| self.fail = Some(GenFail::Reraise(exc)))
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
/// Non-native type: strict raises (except `JSON::Fragment`, which
|
|
@@ -285,7 +288,7 @@ impl Gen<'_> {
|
|
|
285
288
|
return self.emit_rails_fallback::<PRETTY>(raw, depth);
|
|
286
289
|
}
|
|
287
290
|
if self.cfg.strict {
|
|
288
|
-
if is_json_fragment(raw) {
|
|
291
|
+
if self.reraise(is_json_fragment(raw))? {
|
|
289
292
|
return self.splice_to_json(raw);
|
|
290
293
|
}
|
|
291
294
|
let name = unsafe {
|
|
@@ -296,53 +299,32 @@ impl Gen<'_> {
|
|
|
296
299
|
self.fail = Some(GenFail::Generator(format!("{name} not allowed in JSON")));
|
|
297
300
|
return Err(());
|
|
298
301
|
}
|
|
299
|
-
if
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
self.append_rstring_raw(json);
|
|
306
|
-
return Ok(());
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
Err(exc) => {
|
|
310
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
311
|
-
return Err(());
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
match protected_to_s(raw) {
|
|
316
|
-
Ok(s) => self.emit_rstring_quoted(s),
|
|
317
|
-
Err(exc) => {
|
|
318
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
319
|
-
Err(())
|
|
302
|
+
if let Some(json) = self.reraise(protected_to_json_if_responds(raw))? {
|
|
303
|
+
if !is_special_const(json)
|
|
304
|
+
&& unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING
|
|
305
|
+
{
|
|
306
|
+
self.append_rstring_raw(json);
|
|
307
|
+
return Ok(());
|
|
320
308
|
}
|
|
321
309
|
}
|
|
310
|
+
let s = self.reraise(protected_to_s(raw))?;
|
|
311
|
+
self.emit_rstring_quoted(s)
|
|
322
312
|
}
|
|
323
313
|
|
|
324
314
|
/// Splice `raw`'s `to_json` result verbatim: the JSON::Fragment
|
|
325
315
|
/// path (pre-rendered JSON, trusted like the gem trusts it).
|
|
326
316
|
fn splice_to_json(&mut self, raw: VALUE) -> Result<(), ()> {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
Ok(_) => {
|
|
336
|
-
self.fail = Some(GenFail::Generator(
|
|
337
|
-
"JSON::Fragment#to_json did not return a String".to_string(),
|
|
338
|
-
));
|
|
339
|
-
Err(())
|
|
340
|
-
}
|
|
341
|
-
Err(exc) => {
|
|
342
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
343
|
-
Err(())
|
|
344
|
-
}
|
|
317
|
+
let json = self.reraise(protected_to_json(raw))?;
|
|
318
|
+
if is_special_const(json)
|
|
319
|
+
|| unsafe { RB_BUILTIN_TYPE(json) } != ruby_value_type::RUBY_T_STRING
|
|
320
|
+
{
|
|
321
|
+
self.fail = Some(GenFail::Generator(
|
|
322
|
+
"JSON::Fragment#to_json did not return a String".to_string(),
|
|
323
|
+
));
|
|
324
|
+
return Err(());
|
|
345
325
|
}
|
|
326
|
+
self.append_rstring_raw(json);
|
|
327
|
+
Ok(())
|
|
346
328
|
}
|
|
347
329
|
|
|
348
330
|
/// Rails-mode fallback, mirroring JSONGemEncoder#jsonify:
|
|
@@ -356,27 +338,22 @@ impl Gen<'_> {
|
|
|
356
338
|
raw: VALUE,
|
|
357
339
|
depth: usize,
|
|
358
340
|
) -> Result<(), ()> {
|
|
359
|
-
if is_json_fragment(raw) {
|
|
341
|
+
if self.reraise(is_json_fragment(raw))? {
|
|
360
342
|
return self.splice_to_json(raw);
|
|
361
343
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
Ok(json) => self.emit_value::<PRETTY>(json, depth),
|
|
375
|
-
Err(exc) => {
|
|
376
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
377
|
-
Err(())
|
|
378
|
-
}
|
|
344
|
+
let json = self.reraise(protected_as_json(raw))?;
|
|
345
|
+
if json == raw {
|
|
346
|
+
let name = unsafe {
|
|
347
|
+
std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
|
|
348
|
+
.to_string_lossy()
|
|
349
|
+
.into_owned()
|
|
350
|
+
};
|
|
351
|
+
self.fail = Some(GenFail::Generator(format!(
|
|
352
|
+
"{name}#as_json returned the receiver"
|
|
353
|
+
)));
|
|
354
|
+
return Err(());
|
|
379
355
|
}
|
|
356
|
+
self.emit_value::<PRETTY>(json, depth)
|
|
380
357
|
}
|
|
381
358
|
|
|
382
359
|
fn nesting_check(&mut self, inner: usize) -> Result<(), ()> {
|
|
@@ -390,14 +367,23 @@ impl Gen<'_> {
|
|
|
390
367
|
fn emit_array<const PRETTY: bool>(&mut self, ary: VALUE, depth: usize) -> Result<(), ()> {
|
|
391
368
|
let inner = depth + 1;
|
|
392
369
|
self.nesting_check(inner)?;
|
|
393
|
-
let len = unsafe { RARRAY_LEN(ary) } as usize;
|
|
394
370
|
self.out.push(b'[');
|
|
395
|
-
if
|
|
371
|
+
if unsafe { RARRAY_LEN(ary) } == 0 {
|
|
396
372
|
self.out.push(b']');
|
|
397
373
|
return Ok(());
|
|
398
374
|
}
|
|
399
375
|
let mut i = 0usize;
|
|
400
|
-
|
|
376
|
+
loop {
|
|
377
|
+
// Length and pointer are re-read every element, like the
|
|
378
|
+
// json gem: a user callback inside the recursion (to_json,
|
|
379
|
+
// to_s, as_json) may shrink the array (slots past the live
|
|
380
|
+
// length hold freed or reused VALUEs; growth is emitted),
|
|
381
|
+
// and any allocation may compact it elsewhere.
|
|
382
|
+
let len = unsafe { RARRAY_LEN(ary) } as usize;
|
|
383
|
+
if i >= len {
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
let elem = unsafe { *RARRAY_CONST_PTR(ary).add(i) };
|
|
401
387
|
if i > 0 {
|
|
402
388
|
self.out.push(b',');
|
|
403
389
|
}
|
|
@@ -405,9 +391,6 @@ impl Gen<'_> {
|
|
|
405
391
|
self.out.extend_from_slice(&self.cfg.array_nl);
|
|
406
392
|
self.push_indent(inner);
|
|
407
393
|
}
|
|
408
|
-
// Re-read the pointer every element: an allocation inside the
|
|
409
|
-
// recursion may trigger GC compaction and move the array.
|
|
410
|
-
let elem = unsafe { *RARRAY_CONST_PTR(ary).add(i) };
|
|
411
394
|
// Numeric runs (compact mode) emit through a raw local
|
|
412
395
|
// cursor under one chunked reservation: per-element Vec
|
|
413
396
|
// operations round-trip length and pointer through memory
|