nosj 0.2.0 → 0.3.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.
data/ext/nosj/src/lazy.rs CHANGED
@@ -17,7 +17,8 @@ use magnus::typed_data::Obj;
17
17
  use magnus::value::ReprValue;
18
18
  use magnus::{DataTypeFunctions, Error, RArray, RString, Ruby, TypedData, Value};
19
19
 
20
- use crate::parse::{err, materialize, parse_native_opts, utf8_input, ParseNativeOpts};
20
+ use crate::errors::parser_error_at;
21
+ use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
21
22
  use crate::pointer::{path_to_pointer, push_escaped_token};
22
23
  use crate::state::PULL_STATE;
23
24
 
@@ -105,20 +106,19 @@ impl LazyNode {
105
106
  /// Wrap a resolved raw-value slice: containers become new lazy nodes,
106
107
  /// scalars materialize now. `sub` must be a subslice of `doc.bytes`.
107
108
  fn resolved_to_value(ruby: &Ruby, doc: &Arc<DocInner>, sub: &[u8]) -> Result<Value, Error> {
109
+ let (start, end) = span_of(doc.bytes(), sub);
108
110
  match sub.first().copied() {
109
111
  Some(k) if k == KIND_OBJECT || k == KIND_ARRAY => {
110
- let base = doc.bytes().as_ptr() as usize;
111
- let start = sub.as_ptr() as usize - base;
112
112
  let node = LazyNode {
113
113
  doc: Arc::clone(doc),
114
114
  start,
115
- end: start + sub.len(),
115
+ end,
116
116
  kind: k,
117
117
  };
118
118
  let obj: Obj<LazyNode> = ruby.obj_wrap(node);
119
119
  Ok(obj.as_value())
120
120
  }
121
- _ => materialize(ruby, sub, &doc.opts),
121
+ _ => materialize_at(ruby, doc.bytes(), start, end, &doc.opts),
122
122
  }
123
123
  }
124
124
 
@@ -139,7 +139,7 @@ fn resolve_in_span(ruby: &Ruby, node: &LazyNode, pointer: &str) -> Result<Value,
139
139
  Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
140
140
  Err(Error::new(ruby.exception_arg_error(), e.to_string()))
141
141
  }
142
- Err(e) => Err(err(ruby, e.to_string())),
142
+ Err(e) => Err(reader_err(ruby, node, e)),
143
143
  }
144
144
  }
145
145
 
