nosj 0.1.0 → 0.2.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 +22 -1
- data/Cargo.lock +11 -1
- data/README.md +63 -11
- data/ext/nosj/Cargo.toml +4 -1
- data/ext/nosj/src/files.rs +218 -0
- data/ext/nosj/src/gen/mod.rs +44 -10
- data/ext/nosj/src/lazy.rs +402 -0
- data/ext/nosj/src/lib.rs +30 -0
- data/ext/nosj/src/pointer.rs +14 -12
- data/lib/nosj/lazy.rb +165 -0
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +85 -2
- data/sig/nosj.rbs +54 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78314f13c659fff6d0e146ac0b951564e66b7b10deeb439e317703754a0100ef
|
|
4
|
+
data.tar.gz: 993f8a38c4e04dcd68a6ec8ee2dc62c611d2a04c1bc0ff23fc31d67ad69dd585
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b5f09e43ed85c2e345a35d0fd681900e205d4743161542d82ba5c3d1c80879c62bff3acf88f12dac5836334c67d933cc6241ead7dbe6a7a820d5f672bccd3d4
|
|
7
|
+
data.tar.gz: 0b48922585bb4863f9ed71fb9500a9a60b4c1f857baeaaf79136ed4c9e752aa71c9c23235e85d3bab38b1f0967b7b7212ed6d771be57d62d87fcd39e91666cd8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
## [0.
|
|
1
|
+
## [0.2.0] - 2026-07-16
|
|
2
|
+
|
|
3
|
+
- File APIs. `NOSJ.load_file(path, opts)` parses a file directly
|
|
4
|
+
(~1.3× faster than `parse(File.read(path))`—no file-sized Ruby
|
|
5
|
+
String is created), and `NOSJ.write_file(path, obj, opts)` generates
|
|
6
|
+
straight to disk, returning the byte count like `File.write`.
|
|
7
|
+
`NOSJ.load_lazy_file(path, opts)` wraps a file as a lazy document
|
|
8
|
+
over a read-only memory map, and `NOSJ.at_pointer_file` /
|
|
9
|
+
`NOSJ.dig_file` pull single values out of a file without reading the
|
|
10
|
+
rest into Ruby. Missing files raise the usual `Errno` exceptions.
|
|
11
|
+
- `NOSJ.lazy`: lazy documents. Wrap a document once, then read only
|
|
12
|
+
what you need: `doc["users"][3]["name"]` parses just that path, `#dig`
|
|
13
|
+
and `#at_pointer` resolve whole paths, and `#keys`, `#size`, and
|
|
14
|
+
`#each` inspect a node without parsing its values. Containers come
|
|
15
|
+
back lazy, scalars come back as plain Ruby values, and repeated
|
|
16
|
+
reads are cached. `#value` (also `#to_h` / `#to_a`) materializes a
|
|
17
|
+
subtree under the usual parse options (`symbolize_names`, `freeze`,
|
|
18
|
+
...). Pass a frozen string and creating the view is practically
|
|
19
|
+
free, even on megabyte documents. Malformed content raises on first
|
|
20
|
+
read, not at wrap time.
|
|
21
|
+
|
|
22
|
+
## [0.1.0] - 2026-07-16
|
|
2
23
|
|
|
3
24
|
Initial release.
|
|
4
25
|
|
data/Cargo.lock
CHANGED
|
@@ -162,6 +162,15 @@ version = "2.8.3"
|
|
|
162
162
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
163
163
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
164
164
|
|
|
165
|
+
[[package]]
|
|
166
|
+
name = "memmap2"
|
|
167
|
+
version = "0.9.11"
|
|
168
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
169
|
+
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
|
|
170
|
+
dependencies = [
|
|
171
|
+
"libc",
|
|
172
|
+
]
|
|
173
|
+
|
|
165
174
|
[[package]]
|
|
166
175
|
name = "minimal-lexical"
|
|
167
176
|
version = "0.2.1"
|
|
@@ -189,10 +198,11 @@ dependencies = [
|
|
|
189
198
|
|
|
190
199
|
[[package]]
|
|
191
200
|
name = "nosj_native"
|
|
192
|
-
version = "0.
|
|
201
|
+
version = "0.2.0"
|
|
193
202
|
dependencies = [
|
|
194
203
|
"ahash",
|
|
195
204
|
"magnus",
|
|
205
|
+
"memmap2",
|
|
196
206
|
"nosj",
|
|
197
207
|
"rb-sys",
|
|
198
208
|
]
|
data/README.md
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
- It is **faster** than gem json and every
|
|
11
11
|
third-party parser, including Oj, RapidJSON, FastJsonparser, Yajl. 1.0–1.8× faster than the bundled json gem, 1.3–11× faster than Oj, and up to 17×
|
|
12
12
|
faster than Yajl—[see Benchmarks](#benchmarks).
|
|
13
|
+
- It has **lazy documents**: `NOSJ.lazy` wraps a document and parses a value only when you touch it—repeated access costs nanoseconds, and everything you never read is never parsed.
|
|
14
|
+
- It has a **partial parsing mode**: JSON Pointer lookups that pull single values out of big documents in microseconds, skipping everything else.
|
|
15
|
+
- It has **file APIs**: parse, generate, dig, and lazy-wrap files directly—no throwaway file-sized Ruby String, and the partial modes memory-map the file so unread pages never even leave the disk.
|
|
13
16
|
- It comes **precompiled** (platform gems built with per-platform optimizations,
|
|
14
17
|
nothing to compile on install).
|
|
15
|
-
-
|
|
16
|
-
- Same API and option names as gem json.
|
|
18
|
+
- Otherwise, same API and option names as gem json.
|
|
17
19
|
|
|
18
20
|
**And there's more**: validate documents without building a single Ruby object, resolve whole batches of paths in one pass, and accelerate an entire application with a one-line drop-in.
|
|
19
21
|
|
|
@@ -83,17 +85,23 @@ NOSJ.generate(obj) # indent, space, object_nl, ...,
|
|
|
83
85
|
NOSJ.pretty_generate(obj) # ascii_only, script_safe, strict
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`NOSJ.parse`, which already leads every parser above:
|
|
88
|
+
**Lazy documents.** `NOSJ.lazy` wraps a document in a lazy view: read
|
|
89
|
+
a field and only that path is parsed—containers stay lazy, scalars
|
|
90
|
+
arrive as plain Ruby values, and repeated reads are cached:
|
|
90
91
|
|
|
91
92
|
```ruby
|
|
92
|
-
NOSJ.
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
doc = NOSJ.lazy(json)
|
|
94
|
+
doc["users"][3]["name"] # parses only this path
|
|
95
|
+
doc.dig("meta", "count") # a whole path in one fused resolution
|
|
96
|
+
doc["users"].size # counted without materializing anything
|
|
97
|
+
doc["users"][3].value # materialize one subtree (parse options apply)
|
|
95
98
|
```
|
|
96
99
|
|
|
100
|
+
Pass a frozen string and creating the view is practically free—
|
|
101
|
+
nanoseconds, even on a megabyte document. Malformed content raises
|
|
102
|
+
when it is first read, not at wrap time.
|
|
103
|
+
|
|
104
|
+
|
|
97
105
|
**Partial parsing.** Pull values out of a document without
|
|
98
106
|
materializing the rest—skipped content is stepped over at SIMD block
|
|
99
107
|
speed, so a lookup costs what it skips, not what the document weighs:
|
|
@@ -114,11 +122,38 @@ at the far end of a 570 KB document costs ~71µs, still 13× faster
|
|
|
114
122
|
than parse-then-dig. Misses return nil; matched subtrees materialize
|
|
115
123
|
with the same options as `parse` (`symbolize_names:`, `freeze:`).
|
|
116
124
|
|
|
125
|
+
**Files.** Every mode has a file-native form, so a document never
|
|
126
|
+
round-trips through a throwaway Ruby String:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
NOSJ.load_file("config.json") # 1.3× File.read + parse
|
|
130
|
+
NOSJ.write_file("out.json", obj) # generate straight to disk
|
|
131
|
+
NOSJ.dig_file("huge.json", "users", 3, "name") # never reads the rest
|
|
132
|
+
NOSJ.at_pointer_file("huge.json", "/meta/count")
|
|
133
|
+
doc = NOSJ.load_lazy_file("huge.json") # lazy view over a memory map
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The partial and lazy forms memory-map the file, so pages you never
|
|
137
|
+
read are never loaded from disk. Missing files raise the usual
|
|
138
|
+
`Errno` exceptions. Measured numbers live in
|
|
139
|
+
[Benchmarks → File APIs](#file-apis).
|
|
140
|
+
|
|
141
|
+
**Validation without parsing.** `NOSJ.valid?` runs the full
|
|
142
|
+
parser—tokenizers, string decode, number validation—into a null sink
|
|
143
|
+
and allocates no Ruby objects at all. It is 2-4× faster than
|
|
144
|
+
`NOSJ.parse`, which already leads every parser above:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
NOSJ.valid?('{"a":1}') #=> true
|
|
148
|
+
NOSJ.valid?('{"a":}') #=> false
|
|
149
|
+
NOSJ.valid?(src, max_nesting: false) # same options as parse
|
|
150
|
+
```
|
|
151
|
+
|
|
117
152
|
## Benchmarks
|
|
118
153
|
|
|
119
154
|
Every installed JSON gem, benchmark-ips: AWS EC2 c7a.2xlarge (AMD EPYC 9R14, Zen 4), Ruby 4.0.6 + YJIT, json 2.21.1, Oj 3.17.4, RapidJSON 0.4.0, FastJsonparser 0.6.0, Yajl 1.4.3, PGO build, 2026-07-16. `×N` = times slower than nosj.
|
|
120
155
|
|
|
121
|
-
Parse
|
|
156
|
+
### Parse
|
|
122
157
|
|
|
123
158
|
| file | nosj (i/s) | json | Oj | FastJsonparser | RapidJSON | Yajl |
|
|
124
159
|
|---|---:|---:|---:|---:|---:|---:|
|
|
@@ -136,7 +171,7 @@ Parse:
|
|
|
136
171
|
| tolstoy | **8.9k** | ×1.79 | ×1.96 | ×2.29 | ×2.10 | ×17.31 |
|
|
137
172
|
| twitter | **1.1k** | ×1.09 | ×1.83 | ×2.25 | ×2.61 | ×5.40 |
|
|
138
173
|
|
|
139
|
-
Generate
|
|
174
|
+
### Generate
|
|
140
175
|
|
|
141
176
|
| file | nosj (i/s) | json | Oj | RapidJSON | Yajl |
|
|
142
177
|
|---|---:|---:|---:|---:|---:|
|
|
@@ -157,6 +192,23 @@ Generate:
|
|
|
157
192
|
\* canada-generate is a statistical tie with the json gem (within
|
|
158
193
|
measurement error).
|
|
159
194
|
|
|
195
|
+
### File APIs
|
|
196
|
+
|
|
197
|
+
twitter.json (570 KB) from a warm page cache, medians of 7 alternating
|
|
198
|
+
rounds against the plain-Ruby composition on the same parser (Apple
|
|
199
|
+
Silicon dev box, Ruby 4.0.6 + YJIT, PGO build, 2026-07-16):
|
|
200
|
+
|
|
201
|
+
| operation | µs/op | vs the Ruby way |
|
|
202
|
+
|---|---:|---|
|
|
203
|
+
| `NOSJ.load_file` (parse the whole file) | 948 | ×1.33 vs `NOSJ.parse(File.read(path))` |
|
|
204
|
+
| `NOSJ.dig_file` (one deep field) | 246 | ×5.2 vs read + parse + dig |
|
|
205
|
+
| `NOSJ.load_lazy_file` + one field | 257 | ×5.0 vs read + parse + dig |
|
|
206
|
+
|
|
207
|
+
`NOSJ.write_file` measures at parity with
|
|
208
|
+
`File.write(NOSJ.generate(obj))` on this box—file-write timings swing
|
|
209
|
+
too much for an honest multiplier; what it saves is the intermediate
|
|
210
|
+
file-sized Ruby String.
|
|
211
|
+
|
|
160
212
|
Reproduce with `rake bench` (the parity-gated comparison, after a PGO retrain—the shipping configuration) or `rake bench:ips` (the multi-gem shoot-out).
|
|
161
213
|
|
|
162
214
|
## Switching from the json gem
|
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.2.0"
|
|
9
9
|
edition = "2021"
|
|
10
10
|
authors = ["Yaroslav Markin <yaroslav@markin.net>"]
|
|
11
11
|
license = "MIT"
|
|
@@ -19,6 +19,9 @@ crate-type = ["cdylib", "rlib"]
|
|
|
19
19
|
magnus = { git = "https://github.com/matsadler/magnus", features = ["embed", "rb-sys"] }
|
|
20
20
|
rb-sys = { version = "0.9.124", default-features = false }
|
|
21
21
|
ahash = "0.8.11"
|
|
22
|
+
# Read-only file mapping for the file entry points (load_lazy_file,
|
|
23
|
+
# at_pointer_file, dig_file): pages never touched are never read.
|
|
24
|
+
memmap2 = "0.9"
|
|
22
25
|
# First-party SIMD JSON parse/generate library (github.com/yaroslav/nosj).
|
|
23
26
|
# For coordinated crate+gem work, temporarily flip to
|
|
24
27
|
# { path = "../../../nosj" } and restore before any commit or release.
|
|
@@ -0,0 +1,218 @@
|
|
|
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::gen;
|
|
21
|
+
use crate::lazy::{self, DocBytes};
|
|
22
|
+
use crate::parse::{err, materialize, parse_native_opts, ParseNativeOpts};
|
|
23
|
+
use crate::pointer::path_to_pointer;
|
|
24
|
+
use crate::state::PULL_STATE;
|
|
25
|
+
|
|
26
|
+
const NOT_UTF8: &str = "input is not valid UTF-8";
|
|
27
|
+
|
|
28
|
+
thread_local! {
|
|
29
|
+
/// Reused read buffer for `load_file`: capacity survives across
|
|
30
|
+
/// calls, so a hot loop over files allocates nothing.
|
|
31
|
+
static FILE_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// On Unix the raw OS error IS the errno `rb_syserr_new` expects.
|
|
35
|
+
#[cfg(unix)]
|
|
36
|
+
fn errno_of(e: &std::io::Error) -> Option<i32> {
|
|
37
|
+
e.raw_os_error()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// On Windows the raw OS error is a Win32 code, not a POSIX errno
|
|
41
|
+
/// (ERROR_PATH_NOT_FOUND = 3 read as errno 3 raised Errno::ESRCH on
|
|
42
|
+
/// CI), so map the portable ErrorKind onto the classic CRT errno
|
|
43
|
+
/// values Ruby's Errno classes use. Unmapped kinds fall back to a
|
|
44
|
+
/// plain RuntimeError with the message.
|
|
45
|
+
#[cfg(windows)]
|
|
46
|
+
fn errno_of(e: &std::io::Error) -> Option<i32> {
|
|
47
|
+
const ENOENT: i32 = 2;
|
|
48
|
+
const EACCES: i32 = 13;
|
|
49
|
+
const EEXIST: i32 = 17;
|
|
50
|
+
match e.kind() {
|
|
51
|
+
std::io::ErrorKind::NotFound => Some(ENOENT),
|
|
52
|
+
std::io::ErrorKind::PermissionDenied => Some(EACCES),
|
|
53
|
+
std::io::ErrorKind::AlreadyExists => Some(EEXIST),
|
|
54
|
+
_ => None,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Map an I/O failure onto the matching `Errno::*` exception (class
|
|
59
|
+
/// parity with `File.read`/`File.write`; the message carries the path).
|
|
60
|
+
fn io_error(ruby: &Ruby, path: &str, e: &std::io::Error) -> Error {
|
|
61
|
+
use magnus::rb_sys::FromRawValue;
|
|
62
|
+
let Some(errno) = errno_of(e) else {
|
|
63
|
+
return err(ruby, format!("{e} - {path}"));
|
|
64
|
+
};
|
|
65
|
+
let Ok(cpath) = std::ffi::CString::new(path) else {
|
|
66
|
+
return err(ruby, format!("{e} - {path}"));
|
|
67
|
+
};
|
|
68
|
+
// SAFETY: rb_syserr_new returns a live Errno exception instance.
|
|
69
|
+
let exc = unsafe { Value::from_raw(rb_sys::rb_syserr_new(errno, cpath.as_ptr())) };
|
|
70
|
+
match magnus::Exception::from_value(exc) {
|
|
71
|
+
Some(exc) => exc.into(),
|
|
72
|
+
None => err(ruby, format!("{e} - {path}")),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// `NOSJ.load_file(path, opts)`: parse a file without ever creating a
|
|
77
|
+
/// file-sized Ruby String.
|
|
78
|
+
pub fn load_file_native(
|
|
79
|
+
ruby: &Ruby,
|
|
80
|
+
_rb_self: Value,
|
|
81
|
+
path: RString,
|
|
82
|
+
opts: Value,
|
|
83
|
+
) -> Result<Value, Error> {
|
|
84
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
85
|
+
let p = path.to_string()?;
|
|
86
|
+
FILE_BUF.with(|cell| {
|
|
87
|
+
let mut buf = cell
|
|
88
|
+
.try_borrow_mut()
|
|
89
|
+
.map_or_else(|_| Vec::new(), |mut b| std::mem::take(&mut *b));
|
|
90
|
+
buf.clear();
|
|
91
|
+
let result = read_into(&mut buf, &p)
|
|
92
|
+
.map_err(|e| io_error(ruby, &p, &e))
|
|
93
|
+
.and_then(|()| {
|
|
94
|
+
if std::str::from_utf8(&buf).is_err() {
|
|
95
|
+
return Err(err(ruby, NOT_UTF8.into()));
|
|
96
|
+
}
|
|
97
|
+
materialize(ruby, &buf, &o)
|
|
98
|
+
});
|
|
99
|
+
if let Ok(mut slot) = cell.try_borrow_mut() {
|
|
100
|
+
*slot = buf;
|
|
101
|
+
}
|
|
102
|
+
result
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn read_into(buf: &mut Vec<u8>, path: &str) -> std::io::Result<()> {
|
|
107
|
+
let mut f = fs::File::open(path)?;
|
|
108
|
+
let hint = f.metadata().map(|m| m.len() as usize).unwrap_or(0);
|
|
109
|
+
buf.reserve(hint.saturating_add(1));
|
|
110
|
+
f.read_to_end(buf)?;
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// `NOSJ.write_file(path, obj, opts)`: generate under the usual
|
|
115
|
+
/// options and write the bytes straight from the generator's pooled
|
|
116
|
+
/// buffer. Returns the byte count, like `File.write`.
|
|
117
|
+
pub fn write_file_native(
|
|
118
|
+
ruby: &Ruby,
|
|
119
|
+
_rb_self: Value,
|
|
120
|
+
path: RString,
|
|
121
|
+
obj: Value,
|
|
122
|
+
opts: Value,
|
|
123
|
+
) -> Result<usize, Error> {
|
|
124
|
+
let p = path.to_string()?;
|
|
125
|
+
gen::generate_bytes_into(ruby, obj, opts, |ruby, bytes| {
|
|
126
|
+
fs::write(&p, bytes).map_err(|e| io_error(ruby, &p, &e))?;
|
|
127
|
+
Ok(bytes.len())
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Map `path` read-only and hand a UTF-8-checked view to `f`.
|
|
132
|
+
fn with_mapped_file<R>(
|
|
133
|
+
ruby: &Ruby,
|
|
134
|
+
path: &str,
|
|
135
|
+
f: impl FnOnce(memmap2::Mmap) -> Result<R, Error>,
|
|
136
|
+
) -> Result<R, Error> {
|
|
137
|
+
let file = fs::File::open(path).map_err(|e| io_error(ruby, path, &e))?;
|
|
138
|
+
// SAFETY: the mapping is read-only; concurrent modification of the
|
|
139
|
+
// file by another process is documented as unsupported (the
|
|
140
|
+
// standard mmap caveat, shared with every mmap-based parser).
|
|
141
|
+
let map = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| io_error(ruby, path, &e))?;
|
|
142
|
+
if std::str::from_utf8(&map).is_err() {
|
|
143
|
+
return Err(err(ruby, NOT_UTF8.into()));
|
|
144
|
+
}
|
|
145
|
+
f(map)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// `NOSJ.load_lazy_file(path, opts)`: a lazy document over a read-only
|
|
149
|
+
/// file mapping. Creation costs one sequential UTF-8 scan; access is
|
|
150
|
+
/// the usual lazy resolution, and pages outside the touched paths are
|
|
151
|
+
/// never read.
|
|
152
|
+
pub fn load_lazy_file_native(
|
|
153
|
+
ruby: &Ruby,
|
|
154
|
+
_rb_self: Value,
|
|
155
|
+
path: RString,
|
|
156
|
+
opts: Value,
|
|
157
|
+
) -> Result<Value, Error> {
|
|
158
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
159
|
+
let p = path.to_string()?;
|
|
160
|
+
with_mapped_file(ruby, &p, |map| {
|
|
161
|
+
lazy::wrap_root(ruby, DocBytes::Mmap(map), o)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Resolve one pointer against a mapped file and materialize the
|
|
166
|
+
/// matched subtree; the mapping is dropped before returning.
|
|
167
|
+
fn resolve_file_pointer(
|
|
168
|
+
ruby: &Ruby,
|
|
169
|
+
path: &str,
|
|
170
|
+
pointer: &str,
|
|
171
|
+
o: &ParseNativeOpts,
|
|
172
|
+
) -> Result<Value, Error> {
|
|
173
|
+
with_mapped_file(ruby, path, |map| {
|
|
174
|
+
let resolved = PULL_STATE.with(|cell| {
|
|
175
|
+
let mut state = cell.borrow_mut();
|
|
176
|
+
// SAFETY: UTF-8 checked by with_mapped_file.
|
|
177
|
+
unsafe { nosj::pointer_utf8_unchecked(&map, pointer, &mut state.bufs) }
|
|
178
|
+
});
|
|
179
|
+
match resolved {
|
|
180
|
+
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
181
|
+
Ok(Some(slice)) => materialize(ruby, slice.as_bytes(), o),
|
|
182
|
+
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
183
|
+
Err(Error::new(ruby.exception_arg_error(), e.to_string()))
|
|
184
|
+
}
|
|
185
|
+
Err(e) => Err(err(ruby, e.to_string())),
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// `NOSJ.at_pointer_file(path, pointer, opts)`: `NOSJ.at_pointer`
|
|
191
|
+
/// against a file, without reading it into Ruby.
|
|
192
|
+
pub fn at_pointer_file_native(
|
|
193
|
+
ruby: &Ruby,
|
|
194
|
+
_rb_self: Value,
|
|
195
|
+
path: RString,
|
|
196
|
+
pointer: RString,
|
|
197
|
+
opts: Value,
|
|
198
|
+
) -> Result<Value, Error> {
|
|
199
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
200
|
+
let p = path.to_string()?;
|
|
201
|
+
let ptr = pointer.to_string()?;
|
|
202
|
+
resolve_file_pointer(ruby, &p, &ptr, &o)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// `NOSJ.dig_file(path, *dig_path)`: `NOSJ.dig` against a file.
|
|
206
|
+
/// Negative indices resolve to nil, as everywhere else.
|
|
207
|
+
pub fn dig_file_native(
|
|
208
|
+
ruby: &Ruby,
|
|
209
|
+
_rb_self: Value,
|
|
210
|
+
path: RString,
|
|
211
|
+
dig_path: RArray,
|
|
212
|
+
) -> Result<Value, Error> {
|
|
213
|
+
let p = path.to_string()?;
|
|
214
|
+
match path_to_pointer(ruby, dig_path)? {
|
|
215
|
+
Some(ptr) => resolve_file_pointer(ruby, &p, &ptr, &ParseNativeOpts::default()),
|
|
216
|
+
None => Ok(ruby.qnil().as_value()),
|
|
217
|
+
}
|
|
218
|
+
}
|
data/ext/nosj/src/gen/mod.rs
CHANGED
|
@@ -102,12 +102,50 @@ pub fn generate_native(
|
|
|
102
102
|
generate_scratched(ruby, obj, &cfg, cap_hint)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/// Generate `obj` under a Ruby options hash (or nil) and hand the
|
|
106
|
+
/// finished bytes to `finish` instead of building a Ruby String: the
|
|
107
|
+
/// entry `NOSJ.write_file` uses to stream straight to disk.
|
|
108
|
+
pub(crate) fn generate_bytes_into<R>(
|
|
109
|
+
ruby: &Ruby,
|
|
110
|
+
obj: Value,
|
|
111
|
+
opts: Value,
|
|
112
|
+
finish: impl FnOnce(&Ruby, &[u8]) -> Result<R, Error>,
|
|
113
|
+
) -> Result<R, Error> {
|
|
114
|
+
use magnus::value::ReprValue;
|
|
115
|
+
if opts.is_nil() {
|
|
116
|
+
return generate_scratched_into(ruby, obj, &opts::DEFAULT_CONFIG, 0, finish);
|
|
117
|
+
}
|
|
118
|
+
let (cfg, cap_hint) = opts::parse_gen_opts(ruby, opts)?;
|
|
119
|
+
generate_scratched_into(ruby, obj, &cfg, cap_hint, finish)
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
fn generate_scratched(
|
|
106
123
|
ruby: &Ruby,
|
|
107
124
|
obj: Value,
|
|
108
125
|
cfg: &opts::GenConfig,
|
|
109
126
|
cap_hint: usize,
|
|
110
127
|
) -> Result<RString, Error> {
|
|
128
|
+
generate_scratched_into(ruby, obj, cfg, cap_hint, finish_rstring)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// The default finisher: the generated bytes as a Ruby String.
|
|
132
|
+
fn finish_rstring(_ruby: &Ruby, out: &[u8]) -> Result<RString, Error> {
|
|
133
|
+
Ok(unsafe {
|
|
134
|
+
RString::from_value(Value::from_raw(rb_sys::rb_utf8_str_new(
|
|
135
|
+
out.as_ptr().cast(),
|
|
136
|
+
out.len() as std::os::raw::c_long,
|
|
137
|
+
)))
|
|
138
|
+
.expect("rb_utf8_str_new returns a String")
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn generate_scratched_into<R>(
|
|
143
|
+
ruby: &Ruby,
|
|
144
|
+
obj: Value,
|
|
145
|
+
cfg: &opts::GenConfig,
|
|
146
|
+
cap_hint: usize,
|
|
147
|
+
finish: impl FnOnce(&Ruby, &[u8]) -> Result<R, Error>,
|
|
148
|
+
) -> Result<R, Error> {
|
|
111
149
|
GEN_SCRATCH.with(|cell| match cell.try_borrow_mut() {
|
|
112
150
|
Ok(mut scratch) => {
|
|
113
151
|
let scratch = &mut *scratch;
|
|
@@ -118,24 +156,26 @@ fn generate_scratched(
|
|
|
118
156
|
cap_hint,
|
|
119
157
|
&mut scratch.buf,
|
|
120
158
|
&mut scratch.keys,
|
|
159
|
+
finish,
|
|
121
160
|
)
|
|
122
161
|
}
|
|
123
162
|
Err(_) => {
|
|
124
163
|
let mut buf = Vec::new();
|
|
125
164
|
let mut keys = GenKeyCache::default();
|
|
126
|
-
generate_with(ruby, obj, cfg, cap_hint, &mut buf, &mut keys)
|
|
165
|
+
generate_with(ruby, obj, cfg, cap_hint, &mut buf, &mut keys, finish)
|
|
127
166
|
}
|
|
128
167
|
})
|
|
129
168
|
}
|
|
130
169
|
|
|
131
|
-
fn generate_with(
|
|
170
|
+
fn generate_with<R>(
|
|
132
171
|
ruby: &Ruby,
|
|
133
172
|
obj: Value,
|
|
134
173
|
cfg: &opts::GenConfig,
|
|
135
174
|
cap_hint: usize,
|
|
136
175
|
out: &mut Vec<u8>,
|
|
137
176
|
keys: &mut GenKeyCache,
|
|
138
|
-
) -> Result<
|
|
177
|
+
finish: impl FnOnce(&Ruby, &[u8]) -> Result<R, Error>,
|
|
178
|
+
) -> Result<R, Error> {
|
|
139
179
|
out.clear();
|
|
140
180
|
if out.capacity() < cap_hint {
|
|
141
181
|
out.reserve(cap_hint);
|
|
@@ -155,13 +195,7 @@ fn generate_with(
|
|
|
155
195
|
|
|
156
196
|
let Gen { out, fail, .. } = g;
|
|
157
197
|
match (result, fail) {
|
|
158
|
-
(Ok(()), None) =>
|
|
159
|
-
RString::from_value(Value::from_raw(rb_sys::rb_utf8_str_new(
|
|
160
|
-
out.as_ptr().cast(),
|
|
161
|
-
out.len() as std::os::raw::c_long,
|
|
162
|
-
)))
|
|
163
|
-
.expect("rb_utf8_str_new returns a String")
|
|
164
|
-
}),
|
|
198
|
+
(Ok(()), None) => finish(ruby, out),
|
|
165
199
|
(_, Some(fail)) => Err(raise_fail(ruby, fail)),
|
|
166
200
|
(Err(()), None) => Err(Error::new(
|
|
167
201
|
ruby.exception_runtime_error(),
|