nosj 0.1.0 → 0.3.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 +110 -1
- data/Cargo.lock +13 -3
- data/README.md +272 -24
- data/ext/nosj/Cargo.toml +5 -2
- data/ext/nosj/src/errors.rs +171 -0
- data/ext/nosj/src/files.rs +240 -0
- data/ext/nosj/src/gen/errors.rs +2 -9
- data/ext/nosj/src/gen/mod.rs +198 -20
- data/ext/nosj/src/gen/opts.rs +87 -9
- data/ext/nosj/src/gen/ruby.rs +51 -0
- data/ext/nosj/src/gen/walker.rs +97 -59
- data/ext/nosj/src/lazy.rs +422 -0
- data/ext/nosj/src/lib.rs +60 -0
- data/ext/nosj/src/lines.rs +90 -0
- data/ext/nosj/src/parse.rs +50 -14
- data/ext/nosj/src/patch.rs +547 -0
- data/ext/nosj/src/pointer.rs +26 -17
- data/ext/nosj/src/reformat.rs +301 -0
- data/ext/nosj/src/sink.rs +6 -0
- data/ext/nosj/src/stats.rs +301 -0
- data/lib/nosj/json.rb +30 -6
- data/lib/nosj/lazy.rb +165 -0
- data/lib/nosj/multi_json.rb +1 -1
- data/lib/nosj/rails.rb +76 -0
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +434 -5
- data/sig/nosj.rbs +120 -0
- metadata +16 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
//! Parse-side exception raising. Malformed documents raise
|
|
2
|
+
//! `NOSJ::ParserError` carrying the failure position (`#byte_offset`,
|
|
3
|
+
//! `#line`, `#column`) and a caret snippet (`#snippet`); too-deep
|
|
4
|
+
//! nesting raises `NOSJ::NestingError`, matching gem json's classes.
|
|
5
|
+
//!
|
|
6
|
+
//! Everything here runs only when a parse has already failed: the
|
|
7
|
+
//! position pass over the source and the exception construction cost
|
|
8
|
+
//! nothing on the happy path, and the accessors on the Ruby side are
|
|
9
|
+
//! plain ivar reads.
|
|
10
|
+
|
|
11
|
+
use magnus::value::ReprValue;
|
|
12
|
+
use magnus::{Class, Error, ExceptionClass, Module, Object, RModule, RObject, Ruby};
|
|
13
|
+
|
|
14
|
+
/// Look up an exception class under NOSJ, falling back to RuntimeError
|
|
15
|
+
/// if the Ruby layer has not defined it (only possible when the native
|
|
16
|
+
/// extension is loaded without lib/nosj.rb).
|
|
17
|
+
pub(crate) fn nosj_exception(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
18
|
+
let lookup = || -> Result<ExceptionClass, Error> {
|
|
19
|
+
let m: RModule = ruby.define_module("NOSJ")?;
|
|
20
|
+
m.const_get(name)
|
|
21
|
+
};
|
|
22
|
+
lookup().unwrap_or_else(|_| ruby.exception_runtime_error())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// NOSJ::ParserError without position info (encoding refusals and
|
|
26
|
+
/// other failures that have no meaningful offset).
|
|
27
|
+
#[cold]
|
|
28
|
+
pub(crate) fn parser_error(ruby: &Ruby, msg: String) -> Error {
|
|
29
|
+
Error::new(nosj_exception(ruby, "ParserError"), msg)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// NOSJ::NestingError (parse side: message parity with gem json, which
|
|
33
|
+
/// raises JSON::NestingError when max_nesting is exceeded).
|
|
34
|
+
#[cold]
|
|
35
|
+
pub(crate) fn nesting_error(ruby: &Ruby, msg: String) -> Error {
|
|
36
|
+
Error::new(nosj_exception(ruby, "NestingError"), msg)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Plain RuntimeError, for failures that are not parse errors (the
|
|
40
|
+
/// unmappable-I/O fallback).
|
|
41
|
+
#[cold]
|
|
42
|
+
pub(crate) fn runtime_error(ruby: &Ruby, msg: String) -> Error {
|
|
43
|
+
Error::new(ruby.exception_runtime_error(), msg)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// NOSJ::ParserError enriched with the failure position: `offset` is a
|
|
47
|
+
/// byte offset into `source` (the full document, so positions stay
|
|
48
|
+
/// absolute even when the failing parse ran over a subtree slice).
|
|
49
|
+
#[cold]
|
|
50
|
+
pub(crate) fn parser_error_at(ruby: &Ruby, source: &[u8], offset: usize, msg: String) -> Error {
|
|
51
|
+
let class = nosj_exception(ruby, "ParserError");
|
|
52
|
+
let loc = locate(source, offset);
|
|
53
|
+
let Ok(exc) = class.new_instance((msg.as_str(),)) else {
|
|
54
|
+
return Error::new(class, msg);
|
|
55
|
+
};
|
|
56
|
+
// Exception instances are plain T_OBJECTs; magnus exposes ivar_set
|
|
57
|
+
// through RObject, not through its Exception wrapper.
|
|
58
|
+
if let Some(obj) = RObject::from_value(exc.as_value()) {
|
|
59
|
+
let set = |name: &str, v: usize| {
|
|
60
|
+
let _ = obj.ivar_set(name, v);
|
|
61
|
+
};
|
|
62
|
+
set("@byte_offset", offset.min(source.len()));
|
|
63
|
+
set("@line", loc.line);
|
|
64
|
+
set("@column", loc.column);
|
|
65
|
+
if let Some(snippet) = &loc.snippet {
|
|
66
|
+
let _ = obj.ivar_set("@snippet", snippet.as_str());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
Error::from(exc)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// A resolved source position: 1-based line, 1-based character column
|
|
73
|
+
/// within that line, and a two-line caret snippet (`None` when the
|
|
74
|
+
/// offending line is empty or not valid UTF-8).
|
|
75
|
+
struct Location {
|
|
76
|
+
line: usize,
|
|
77
|
+
column: usize,
|
|
78
|
+
snippet: Option<String>,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// UTF-8 continuation bytes are 0b10xxxxxx; every other byte starts a
|
|
82
|
+
/// character, so counting non-continuation bytes counts characters.
|
|
83
|
+
const UTF8_CONTINUATION_MASK: u8 = 0b1100_0000;
|
|
84
|
+
const UTF8_CONTINUATION_BITS: u8 = 0b1000_0000;
|
|
85
|
+
|
|
86
|
+
fn count_chars(bytes: &[u8]) -> usize {
|
|
87
|
+
bytes
|
|
88
|
+
.iter()
|
|
89
|
+
.filter(|&&b| b & UTF8_CONTINUATION_MASK != UTF8_CONTINUATION_BITS)
|
|
90
|
+
.count()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn locate(source: &[u8], offset: usize) -> Location {
|
|
94
|
+
let off = offset.min(source.len());
|
|
95
|
+
let line = 1 + count_newlines(&source[..off]);
|
|
96
|
+
let line_start = source[..off]
|
|
97
|
+
.iter()
|
|
98
|
+
.rposition(|&b| b == b'\n')
|
|
99
|
+
.map_or(0, |p| p + 1);
|
|
100
|
+
let mut line_end = source[off..]
|
|
101
|
+
.iter()
|
|
102
|
+
.position(|&b| b == b'\n')
|
|
103
|
+
.map_or(source.len(), |p| off + p);
|
|
104
|
+
if line_end > line_start && source[line_end - 1] == b'\r' {
|
|
105
|
+
line_end -= 1;
|
|
106
|
+
}
|
|
107
|
+
// An offset sitting on the line terminator itself carets one past
|
|
108
|
+
// the last character of the line's content.
|
|
109
|
+
let caret = off.clamp(line_start, line_end);
|
|
110
|
+
let column = 1 + count_chars(&source[line_start..caret]);
|
|
111
|
+
let snippet = std::str::from_utf8(&source[line_start..line_end])
|
|
112
|
+
.ok()
|
|
113
|
+
.and_then(|l| build_snippet(l, caret - line_start));
|
|
114
|
+
Location {
|
|
115
|
+
line,
|
|
116
|
+
column,
|
|
117
|
+
snippet,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn count_newlines(bytes: &[u8]) -> usize {
|
|
122
|
+
bytes.iter().filter(|&&b| b == b'\n').count()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Characters kept around the caret. Minified JSON is routinely one
|
|
126
|
+
/// multi-kilobyte line, so the snippet shows a window, not the line.
|
|
127
|
+
const SNIPPET_CHARS_BEFORE: usize = 50;
|
|
128
|
+
const SNIPPET_CHARS_AFTER: usize = 30;
|
|
129
|
+
const SNIPPET_ELLIPSIS: char = '…';
|
|
130
|
+
|
|
131
|
+
/// Render the offending line plus a caret line underneath, windowed
|
|
132
|
+
/// around `caret_byte` (a byte offset into `line`).
|
|
133
|
+
fn build_snippet(line: &str, caret_byte: usize) -> Option<String> {
|
|
134
|
+
if line.is_empty() {
|
|
135
|
+
return None;
|
|
136
|
+
}
|
|
137
|
+
let mut caret_byte = caret_byte.min(line.len());
|
|
138
|
+
while !line.is_char_boundary(caret_byte) {
|
|
139
|
+
caret_byte -= 1;
|
|
140
|
+
}
|
|
141
|
+
let caret_chars = line[..caret_byte].chars().count();
|
|
142
|
+
let total_chars = caret_chars + line[caret_byte..].chars().count();
|
|
143
|
+
|
|
144
|
+
let window_first = caret_chars.saturating_sub(SNIPPET_CHARS_BEFORE);
|
|
145
|
+
let window_last = (caret_chars + SNIPPET_CHARS_AFTER).min(total_chars);
|
|
146
|
+
let mut caret_column = caret_chars - window_first;
|
|
147
|
+
|
|
148
|
+
let mut out = String::new();
|
|
149
|
+
if window_first > 0 {
|
|
150
|
+
out.push(SNIPPET_ELLIPSIS);
|
|
151
|
+
caret_column += 1;
|
|
152
|
+
}
|
|
153
|
+
for ch in line
|
|
154
|
+
.chars()
|
|
155
|
+
.skip(window_first)
|
|
156
|
+
.take(window_last - window_first)
|
|
157
|
+
{
|
|
158
|
+
// Control characters (tabs included) render as one space so
|
|
159
|
+
// the caret column stays aligned with what is printed.
|
|
160
|
+
out.push(if ch.is_control() { ' ' } else { ch });
|
|
161
|
+
}
|
|
162
|
+
if window_last < total_chars {
|
|
163
|
+
out.push(SNIPPET_ELLIPSIS);
|
|
164
|
+
}
|
|
165
|
+
out.push('\n');
|
|
166
|
+
for _ in 0..caret_column {
|
|
167
|
+
out.push(' ');
|
|
168
|
+
}
|
|
169
|
+
out.push('^');
|
|
170
|
+
Some(out)
|
|
171
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//! File entry points. `load_file` reads natively into a reused Rust
|
|
2
|
+
//! buffer, so a document never round-trips through a throwaway Ruby
|
|
3
|
+
//! String (the string allocation and its GC churn are ~37% of
|
|
4
|
+
//! `parse(File.read(path))` on a 570 KB document). `write_file`
|
|
5
|
+
//! streams the generator's pooled buffer straight to disk. The mmap
|
|
6
|
+
//! trio (`load_lazy_file`, `at_pointer_file`, `dig_file`) maps the
|
|
7
|
+
//! file read-only, so partial access never reads unused pages off
|
|
8
|
+
//! disk.
|
|
9
|
+
//!
|
|
10
|
+
//! I/O failures raise the mapped `Errno::*` exception, like `File`
|
|
11
|
+
//! methods do.
|
|
12
|
+
|
|
13
|
+
use std::cell::RefCell;
|
|
14
|
+
use std::fs;
|
|
15
|
+
use std::io::Read;
|
|
16
|
+
|
|
17
|
+
use magnus::value::ReprValue;
|
|
18
|
+
use magnus::{Error, RArray, RString, Ruby, Value};
|
|
19
|
+
|
|
20
|
+
use crate::errors::{parser_error_at, runtime_error};
|
|
21
|
+
use crate::gen;
|
|
22
|
+
use crate::lazy::{self, DocBytes};
|
|
23
|
+
use crate::parse::{err, materialize, materialize_at, parse_native_opts, span_of, ParseNativeOpts};
|
|
24
|
+
use crate::pointer::path_to_pointer;
|
|
25
|
+
use crate::state::PULL_STATE;
|
|
26
|
+
|
|
27
|
+
const NOT_UTF8: &str = "input is not valid UTF-8";
|
|
28
|
+
|
|
29
|
+
thread_local! {
|
|
30
|
+
/// Reused read buffer for `load_file`: capacity survives across
|
|
31
|
+
/// calls, so a hot loop over files allocates nothing.
|
|
32
|
+
static FILE_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// On Unix the raw OS error IS the errno `rb_syserr_new` expects.
|
|
36
|
+
#[cfg(unix)]
|
|
37
|
+
fn errno_of(e: &std::io::Error) -> Option<i32> {
|
|
38
|
+
e.raw_os_error()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// On Windows the raw OS error is a Win32 code, not a POSIX errno
|
|
42
|
+
/// (ERROR_PATH_NOT_FOUND = 3 read as errno 3 raised Errno::ESRCH on
|
|
43
|
+
/// CI), so map the portable ErrorKind onto the classic CRT errno
|
|
44
|
+
/// values Ruby's Errno classes use. Unmapped kinds fall back to a
|
|
45
|
+
/// plain RuntimeError with the message.
|
|
46
|
+
#[cfg(windows)]
|
|
47
|
+
fn errno_of(e: &std::io::Error) -> Option<i32> {
|
|
48
|
+
const ENOENT: i32 = 2;
|
|
49
|
+
const EACCES: i32 = 13;
|
|
50
|
+
const EEXIST: i32 = 17;
|
|
51
|
+
match e.kind() {
|
|
52
|
+
std::io::ErrorKind::NotFound => Some(ENOENT),
|
|
53
|
+
std::io::ErrorKind::PermissionDenied => Some(EACCES),
|
|
54
|
+
std::io::ErrorKind::AlreadyExists => Some(EEXIST),
|
|
55
|
+
_ => None,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Map an I/O failure onto the matching `Errno::*` exception (class
|
|
60
|
+
/// parity with `File.read`/`File.write`; the message carries the path).
|
|
61
|
+
fn io_error(ruby: &Ruby, path: &str, e: &std::io::Error) -> Error {
|
|
62
|
+
use magnus::rb_sys::FromRawValue;
|
|
63
|
+
let Some(errno) = errno_of(e) else {
|
|
64
|
+
return runtime_error(ruby, format!("{e} - {path}"));
|
|
65
|
+
};
|
|
66
|
+
let Ok(cpath) = std::ffi::CString::new(path) else {
|
|
67
|
+
return runtime_error(ruby, format!("{e} - {path}"));
|
|
68
|
+
};
|
|
69
|
+
// SAFETY: rb_syserr_new returns a live Errno exception instance.
|
|
70
|
+
let exc = unsafe { Value::from_raw(rb_sys::rb_syserr_new(errno, cpath.as_ptr())) };
|
|
71
|
+
match magnus::Exception::from_value(exc) {
|
|
72
|
+
Some(exc) => exc.into(),
|
|
73
|
+
None => runtime_error(ruby, format!("{e} - {path}")),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// `NOSJ.load_file(path, opts)`: parse a file without ever creating a
|
|
78
|
+
/// file-sized Ruby String.
|
|
79
|
+
pub fn load_file_native(
|
|
80
|
+
ruby: &Ruby,
|
|
81
|
+
_rb_self: Value,
|
|
82
|
+
path: RString,
|
|
83
|
+
opts: Value,
|
|
84
|
+
) -> Result<Value, Error> {
|
|
85
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
86
|
+
let p = path.to_string()?;
|
|
87
|
+
FILE_BUF.with(|cell| {
|
|
88
|
+
let mut buf = cell
|
|
89
|
+
.try_borrow_mut()
|
|
90
|
+
.map_or_else(|_| Vec::new(), |mut b| std::mem::take(&mut *b));
|
|
91
|
+
buf.clear();
|
|
92
|
+
let result = read_into(&mut buf, &p)
|
|
93
|
+
.map_err(|e| io_error(ruby, &p, &e))
|
|
94
|
+
.and_then(|()| {
|
|
95
|
+
if std::str::from_utf8(&buf).is_err() {
|
|
96
|
+
return Err(err(ruby, NOT_UTF8.into()));
|
|
97
|
+
}
|
|
98
|
+
materialize(ruby, &buf, &o)
|
|
99
|
+
});
|
|
100
|
+
if let Ok(mut slot) = cell.try_borrow_mut() {
|
|
101
|
+
*slot = buf;
|
|
102
|
+
}
|
|
103
|
+
result
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn read_into(buf: &mut Vec<u8>, path: &str) -> std::io::Result<()> {
|
|
108
|
+
let mut f = fs::File::open(path)?;
|
|
109
|
+
let hint = f.metadata().map(|m| m.len() as usize).unwrap_or(0);
|
|
110
|
+
buf.reserve(hint.saturating_add(1));
|
|
111
|
+
f.read_to_end(buf)?;
|
|
112
|
+
Ok(())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// `NOSJ.write_file(path, obj, opts)`: generate under the usual
|
|
116
|
+
/// options and write the bytes straight from the generator's pooled
|
|
117
|
+
/// buffer. Returns the byte count, like `File.write`.
|
|
118
|
+
pub fn write_file_native(
|
|
119
|
+
ruby: &Ruby,
|
|
120
|
+
_rb_self: Value,
|
|
121
|
+
path: RString,
|
|
122
|
+
obj: Value,
|
|
123
|
+
opts: Value,
|
|
124
|
+
) -> Result<usize, Error> {
|
|
125
|
+
let p = path.to_string()?;
|
|
126
|
+
gen::generate_bytes_into(ruby, obj, opts, |ruby, bytes| {
|
|
127
|
+
fs::write(&p, bytes).map_err(|e| io_error(ruby, &p, &e))?;
|
|
128
|
+
Ok(bytes.len())
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// `NOSJ.write_lines(path, values, opts)`: generate NDJSON (one
|
|
133
|
+
/// document per element, newline-terminated) and write the bytes
|
|
134
|
+
/// straight from the generator's pooled buffer. Returns the byte
|
|
135
|
+
/// count, like `File.write`.
|
|
136
|
+
pub fn write_lines_native(
|
|
137
|
+
ruby: &Ruby,
|
|
138
|
+
_rb_self: Value,
|
|
139
|
+
path: RString,
|
|
140
|
+
values: magnus::RArray,
|
|
141
|
+
opts: Value,
|
|
142
|
+
) -> Result<usize, Error> {
|
|
143
|
+
let p = path.to_string()?;
|
|
144
|
+
gen::generate_lines_bytes_into(ruby, values, opts, |ruby, bytes| {
|
|
145
|
+
fs::write(&p, bytes).map_err(|e| io_error(ruby, &p, &e))?;
|
|
146
|
+
Ok(bytes.len())
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Map `path` read-only and hand a UTF-8-checked view to `f`.
|
|
151
|
+
pub(crate) fn with_mapped_file<R>(
|
|
152
|
+
ruby: &Ruby,
|
|
153
|
+
path: &str,
|
|
154
|
+
f: impl FnOnce(memmap2::Mmap) -> Result<R, Error>,
|
|
155
|
+
) -> Result<R, Error> {
|
|
156
|
+
let file = fs::File::open(path).map_err(|e| io_error(ruby, path, &e))?;
|
|
157
|
+
// SAFETY: the mapping is read-only; concurrent modification of the
|
|
158
|
+
// file by another process is documented as unsupported (the
|
|
159
|
+
// standard mmap caveat, shared with every mmap-based parser).
|
|
160
|
+
let map = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| io_error(ruby, path, &e))?;
|
|
161
|
+
if std::str::from_utf8(&map).is_err() {
|
|
162
|
+
return Err(err(ruby, NOT_UTF8.into()));
|
|
163
|
+
}
|
|
164
|
+
f(map)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// `NOSJ.load_lazy_file(path, opts)`: a lazy document over a read-only
|
|
168
|
+
/// file mapping. Creation costs one sequential UTF-8 scan; access is
|
|
169
|
+
/// the usual lazy resolution, and pages outside the touched paths are
|
|
170
|
+
/// never read.
|
|
171
|
+
pub fn load_lazy_file_native(
|
|
172
|
+
ruby: &Ruby,
|
|
173
|
+
_rb_self: Value,
|
|
174
|
+
path: RString,
|
|
175
|
+
opts: Value,
|
|
176
|
+
) -> Result<Value, Error> {
|
|
177
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
178
|
+
let p = path.to_string()?;
|
|
179
|
+
with_mapped_file(ruby, &p, |map| {
|
|
180
|
+
lazy::wrap_root(ruby, DocBytes::Mmap(map), o)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Resolve one pointer against a mapped file and materialize the
|
|
185
|
+
/// matched subtree; the mapping is dropped before returning.
|
|
186
|
+
fn resolve_file_pointer(
|
|
187
|
+
ruby: &Ruby,
|
|
188
|
+
path: &str,
|
|
189
|
+
pointer: &str,
|
|
190
|
+
o: &ParseNativeOpts,
|
|
191
|
+
) -> Result<Value, Error> {
|
|
192
|
+
with_mapped_file(ruby, path, |map| {
|
|
193
|
+
let resolved = PULL_STATE.with(|cell| {
|
|
194
|
+
let mut state = cell.borrow_mut();
|
|
195
|
+
// SAFETY: UTF-8 checked by with_mapped_file.
|
|
196
|
+
unsafe { nosj::pointer_utf8_unchecked(&map, pointer, &mut state.bufs) }
|
|
197
|
+
});
|
|
198
|
+
match resolved {
|
|
199
|
+
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
200
|
+
Ok(Some(slice)) => {
|
|
201
|
+
let (start, end) = span_of(&map, slice.as_bytes());
|
|
202
|
+
materialize_at(ruby, &map, start, end, o)
|
|
203
|
+
}
|
|
204
|
+
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
205
|
+
Err(Error::new(ruby.exception_arg_error(), e.to_string()))
|
|
206
|
+
}
|
|
207
|
+
Err(e) => Err(parser_error_at(ruby, &map, e.offset, e.to_string())),
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// `NOSJ.at_pointer_file(path, pointer, opts)`: `NOSJ.at_pointer`
|
|
213
|
+
/// against a file, without reading it into Ruby.
|
|
214
|
+
pub fn at_pointer_file_native(
|
|
215
|
+
ruby: &Ruby,
|
|
216
|
+
_rb_self: Value,
|
|
217
|
+
path: RString,
|
|
218
|
+
pointer: RString,
|
|
219
|
+
opts: Value,
|
|
220
|
+
) -> Result<Value, Error> {
|
|
221
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
222
|
+
let p = path.to_string()?;
|
|
223
|
+
let ptr = pointer.to_string()?;
|
|
224
|
+
resolve_file_pointer(ruby, &p, &ptr, &o)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// `NOSJ.dig_file(path, *dig_path)`: `NOSJ.dig` against a file.
|
|
228
|
+
/// Negative indices resolve to nil, as everywhere else.
|
|
229
|
+
pub fn dig_file_native(
|
|
230
|
+
ruby: &Ruby,
|
|
231
|
+
_rb_self: Value,
|
|
232
|
+
path: RString,
|
|
233
|
+
dig_path: RArray,
|
|
234
|
+
) -> Result<Value, Error> {
|
|
235
|
+
let p = path.to_string()?;
|
|
236
|
+
match path_to_pointer(ruby, dig_path)? {
|
|
237
|
+
Some(ptr) => resolve_file_pointer(ruby, &p, &ptr, &ParseNativeOpts::default()),
|
|
238
|
+
None => Ok(ruby.qnil().as_value()),
|
|
239
|
+
}
|
|
240
|
+
}
|
data/ext/nosj/src/gen/errors.rs
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
use magnus::error::ErrorType;
|
|
6
6
|
use magnus::rb_sys::AsRawValue;
|
|
7
7
|
use magnus::value::ReprValue;
|
|
8
|
-
use magnus::{Error,
|
|
8
|
+
use magnus::{Error, Ruby};
|
|
9
9
|
|
|
10
10
|
use super::ruby::rstring_bytes;
|
|
11
|
+
use crate::errors::nosj_exception;
|
|
11
12
|
|
|
12
13
|
pub(super) enum GenFail {
|
|
13
14
|
/// Re-raise a Ruby exception captured by a protected call (user
|
|
@@ -22,14 +23,6 @@ pub(super) enum GenFail {
|
|
|
22
23
|
Nesting(usize),
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
fn nosj_exception(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
26
|
-
let lookup = || -> Result<ExceptionClass, Error> {
|
|
27
|
-
let m: RModule = ruby.define_module("NOSJ")?;
|
|
28
|
-
m.const_get(name)
|
|
29
|
-
};
|
|
30
|
-
lookup().unwrap_or_else(|_| ruby.exception_runtime_error())
|
|
31
|
-
}
|
|
32
|
-
|
|
33
26
|
/// The exception's `to_s` (its message), matching what the gem embeds
|
|
34
27
|
/// when it wraps a secondary exception.
|
|
35
28
|
fn error_message(err: &Error) -> String {
|