@@ -188,7 +188,12 @@ pub(crate) fn wrap_root(
188
188
  let doc = Arc::new(DocInner { bytes, opts });
189
189
  let input = doc.bytes();
190
190
  let Some(start) = input.iter().position(|b| !WS.contains(b)) else {
191
- return Err(err(ruby, "unexpected end of input".into()));
191
+ return Err(parser_error_at(
192
+ ruby,
193
+ input,
194
+ input.len(),
195
+ "unexpected end of input".into(),
196
+ ));
192
197
  };
193
198
  let end = input.iter().rposition(|b| !WS.contains(b)).unwrap() + 1;
194
199
  let sub = &doc.bytes()[start..end];
@@ -245,7 +250,13 @@ pub fn lazy_at_pointer(
245
250
  /// `__materialize`: the whole span as plain Ruby values, under the
246
251
  /// document's parse options.
247
252
  pub fn lazy_materialize(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<Value, Error> {
248
- materialize(ruby, rb_self.span(), &rb_self.doc.opts)
253
+ materialize_at(
254
+ ruby,
255
+ rb_self.doc.bytes(),
256
+ rb_self.start,
257
+ rb_self.end,
258
+ &rb_self.doc.opts,
259
+ )
249
260
  }
250
261
 
251
262
  /// `__kind`: `:object` or `:array`.
@@ -261,8 +272,11 @@ pub fn lazy_byte_size(_ruby: &Ruby, rb_self: Obj<LazyNode>) -> usize {
261
272
  rb_self.end - rb_self.start
262
273
  }
263
274
 
264
- fn reader_err(ruby: &Ruby, e: nosj::ParseError) -> Error {
265
- err(ruby, e.to_string())
275
+ /// A walk failure inside `node`'s span: the reader's offset is
276
+ /// span-relative, so shift by the node's start to report an absolute
277
+ /// document position.
278
+ fn reader_err(ruby: &Ruby, node: &LazyNode, e: nosj::ParseError) -> Error {
279
+ parser_error_at(ruby, node.doc.bytes(), node.start + e.offset, e.to_string())
266
280
  }
267
281
 
268
282
  /// `__keys`: the object's decoded keys, one Reader walk, values skipped.
@@ -278,8 +292,11 @@ pub fn lazy_keys(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
278
292
  let mut state = cell.borrow_mut();
279
293
  // SAFETY: spans are valid UTF-8 (see resolve_in_span).
280
294
  let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
281
- r.next_node().map_err(|e| reader_err(ruby, e))?;
282
- let mut has = match r.object_first_key().map_err(|e| reader_err(ruby, e))? {
295
+ r.next_node().map_err(|e| reader_err(ruby, &rb_self, e))?;
296
+ let mut has = match r
297
+ .object_first_key()
298
+ .map_err(|e| reader_err(ruby, &rb_self, e))?
299
+ {
283
300
  Some(k) => {
284
301
  out.push(ruby.str_new(k))?;
285
302
  true
@@ -287,8 +304,11 @@ pub fn lazy_keys(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
287
304
  None => false,
288
305
  };
289
306
  while has {
290
- r.skip_value().map_err(|e| reader_err(ruby, e))?;
291
- has = match r.object_next_key().map_err(|e| reader_err(ruby, e))? {
307
+ r.skip_value().map_err(|e| reader_err(ruby, &rb_self, e))?;
308
+ has = match r
309
+ .object_next_key()
310
+ .map_err(|e| reader_err(ruby, &rb_self, e))?
311
+ {
292
312
  Some(k) => {
293
313
  out.push(ruby.str_new(k))?;
294
314
  true
@@ -308,27 +328,27 @@ pub fn lazy_size(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<usize, Error> {
308
328
  let mut state = cell.borrow_mut();
309
329
  // SAFETY: spans are valid UTF-8 (see resolve_in_span).
310
330
  let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
311
- r.next_node().map_err(|e| reader_err(ruby, e))?;
331
+ r.next_node().map_err(|e| reader_err(ruby, &rb_self, e))?;
312
332
  let mut n = 0usize;
313
333
  if rb_self.kind == KIND_OBJECT {
314
334
  let mut has = r
315
335
  .object_first_key()
316
- .map_err(|e| reader_err(ruby, e))?
336
+ .map_err(|e| reader_err(ruby, &rb_self, e))?
317
337
  .is_some();
318
338
  while has {
319
339
  n += 1;
320
- r.skip_value().map_err(|e| reader_err(ruby, e))?;
340
+ r.skip_value().map_err(|e| reader_err(ruby, &rb_self, e))?;
321
341
  has = r
322
342
  .object_next_key()
323
- .map_err(|e| reader_err(ruby, e))?
343
+ .map_err(|e| reader_err(ruby, &rb_self, e))?
324
344
  .is_some();
325
345
  }
326
346
  } else {
327
- let mut has = r.array_first().map_err(|e| reader_err(ruby, e))?;
347
+ let mut has = r.array_first().map_err(|e| reader_err(ruby, &rb_self, e))?;
328
348
  while has {
329
349
  n += 1;
330
- r.skip_value().map_err(|e| reader_err(ruby, e))?;
331
- has = r.array_next().map_err(|e| reader_err(ruby, e))?;
350
+ r.skip_value().map_err(|e| reader_err(ruby, &rb_self, e))?;
351
+ has = r.array_next().map_err(|e| reader_err(ruby, &rb_self, e))?;
332
352
  }
333
353
  }
334
354
  Ok(n)
@@ -383,7 +403,7 @@ pub fn lazy_children(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Erro
383
403
  }
384
404
  Ok(out)
385
405
  });
386
- let descs = descs.map_err(|e| reader_err(ruby, e))?;
406
+ let descs = descs.map_err(|e| reader_err(ruby, &rb_self, e))?;
387
407
 
388
408
  let out = ruby.ary_new_capa(descs.len());
389
409
  for d in descs {
data/ext/nosj/src/lib.rs CHANGED
@@ -9,6 +9,8 @@
9
9
  //! demand over shared document bytes).
10
10
  //! - `files.rs`: file entry points (load_file, write_file, and the
11
11
  //! mmap-backed load_lazy_file / at_pointer_file / dig_file).
12
+ //! - `stats.rs`: document statistics (NOSJ.stats / stats_file) from a
13
+ //! counting sink pass.
12
14
  //! - `sink.rs`: the VALUE-building and validation sinks with their
13
15
  //! interned-key caches.
14
16
  //! - `state.rs`: per-thread reusable state and the GC-marked value
@@ -16,13 +18,18 @@
16
18
  //! - `gen/`: generation (JSON.generate-compatible walker, options,
17
19
  //! key cache, protect shims, error mapping).
18
20
 
21
+ pub mod errors;
19
22
  pub mod files;
20
23
  pub mod gen;
21
24
  pub mod lazy;
25
+ pub mod lines;
22
26
  pub mod parse;
27
+ pub mod patch;
23
28
  pub mod pointer;
29
+ pub mod reformat;
24
30
  pub mod sink;
25
31
  pub mod state;
32
+ pub mod stats;
26
33
 
27
34
  use magnus::{method, prelude::*, Error, Ruby};
28
35
 
@@ -57,6 +64,25 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
57
64
  method!(files::at_pointer_file_native, 3),
58
65
  )?;
59
66
  module.define_singleton_method("dig_file_native", method!(files::dig_file_native, 2))?;
67
+ module.define_singleton_method("stats_native", method!(stats::stats_native, 2))?;
68
+ module.define_singleton_method("stats_file_native", method!(stats::stats_file_native, 2))?;
69
+ module.define_singleton_method("each_line_native", method!(lines::each_line_native, 2))?;
70
+ module.define_singleton_method(
71
+ "each_line_file_native",
72
+ method!(lines::each_line_file_native, 2),
73
+ )?;
74
+ module.define_singleton_method(
75
+ "generate_lines_native",
76
+ method!(gen::generate_lines_native, 2),
77
+ )?;
78
+ module.define_singleton_method("write_lines_native", method!(files::write_lines_native, 3))?;
79
+ module.define_singleton_method("splice_native", method!(patch::splice_native, 3))?;
80
+ module.define_singleton_method("patch_native", method!(patch::patch_native, 3))?;
81
+ module.define_singleton_method("reformat_native", method!(reformat::reformat_native, 2))?;
82
+ module.define_singleton_method(
83
+ "reformat_file_native",
84
+ method!(reformat::reformat_file_native, 2),
85
+ )?;
60
86
  let lazy_class = module.define_class("Lazy", ruby.class_object())?;
61
87
  // Nodes are only born from NOSJ.lazy / lazy resolution.
62
88
  lazy_class.undef_default_alloc_func();
@@ -70,6 +96,10 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
70
96
  lazy_class.define_method("__size", method!(lazy::lazy_size, 0))?;
71
97
  lazy_class.define_method("__children", method!(lazy::lazy_children, 0))?;
72
98
  module.define_singleton_method("generate_native", method!(gen::generate_native, 2))?;
99
+ module.define_singleton_method(
100
+ "generate_rails_native",
101
+ method!(gen::generate_rails_native, 3),
102
+ )?;
73
103
  // `generate` itself is native and variadic: the json gem routes
74
104
  // its `generate` through a Ruby frame into C, so skipping our own
75
105
  // forwarder frame is a straight per-call win on small documents.
@@ -0,0 +1,90 @@
1
+ //! NDJSON / JSON Lines: `NOSJ.each_line` walks a newline-delimited
2
+ //! document yielding one parsed value per line, and the file form does
3
+ //! it over a read-only memory map. A raw newline can never occur
4
+ //! INSIDE a JSON value (control characters must be escaped), so line
5
+ //! splitting is exact framing, and one-value-per-line is what the
6
+ //! format requires: a second value on a line fails as trailing
7
+ //! garbage, with an absolute document position on the error.
8
+
9
+ use magnus::value::ReprValue;
10
+ use magnus::{Error, RString, Ruby, Value};
11
+
12
+ use crate::files::with_mapped_file;
13
+ use crate::parse::{materialize_at, parse_native_opts, utf8_input, ParseNativeOpts};
14
+
15
+ /// Blank-line whitespace (the newline itself is the separator). Lines
16
+ /// holding only this are skipped, per the NDJSON convention that
17
+ /// parsers ignore empty lines (trailing newlines at EOF are universal).
18
+ const LINE_WS: [u8; 3] = *b" \t\r";
19
+
20
+ fn blank(line: &[u8]) -> bool {
21
+ line.iter().all(|b| LINE_WS.contains(b))
22
+ }
23
+
24
+ /// Yield one parsed value per non-blank line. Each line parses through
25
+ /// the shared sink machinery against the FULL source, so a malformed
26
+ /// line raises the rich ParserError whose `#line` is the physical
27
+ /// NDJSON line number. The thread state is borrowed per line, never
28
+ /// across a yield: the block is free to call back into NOSJ.
29
+ fn walk_lines(ruby: &Ruby, source: &[u8], o: &ParseNativeOpts) -> Result<(), Error> {
30
+ let mut pos = 0;
31
+ while pos < source.len() {
32
+ let line_end = source[pos..]
33
+ .iter()
34
+ .position(|&b| b == b'\n')
35
+ .map_or(source.len(), |p| pos + p);
36
+ if !blank(&source[pos..line_end]) {
37
+ let value = materialize_at(ruby, source, pos, line_end, o)?;
38
+ let _: Value = ruby.yield_value(value)?;
39
+ }
40
+ pos = line_end + 1;
41
+ }
42
+ Ok(())
43
+ }
44
+
45
+ /// `NOSJ.each_line(source, opts) { |value| }`: the Ruby wrapper
46
+ /// guarantees a block (and builds the Enumerator otherwise).
47
+ pub fn each_line_native(
48
+ ruby: &Ruby,
49
+ _rb_self: Value,
50
+ data: RString,
51
+ opts: Value,
52
+ ) -> Result<Value, Error> {
53
+ let o = parse_native_opts(ruby, opts)?;
54
+ let input = utf8_input(ruby, &data)?;
55
+ if data.as_value().is_frozen() {
56
+ // A frozen source cannot be mutated by the block, and `data`
57
+ // lives on this frame, so the borrow stays valid across yields.
58
+ walk_lines(ruby, input, &o)?;
59
+ } else {
60
+ // The block could mutate (or free the buffer of) an unfrozen
61
+ // source mid-iteration; walk a private copy. Same policy as
62
+ // NOSJ.lazy: pass a frozen string for zero-copy.
63
+ let owned = input.to_vec();
64
+ walk_lines(ruby, &owned, &o)?;
65
+ }
66
+ Ok(ruby.qnil().as_value())
67
+ }
68
+
69
+ /// `NOSJ.each_line_file(path, opts) { |value| }`: NDJSON over a
70
+ /// read-only memory map; the file never becomes a Ruby String.
71
+ pub fn each_line_file_native(
72
+ ruby: &Ruby,
73
+ _rb_self: Value,
74
+ path: RString,
75
+ opts: Value,
76
+ ) -> Result<Value, Error> {
77
+ let o = parse_native_opts(ruby, opts)?;
78
+ let p = path.to_string()?;
79
+ // An empty file is a valid, zero-event NDJSON stream, but mapping
80
+ // a zero-length file fails with EINVAL on Linux; answer before the
81
+ // mapper. Metadata failures (ENOENT, ...) fall through so the
82
+ // mapper raises the same Errno an open would.
83
+ if std::fs::metadata(&p).is_ok_and(|m| m.len() == 0) {
84
+ return Ok(ruby.qnil().as_value());
85
+ }
86
+ with_mapped_file(ruby, &p, |map| {
87
+ walk_lines(ruby, &map, &o)?;
88
+ Ok(ruby.qnil().as_value())
89
+ })
90
+ }
@@ -6,12 +6,18 @@
6
6
  use magnus::rb_sys::{AsRawValue, FromRawValue};
7
7
  use magnus::{Error, RString, Ruby, Value};
8
8
 
9
+ use crate::errors::{nesting_error, parser_error, parser_error_at};
9
10
  use crate::sink::{NullSink, RubyValueSink, SinkAbort, MAX_NESTING};
10
11
  use crate::state::{ensure_marked_shadow, PullState, PULL_STATE};
11
12
 
12
- #[cold]
13
- pub(crate) fn err(ruby: &Ruby, msg: String) -> Error {
14
- Error::new(ruby.exception_runtime_error(), msg)
13
+ pub(crate) use crate::errors::parser_error as err;
14
+
15
+ /// Byte range of `sub` within `source`. `sub` must be a subslice of
16
+ /// `source` (it always is here: pointer resolution and lazy spans hand
17
+ /// out slices borrowed from the document they resolved against).
18
+ pub(crate) fn span_of(source: &[u8], sub: &[u8]) -> (usize, usize) {
19
+ let start = sub.as_ptr() as usize - source.as_ptr() as usize;
20
+ (start, start + sub.len())
15
21
  }
16
22
 
17
23
  /// Validate that `data` is UTF-8 (or US-ASCII) with intact coderange and
@@ -107,12 +113,16 @@ pub(crate) fn parse_native_opts(ruby: &Ruby, opts: Value) -> Result<ParseNativeO
107
113
  }
108
114
 
109
115
  /// Pop the root value off the sink stack, or map a drive failure onto
110
- /// the gem's exceptions. Shared by every driver.
116
+ /// the gem's exceptions. Shared by every driver. `source`/`base` locate
117
+ /// the driven bytes within the full document, so ParserError positions
118
+ /// stay absolute when a subtree slice was parsed.
111
119
  fn finish_drive(
112
120
  ruby: &Ruby,
113
121
  result: Result<(), nosj::DriveError<SinkAbort>>,
114
122
  stack: &mut Vec<rb_sys::VALUE>,
115
123
  max_nesting: usize,
124
+ source: &[u8],
125
+ base: usize,
116
126
  ) -> Result<Value, Error> {
117
127
  match result {
118
128
  Ok(()) => {
@@ -122,23 +132,52 @@ fn finish_drive(
122
132
  Ok(unsafe { Value::from_raw(raw) })
123
133
  }
124
134
  Err(nosj::DriveError::Sink(SinkAbort::Overflow)) => {
125
- Err(err(ruby, "document too large".into()))
135
+ Err(parser_error(ruby, "document too large".into()))
126
136
  }
127
137
  Err(nosj::DriveError::Sink(SinkAbort::BadBigint)) => {
128
- Err(err(ruby, "invalid bignum".into()))
138
+ Err(parser_error(ruby, "invalid bignum".into()))
129
139
  }
130
- Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(err(
140
+ Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
131
141
  ruby,
132
142
  format!("nesting of {} is too deep", max_nesting.saturating_add(1)),
133
143
  )),
134
- Err(nosj::DriveError::Parse(e)) => Err(err(ruby, e.to_string())),
144
+ // Raised only by the reformat pipe's sink, which never drives
145
+ // through here; the match must stay total.
146
+ Err(nosj::DriveError::Sink(SinkAbort::BrokenUtf8Output)) => Err(parser_error(
147
+ ruby,
148
+ "source sequence is illegal/malformed utf-8".into(),
149
+ )),
150
+ // Also reformat-pipe-only, kept total for the same reason.
151
+ Err(nosj::DriveError::Sink(SinkAbort::NonFiniteFloat(spelling))) => Err(parser_error(
152
+ ruby,
153
+ format!("{spelling} not allowed in JSON"),
154
+ )),
155
+ Err(nosj::DriveError::Parse(e)) => Err(parser_error_at(
156
+ ruby,
157
+ source,
158
+ base + e.offset,
159
+ e.to_string(),
160
+ )),
135
161
  }
136
162
  }
137
163
 
138
- /// Drive the fused cursor over `input`, building Ruby values through the
139
- /// shared thread-local sink machinery. `input` must be valid UTF-8 (see
140
- /// [`utf8_input`]).
141
- pub(crate) fn materialize(ruby: &Ruby, input: &[u8], o: &ParseNativeOpts) -> Result<Value, Error> {
164
+ /// Drive the fused cursor over the whole of `source`. See
165
+ /// [`materialize_at`].
166
+ pub(crate) fn materialize(ruby: &Ruby, source: &[u8], o: &ParseNativeOpts) -> Result<Value, Error> {
167
+ materialize_at(ruby, source, 0, source.len(), o)
168
+ }
169
+
170
+ /// Drive the fused cursor over `source[start..end]`, building Ruby
171
+ /// values through the shared thread-local sink machinery. `source` must
172
+ /// be valid UTF-8 (see [`utf8_input`]); the full document is passed so
173
+ /// error positions come out absolute.
174
+ pub(crate) fn materialize_at(
175
+ ruby: &Ruby,
176
+ source: &[u8],
177
+ start: usize,
178
+ end: usize,
179
+ o: &ParseNativeOpts,
180
+ ) -> Result<Value, Error> {
142
181
  PULL_STATE.with(|cell| {
143
182
  let mut state = cell.borrow_mut();
144
183
  ensure_marked_shadow(&mut state.vstack);
@@ -166,8 +205,10 @@ pub(crate) fn materialize(ruby: &Ruby, input: &[u8], o: &ParseNativeOpts) -> Res
166
205
  };
167
206
 
168
207
  // Safety: callers verified UTF-8 (coderange or nosj slice).
169
- let result = unsafe { nosj::parse_utf8_unchecked_with(input, bufs, &mut sink, o.popts) };
170
- finish_drive(ruby, result, sink.stack, o.max_nesting)
208
+ let result = unsafe {
209
+ nosj::parse_utf8_unchecked_with(&source[start..end], bufs, &mut sink, o.popts)
210
+ };
211
+ finish_drive(ruby, result, sink.stack, o.max_nesting, source, start)
171
212
  })
172
213
  }
173
214