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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +112 -0
- data/Cargo.lock +3 -3
- data/README.md +220 -19
- data/ext/nosj/Cargo.toml +2 -2
- data/ext/nosj/fuzz/Cargo.toml +41 -0
- data/ext/nosj/fuzz/fuzz_targets/lines.rs +9 -0
- data/ext/nosj/fuzz/fuzz_targets/patch.rs +10 -0
- data/ext/nosj/fuzz/fuzz_targets/reformat.rs +10 -0
- data/ext/nosj/fuzz/src/lib.rs +48 -0
- data/ext/nosj/fuzz/src/prelude.rb +400 -0
- data/ext/nosj/src/errors.rs +171 -0
- data/ext/nosj/src/files.rs +29 -7
- data/ext/nosj/src/gen/errors.rs +2 -9
- data/ext/nosj/src/gen/mod.rs +156 -12
- data/ext/nosj/src/gen/opts.rs +88 -10
- data/ext/nosj/src/gen/ruby.rs +51 -0
- data/ext/nosj/src/gen/walker.rs +97 -59
- data/ext/nosj/src/lazy.rs +42 -22
- data/ext/nosj/src/lib.rs +30 -0
- data/ext/nosj/src/lines.rs +90 -0
- data/ext/nosj/src/parse.rs +55 -14
- data/ext/nosj/src/patch.rs +547 -0
- data/ext/nosj/src/pointer.rs +12 -5
- data/ext/nosj/src/reformat.rs +320 -0
- data/ext/nosj/src/sink.rs +11 -0
- data/ext/nosj/src/stats.rs +301 -0
- data/lib/nosj/json.rb +30 -6
- 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 +351 -5
- data/sig/nosj.rbs +66 -0
- metadata +16 -2
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
//! Byte-splicing edits and RFC 6902 JSON Patch over raw documents.
|
|
2
|
+
//!
|
|
3
|
+
//! `NOSJ.splice` resolves every target pointer in ONE forward pass
|
|
4
|
+
//! (the batch resolver), then rebuilds the string copying the
|
|
5
|
+
//! untouched bytes around freshly generated values: no parse of the
|
|
6
|
+
//! rest, no re-generation, formatting outside the targets preserved
|
|
7
|
+
//! byte-for-byte. `NOSJ.patch` applies RFC 6902 operations
|
|
8
|
+
//! sequentially the same way; structural ops (add/remove) locate
|
|
9
|
+
//! entries by walking ONLY the parent container's span with the pull
|
|
10
|
+
//! Reader. Values are emitted through the shared gen machinery, so
|
|
11
|
+
//! bytes match `NOSJ.generate` exactly.
|
|
12
|
+
|
|
13
|
+
use magnus::value::ReprValue;
|
|
14
|
+
use magnus::{Error, ExceptionClass, RArray, RHash, RString, Ruby, Value};
|
|
15
|
+
|
|
16
|
+
use crate::errors::{nosj_exception, parser_error_at};
|
|
17
|
+
use crate::gen::{self, opts::GenConfig};
|
|
18
|
+
use crate::parse::{materialize_at, span_of, utf8_input, ParseNativeOpts};
|
|
19
|
+
use crate::state::PULL_STATE;
|
|
20
|
+
|
|
21
|
+
const WS: [u8; 4] = *b" \t\n\r";
|
|
22
|
+
|
|
23
|
+
fn patch_error(ruby: &Ruby, msg: String) -> Error {
|
|
24
|
+
Error::new(nosj_exception(ruby, "PatchError"), msg)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn arg_error(ruby: &Ruby, msg: String) -> Error {
|
|
28
|
+
Error::new(ruby.exception_arg_error(), msg)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Decode generate options into a borrowed config: the shared default
|
|
32
|
+
/// for nil, else a config built into `slot`.
|
|
33
|
+
fn gen_config<'a>(
|
|
34
|
+
ruby: &Ruby,
|
|
35
|
+
opts: Value,
|
|
36
|
+
slot: &'a mut Option<GenConfig>,
|
|
37
|
+
) -> Result<&'a GenConfig, Error> {
|
|
38
|
+
if opts.is_nil() {
|
|
39
|
+
Ok(&gen::opts::DEFAULT_CONFIG)
|
|
40
|
+
} else {
|
|
41
|
+
Ok(slot.insert(gen::opts::parse_gen_opts(ruby, opts)?.0))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Resolve `pointer` over `doc`, returning the value's byte span.
|
|
46
|
+
/// Parse failures raise the rich ParserError (absolute positions);
|
|
47
|
+
/// pointer syntax errors raise ArgumentError, like `at_pointer`.
|
|
48
|
+
fn span_at(ruby: &Ruby, doc: &[u8], pointer: &str) -> Result<Option<(usize, usize)>, Error> {
|
|
49
|
+
let resolved = PULL_STATE.with(|cell| {
|
|
50
|
+
let mut state = cell.borrow_mut();
|
|
51
|
+
// SAFETY: every entry validated the document bytes as UTF-8.
|
|
52
|
+
unsafe { nosj::pointer_utf8_unchecked(doc, pointer, &mut state.bufs) }
|
|
53
|
+
});
|
|
54
|
+
match resolved {
|
|
55
|
+
Ok(None) => Ok(None),
|
|
56
|
+
Ok(Some(slice)) => Ok(Some(span_of(doc, slice.as_bytes()))),
|
|
57
|
+
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
58
|
+
Err(arg_error(ruby, e.to_string()))
|
|
59
|
+
}
|
|
60
|
+
Err(e) => Err(parser_error_at(ruby, doc, e.offset, e.to_string())),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// One direct child of a container: decoded key (objects only) and
|
|
65
|
+
/// the value's byte span, doc-absolute.
|
|
66
|
+
struct ChildSpan {
|
|
67
|
+
key: Option<String>,
|
|
68
|
+
start: usize,
|
|
69
|
+
end: usize,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const KIND_OBJECT: u8 = b'{';
|
|
73
|
+
|
|
74
|
+
/// Walk a container span with the pull Reader, collecting every direct
|
|
75
|
+
/// child's span (and decoded key). Mirrors the lazy-document children
|
|
76
|
+
/// walk; errors report doc-absolute positions.
|
|
77
|
+
fn container_children(
|
|
78
|
+
ruby: &Ruby,
|
|
79
|
+
doc: &[u8],
|
|
80
|
+
start: usize,
|
|
81
|
+
end: usize,
|
|
82
|
+
) -> Result<(u8, Vec<ChildSpan>), Error> {
|
|
83
|
+
let span = &doc[start..end];
|
|
84
|
+
let kind = span.first().copied().unwrap_or(0);
|
|
85
|
+
let walk: Result<Vec<ChildSpan>, nosj::ParseError> = PULL_STATE.with(|cell| {
|
|
86
|
+
let mut state = cell.borrow_mut();
|
|
87
|
+
// SAFETY: doc validated UTF-8 by the entry; spans lie on token
|
|
88
|
+
// edges.
|
|
89
|
+
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(span, &mut state.bufs) };
|
|
90
|
+
r.next_node()?;
|
|
91
|
+
let mut out = Vec::new();
|
|
92
|
+
if kind == KIND_OBJECT {
|
|
93
|
+
let mut key = r.object_first_key()?.map(String::from);
|
|
94
|
+
while let Some(k) = key {
|
|
95
|
+
let sub = r.skip_value()?;
|
|
96
|
+
let (s, e) = span_of(span, sub.as_bytes());
|
|
97
|
+
out.push(ChildSpan {
|
|
98
|
+
key: Some(k),
|
|
99
|
+
start: start + s,
|
|
100
|
+
end: start + e,
|
|
101
|
+
});
|
|
102
|
+
key = r.object_next_key()?.map(String::from);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
let mut has = r.array_first()?;
|
|
106
|
+
while has {
|
|
107
|
+
let sub = r.skip_value()?;
|
|
108
|
+
let (s, e) = span_of(span, sub.as_bytes());
|
|
109
|
+
out.push(ChildSpan {
|
|
110
|
+
key: None,
|
|
111
|
+
start: start + s,
|
|
112
|
+
end: start + e,
|
|
113
|
+
});
|
|
114
|
+
has = r.array_next()?;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
Ok(out)
|
|
118
|
+
});
|
|
119
|
+
let children = walk.map_err(|e| parser_error_at(ruby, doc, start + e.offset, e.to_string()))?;
|
|
120
|
+
Ok((kind, children))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Split an RFC 6901 pointer into (parent, unescaped last token).
|
|
124
|
+
/// Returns None for the root pointer "".
|
|
125
|
+
fn split_pointer(pointer: &str) -> Option<(&str, String)> {
|
|
126
|
+
let cut = pointer.rfind('/')?;
|
|
127
|
+
let token = pointer[cut + 1..].replace("~1", "/").replace("~0", "~");
|
|
128
|
+
Some((&pointer[..cut], token))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// RFC 6902 array index token: digits only, no leading zeros.
|
|
132
|
+
fn parse_index(token: &str) -> Option<usize> {
|
|
133
|
+
if token.is_empty() || (token.len() > 1 && token.starts_with('0')) {
|
|
134
|
+
return None;
|
|
135
|
+
}
|
|
136
|
+
if !token.bytes().all(|b| b.is_ascii_digit()) {
|
|
137
|
+
return None;
|
|
138
|
+
}
|
|
139
|
+
token.parse().ok()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn skip_ws(doc: &[u8], mut pos: usize) -> usize {
|
|
143
|
+
while pos < doc.len() && WS.contains(&doc[pos]) {
|
|
144
|
+
pos += 1;
|
|
145
|
+
}
|
|
146
|
+
pos
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Start of entry `i`'s bytes (the key quote for objects, the value
|
|
150
|
+
/// for arrays): first content after the opening bracket for entry 0,
|
|
151
|
+
/// after the separating comma otherwise. Pure byte scanning over a
|
|
152
|
+
/// region the Reader walk just validated.
|
|
153
|
+
fn entry_start(doc: &[u8], container_start: usize, children: &[ChildSpan], i: usize) -> usize {
|
|
154
|
+
if i == 0 {
|
|
155
|
+
skip_ws(doc, container_start + 1)
|
|
156
|
+
} else {
|
|
157
|
+
let after_prev = skip_ws(doc, children[i - 1].end);
|
|
158
|
+
// after_prev sits on the ',' between entries.
|
|
159
|
+
skip_ws(doc, after_prev + 1)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// What goes into the document at a splice point.
|
|
164
|
+
enum Insert {
|
|
165
|
+
/// Generate this Ruby value through the gen machinery.
|
|
166
|
+
Value(Value),
|
|
167
|
+
/// Pre-rendered bytes: move/copy splice the source span verbatim,
|
|
168
|
+
/// and entry insertions carry their key and comma.
|
|
169
|
+
Owned(Vec<u8>),
|
|
170
|
+
Nothing,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Rebuild `doc` with `[start, end)` replaced by `insert`.
|
|
174
|
+
fn apply_edit(
|
|
175
|
+
ruby: &Ruby,
|
|
176
|
+
doc: &[u8],
|
|
177
|
+
start: usize,
|
|
178
|
+
end: usize,
|
|
179
|
+
insert: &Insert,
|
|
180
|
+
cfg: &GenConfig,
|
|
181
|
+
) -> Result<Vec<u8>, Error> {
|
|
182
|
+
let mut out = Vec::with_capacity(doc.len() + 64);
|
|
183
|
+
out.extend_from_slice(&doc[..start]);
|
|
184
|
+
match insert {
|
|
185
|
+
Insert::Value(v) => gen::emit_into(ruby, *v, cfg, &mut out)?,
|
|
186
|
+
Insert::Owned(bytes) => out.extend_from_slice(bytes),
|
|
187
|
+
Insert::Nothing => {}
|
|
188
|
+
}
|
|
189
|
+
out.extend_from_slice(&doc[end..]);
|
|
190
|
+
Ok(out)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Render `"key":` plus the value bytes for an object entry insertion.
|
|
194
|
+
fn render_entry(ruby: &Ruby, key: &str, value: &Insert, cfg: &GenConfig) -> Result<Vec<u8>, Error> {
|
|
195
|
+
let mut bytes = Vec::with_capacity(key.len() + 16);
|
|
196
|
+
let key_str: Value = ruby.str_new(key).as_value();
|
|
197
|
+
gen::emit_into(ruby, key_str, cfg, &mut bytes)?;
|
|
198
|
+
bytes.push(b':');
|
|
199
|
+
match value {
|
|
200
|
+
Insert::Value(v) => gen::emit_into(ruby, *v, cfg, &mut bytes)?,
|
|
201
|
+
Insert::Owned(raw) => bytes.extend_from_slice(raw),
|
|
202
|
+
Insert::Nothing => unreachable!("entry insertions always carry a value"),
|
|
203
|
+
}
|
|
204
|
+
Ok(bytes)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// RFC 6902 `add`: object members insert-or-replace, array indices
|
|
208
|
+
/// insert before (with `-` appending), the root replaces the document.
|
|
209
|
+
fn op_add(
|
|
210
|
+
ruby: &Ruby,
|
|
211
|
+
doc: &[u8],
|
|
212
|
+
path: &str,
|
|
213
|
+
value: &Insert,
|
|
214
|
+
cfg: &GenConfig,
|
|
215
|
+
) -> Result<Vec<u8>, Error> {
|
|
216
|
+
let Some((parent, token)) = split_pointer(path) else {
|
|
217
|
+
// Root: the value becomes the entire document.
|
|
218
|
+
return apply_edit(ruby, doc, 0, doc.len(), value, cfg);
|
|
219
|
+
};
|
|
220
|
+
let Some((pstart, pend)) = span_at(ruby, doc, parent)? else {
|
|
221
|
+
return Err(patch_error(
|
|
222
|
+
ruby,
|
|
223
|
+
format!("add target parent {parent:?} does not exist"),
|
|
224
|
+
));
|
|
225
|
+
};
|
|
226
|
+
let (kind, children) = container_children(ruby, doc, pstart, pend)?;
|
|
227
|
+
if kind == KIND_OBJECT {
|
|
228
|
+
if let Some(i) = children
|
|
229
|
+
.iter()
|
|
230
|
+
.position(|c| c.key.as_deref() == Some(&*token))
|
|
231
|
+
{
|
|
232
|
+
// Existing member: add replaces its value.
|
|
233
|
+
return apply_edit(ruby, doc, children[i].start, children[i].end, value, cfg);
|
|
234
|
+
}
|
|
235
|
+
let mut entry = render_entry(ruby, &token, value, cfg)?;
|
|
236
|
+
if let Some(last) = children.last() {
|
|
237
|
+
let mut bytes = Vec::with_capacity(entry.len() + 1);
|
|
238
|
+
bytes.push(b',');
|
|
239
|
+
bytes.append(&mut entry);
|
|
240
|
+
apply_edit(ruby, doc, last.end, last.end, &Insert::Owned(bytes), cfg)
|
|
241
|
+
} else {
|
|
242
|
+
// Empty object: insert just before the closing brace.
|
|
243
|
+
apply_edit(ruby, doc, pend - 1, pend - 1, &Insert::Owned(entry), cfg)
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
let index = if token == "-" {
|
|
247
|
+
children.len()
|
|
248
|
+
} else {
|
|
249
|
+
parse_index(&token).ok_or_else(|| {
|
|
250
|
+
patch_error(ruby, format!("invalid array index {token:?} in {path:?}"))
|
|
251
|
+
})?
|
|
252
|
+
};
|
|
253
|
+
if index > children.len() {
|
|
254
|
+
return Err(patch_error(
|
|
255
|
+
ruby,
|
|
256
|
+
format!("array index {index} out of range in {path:?}"),
|
|
257
|
+
));
|
|
258
|
+
}
|
|
259
|
+
if index == children.len() {
|
|
260
|
+
// Append (also the empty-array case).
|
|
261
|
+
if let Some(last) = children.last() {
|
|
262
|
+
let mut bytes = vec![b','];
|
|
263
|
+
extend_insert(ruby, &mut bytes, value, cfg)?;
|
|
264
|
+
apply_edit(ruby, doc, last.end, last.end, &Insert::Owned(bytes), cfg)
|
|
265
|
+
} else {
|
|
266
|
+
let mut bytes = Vec::new();
|
|
267
|
+
extend_insert(ruby, &mut bytes, value, cfg)?;
|
|
268
|
+
apply_edit(ruby, doc, pend - 1, pend - 1, &Insert::Owned(bytes), cfg)
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
// Insert before element `index`.
|
|
272
|
+
let at = children[index].start;
|
|
273
|
+
let mut bytes = Vec::new();
|
|
274
|
+
extend_insert(ruby, &mut bytes, value, cfg)?;
|
|
275
|
+
bytes.push(b',');
|
|
276
|
+
apply_edit(ruby, doc, at, at, &Insert::Owned(bytes), cfg)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn extend_insert(
|
|
282
|
+
ruby: &Ruby,
|
|
283
|
+
bytes: &mut Vec<u8>,
|
|
284
|
+
value: &Insert,
|
|
285
|
+
cfg: &GenConfig,
|
|
286
|
+
) -> Result<(), Error> {
|
|
287
|
+
match value {
|
|
288
|
+
Insert::Value(v) => gen::emit_into(ruby, *v, cfg, bytes),
|
|
289
|
+
Insert::Owned(raw) => {
|
|
290
|
+
bytes.extend_from_slice(raw);
|
|
291
|
+
Ok(())
|
|
292
|
+
}
|
|
293
|
+
Insert::Nothing => unreachable!("insertions always carry a value"),
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// RFC 6902 `remove`: the entry disappears along with its key and one
|
|
298
|
+
/// separating comma; surrounding formatting stays.
|
|
299
|
+
fn op_remove(ruby: &Ruby, doc: &[u8], path: &str) -> Result<Vec<u8>, Error> {
|
|
300
|
+
let Some((parent, token)) = split_pointer(path) else {
|
|
301
|
+
return Err(patch_error(ruby, "cannot remove the root".into()));
|
|
302
|
+
};
|
|
303
|
+
let Some((pstart, pend)) = span_at(ruby, doc, parent)? else {
|
|
304
|
+
return Err(patch_error(
|
|
305
|
+
ruby,
|
|
306
|
+
format!("remove target parent {parent:?} does not exist"),
|
|
307
|
+
));
|
|
308
|
+
};
|
|
309
|
+
let (kind, children) = container_children(ruby, doc, pstart, pend)?;
|
|
310
|
+
let i = if kind == KIND_OBJECT {
|
|
311
|
+
children
|
|
312
|
+
.iter()
|
|
313
|
+
.position(|c| c.key.as_deref() == Some(&*token))
|
|
314
|
+
} else {
|
|
315
|
+
parse_index(&token).filter(|&i| i < children.len())
|
|
316
|
+
};
|
|
317
|
+
let Some(i) = i else {
|
|
318
|
+
return Err(patch_error(
|
|
319
|
+
ruby,
|
|
320
|
+
format!("remove target {path:?} does not exist"),
|
|
321
|
+
));
|
|
322
|
+
};
|
|
323
|
+
let (rs, re) = if children.len() == 1 {
|
|
324
|
+
(pstart + 1, pend - 1)
|
|
325
|
+
} else if i + 1 < children.len() {
|
|
326
|
+
(
|
|
327
|
+
entry_start(doc, pstart, &children, i),
|
|
328
|
+
entry_start(doc, pstart, &children, i + 1),
|
|
329
|
+
)
|
|
330
|
+
} else {
|
|
331
|
+
(children[i - 1].end, children[i].end)
|
|
332
|
+
};
|
|
333
|
+
apply_edit(
|
|
334
|
+
ruby,
|
|
335
|
+
doc,
|
|
336
|
+
rs,
|
|
337
|
+
re,
|
|
338
|
+
&Insert::Nothing,
|
|
339
|
+
&gen::opts::DEFAULT_CONFIG,
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// Deep JSON equality for `test`: the target span materializes and
|
|
344
|
+
/// compares with Ruby `==` (JSON types map onto Ruby ones 1:1).
|
|
345
|
+
fn op_test(ruby: &Ruby, doc: &[u8], path: &str, expected: Value) -> Result<(), Error> {
|
|
346
|
+
let Some((start, end)) = span_at(ruby, doc, path)? else {
|
|
347
|
+
return Err(patch_error(
|
|
348
|
+
ruby,
|
|
349
|
+
format!("test target {path:?} does not exist"),
|
|
350
|
+
));
|
|
351
|
+
};
|
|
352
|
+
let actual = materialize_at(ruby, doc, start, end, &ParseNativeOpts::default())?;
|
|
353
|
+
let equal: bool = actual.funcall("==", (expected,))?;
|
|
354
|
+
if !equal {
|
|
355
|
+
return Err(patch_error(ruby, format!("test failed at {path:?}")));
|
|
356
|
+
}
|
|
357
|
+
Ok(())
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// `NOSJ.splice(json, edits, opts)`: batch pointer replacement. All
|
|
361
|
+
/// targets resolve in ONE forward pass; the output is built in one
|
|
362
|
+
/// sweep copying every byte outside the target spans untouched.
|
|
363
|
+
pub fn splice_native(
|
|
364
|
+
ruby: &Ruby,
|
|
365
|
+
_rb_self: Value,
|
|
366
|
+
data: RString,
|
|
367
|
+
edits: RHash,
|
|
368
|
+
opts: Value,
|
|
369
|
+
) -> Result<RString, Error> {
|
|
370
|
+
let mut slot = None;
|
|
371
|
+
let cfg = gen_config(ruby, opts, &mut slot)?;
|
|
372
|
+
let input = utf8_input(ruby, &data)?;
|
|
373
|
+
|
|
374
|
+
let mut pointers: Vec<String> = Vec::with_capacity(edits.len());
|
|
375
|
+
let mut values: Vec<Value> = Vec::with_capacity(edits.len());
|
|
376
|
+
edits.foreach(|k: Value, v: Value| {
|
|
377
|
+
let ptr = RString::from_value(k)
|
|
378
|
+
.ok_or_else(|| arg_error(ruby, "splice pointers must be Strings".into()))?;
|
|
379
|
+
pointers.push(ptr.to_string()?);
|
|
380
|
+
values.push(v);
|
|
381
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
382
|
+
})?;
|
|
383
|
+
|
|
384
|
+
let refs: Vec<&str> = pointers.iter().map(String::as_str).collect();
|
|
385
|
+
let resolved = PULL_STATE.with(|cell| {
|
|
386
|
+
let mut state = cell.borrow_mut();
|
|
387
|
+
// SAFETY: coderange verified by utf8_input.
|
|
388
|
+
unsafe { nosj::pointers_utf8_unchecked(input, &refs, &mut state.bufs) }
|
|
389
|
+
});
|
|
390
|
+
let hits = match resolved {
|
|
391
|
+
Ok(hits) => hits,
|
|
392
|
+
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
393
|
+
return Err(arg_error(ruby, e.to_string()));
|
|
394
|
+
}
|
|
395
|
+
Err(e) => return Err(parser_error_at(ruby, input, e.offset, e.to_string())),
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
let mut spans: Vec<(usize, usize, Value)> = Vec::with_capacity(pointers.len());
|
|
399
|
+
for (i, hit) in hits.into_iter().enumerate() {
|
|
400
|
+
let Some(slice) = hit else {
|
|
401
|
+
let exc: ExceptionClass = ruby.exception_key_error();
|
|
402
|
+
return Err(Error::new(
|
|
403
|
+
exc,
|
|
404
|
+
format!("pointer {:?} does not resolve", pointers[i]),
|
|
405
|
+
));
|
|
406
|
+
};
|
|
407
|
+
let (s, e) = span_of(input, slice.as_bytes());
|
|
408
|
+
spans.push((s, e, values[i]));
|
|
409
|
+
}
|
|
410
|
+
spans.sort_unstable_by_key(|&(s, _, _)| s);
|
|
411
|
+
for pair in spans.windows(2) {
|
|
412
|
+
if pair[1].0 < pair[0].1 {
|
|
413
|
+
return Err(arg_error(
|
|
414
|
+
ruby,
|
|
415
|
+
"splice targets overlap (one pointer addresses a value inside another)".into(),
|
|
416
|
+
));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let mut out = Vec::with_capacity(input.len() + 64);
|
|
421
|
+
let mut pos = 0;
|
|
422
|
+
for &(s, e, v) in &spans {
|
|
423
|
+
out.extend_from_slice(&input[pos..s]);
|
|
424
|
+
gen::emit_into(ruby, v, cfg, &mut out)?;
|
|
425
|
+
pos = e;
|
|
426
|
+
}
|
|
427
|
+
out.extend_from_slice(&input[pos..]);
|
|
428
|
+
finish_string(&out)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
pub(crate) fn finish_string(bytes: &[u8]) -> Result<RString, Error> {
|
|
432
|
+
use magnus::rb_sys::FromRawValue;
|
|
433
|
+
Ok(unsafe {
|
|
434
|
+
RString::from_value(Value::from_raw(rb_sys::rb_utf8_str_new(
|
|
435
|
+
bytes.as_ptr().cast(),
|
|
436
|
+
bytes.len() as std::os::raw::c_long,
|
|
437
|
+
)))
|
|
438
|
+
.expect("rb_utf8_str_new returns a String")
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// Fetch a patch-operation field by String or Symbol key. An absent
|
|
443
|
+
/// key is None; an explicit null stays Some (RFC 6902 allows
|
|
444
|
+
/// `"value": null`).
|
|
445
|
+
fn op_field(ruby: &Ruby, op: RHash, name: &str) -> Option<Value> {
|
|
446
|
+
op.get(name).or_else(|| op.get(ruby.to_symbol(name)))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn op_str_field(ruby: &Ruby, op: RHash, name: &str) -> Result<Option<String>, Error> {
|
|
450
|
+
match op_field(ruby, op, name) {
|
|
451
|
+
None => Ok(None),
|
|
452
|
+
Some(v) => {
|
|
453
|
+
let s = RString::from_value(v)
|
|
454
|
+
.ok_or_else(|| arg_error(ruby, format!("patch op {name:?} must be a String")))?;
|
|
455
|
+
Ok(Some(s.to_string()?))
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/// `NOSJ.patch(json, ops, opts)`: RFC 6902, applied sequentially over
|
|
461
|
+
/// the evolving raw document.
|
|
462
|
+
pub fn patch_native(
|
|
463
|
+
ruby: &Ruby,
|
|
464
|
+
_rb_self: Value,
|
|
465
|
+
data: RString,
|
|
466
|
+
ops: RArray,
|
|
467
|
+
opts: Value,
|
|
468
|
+
) -> Result<RString, Error> {
|
|
469
|
+
let mut slot = None;
|
|
470
|
+
let cfg = gen_config(ruby, opts, &mut slot)?;
|
|
471
|
+
let input = utf8_input(ruby, &data)?;
|
|
472
|
+
let mut doc: Vec<u8> = input.to_vec();
|
|
473
|
+
|
|
474
|
+
for i in 0..ops.len() {
|
|
475
|
+
let entry: Value = ops.entry(i as isize)?;
|
|
476
|
+
let op = RHash::from_value(entry)
|
|
477
|
+
.ok_or_else(|| arg_error(ruby, format!("patch op {i} is not a Hash")))?;
|
|
478
|
+
let name = op_str_field(ruby, op, "op")?
|
|
479
|
+
.ok_or_else(|| arg_error(ruby, format!("patch op {i} is missing \"op\"")))?;
|
|
480
|
+
let path = op_str_field(ruby, op, "path")?
|
|
481
|
+
.ok_or_else(|| arg_error(ruby, format!("patch op {i} is missing \"path\"")))?;
|
|
482
|
+
let value = || {
|
|
483
|
+
op_field(ruby, op, "value").ok_or_else(|| {
|
|
484
|
+
arg_error(ruby, format!("patch op {i} ({name}) is missing \"value\""))
|
|
485
|
+
})
|
|
486
|
+
};
|
|
487
|
+
let from = || {
|
|
488
|
+
op_str_field(ruby, op, "from")?.ok_or_else(|| {
|
|
489
|
+
arg_error(ruby, format!("patch op {i} ({name}) is missing \"from\""))
|
|
490
|
+
})
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
doc = match name.as_str() {
|
|
494
|
+
"add" => op_add(ruby, &doc, &path, &Insert::Value(value()?), cfg)?,
|
|
495
|
+
"replace" => {
|
|
496
|
+
let Some((s, e)) = span_at(ruby, &doc, &path)? else {
|
|
497
|
+
return Err(patch_error(
|
|
498
|
+
ruby,
|
|
499
|
+
format!("replace target {path:?} does not exist"),
|
|
500
|
+
));
|
|
501
|
+
};
|
|
502
|
+
apply_edit(ruby, &doc, s, e, &Insert::Value(value()?), cfg)?
|
|
503
|
+
}
|
|
504
|
+
"remove" => op_remove(ruby, &doc, &path)?,
|
|
505
|
+
"move" => {
|
|
506
|
+
let from = from()?;
|
|
507
|
+
if path == from {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if path.starts_with(&format!("{from}/")) {
|
|
511
|
+
return Err(patch_error(
|
|
512
|
+
ruby,
|
|
513
|
+
format!("cannot move {from:?} into its own child {path:?}"),
|
|
514
|
+
));
|
|
515
|
+
}
|
|
516
|
+
let Some((s, e)) = span_at(ruby, &doc, &from)? else {
|
|
517
|
+
return Err(patch_error(
|
|
518
|
+
ruby,
|
|
519
|
+
format!("move source {from:?} does not exist"),
|
|
520
|
+
));
|
|
521
|
+
};
|
|
522
|
+
let raw = doc[s..e].to_vec();
|
|
523
|
+
let removed = op_remove(ruby, &doc, &from)?;
|
|
524
|
+
op_add(ruby, &removed, &path, &Insert::Owned(raw), cfg)?
|
|
525
|
+
}
|
|
526
|
+
"copy" => {
|
|
527
|
+
let from = from()?;
|
|
528
|
+
let Some((s, e)) = span_at(ruby, &doc, &from)? else {
|
|
529
|
+
return Err(patch_error(
|
|
530
|
+
ruby,
|
|
531
|
+
format!("copy source {from:?} does not exist"),
|
|
532
|
+
));
|
|
533
|
+
};
|
|
534
|
+
let raw = doc[s..e].to_vec();
|
|
535
|
+
op_add(ruby, &doc, &path, &Insert::Owned(raw), cfg)?
|
|
536
|
+
}
|
|
537
|
+
"test" => {
|
|
538
|
+
op_test(ruby, &doc, &path, value()?)?;
|
|
539
|
+
doc
|
|
540
|
+
}
|
|
541
|
+
other => {
|
|
542
|
+
return Err(arg_error(ruby, format!("unknown patch op {other:?}")));
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
finish_string(&doc)
|
|
547
|
+
}
|
data/ext/nosj/src/pointer.rs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
use magnus::{Error, RString, Ruby, Value};
|
|
7
7
|
|
|
8
|
-
use crate::
|
|
8
|
+
use crate::errors::parser_error_at;
|
|
9
|
+
use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
|
|
9
10
|
use crate::state::PULL_STATE;
|
|
10
11
|
|
|
11
12
|
/// Resolve one JSON Pointer against `data`, materializing the matched
|
|
@@ -27,11 +28,14 @@ fn at_pointer_impl(
|
|
|
27
28
|
});
|
|
28
29
|
match resolved {
|
|
29
30
|
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
30
|
-
Ok(Some(slice)) =>
|
|
31
|
+
Ok(Some(slice)) => {
|
|
32
|
+
let (start, end) = span_of(input, slice.as_bytes());
|
|
33
|
+
materialize_at(ruby, input, start, end, o)
|
|
34
|
+
}
|
|
31
35
|
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
32
36
|
Err(Error::new(ruby.exception_arg_error(), e.to_string()))
|
|
33
37
|
}
|
|
34
|
-
Err(e) => Err(
|
|
38
|
+
Err(e) => Err(parser_error_at(ruby, input, e.offset, e.to_string())),
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -133,7 +137,7 @@ fn at_pointers_impl(
|
|
|
133
137
|
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
134
138
|
return Err(Error::new(ruby.exception_arg_error(), e.to_string()));
|
|
135
139
|
}
|
|
136
|
-
Err(e) => return Err(
|
|
140
|
+
Err(e) => return Err(parser_error_at(ruby, input, e.offset, e.to_string())),
|
|
137
141
|
};
|
|
138
142
|
|
|
139
143
|
let out = ruby.ary_new_capa(pointers.len());
|
|
@@ -144,7 +148,10 @@ fn at_pointers_impl(
|
|
|
144
148
|
None
|
|
145
149
|
};
|
|
146
150
|
match hit {
|
|
147
|
-
Some(slice) =>
|
|
151
|
+
Some(slice) => {
|
|
152
|
+
let (start, end) = span_of(input, slice.as_bytes());
|
|
153
|
+
out.push(materialize_at(ruby, input, start, end, o)?)?;
|
|
154
|
+
}
|
|
148
155
|
None => out.push(ruby.qnil())?,
|
|
149
156
|
}
|
|
150
157
|
}
|