nosj 0.1.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 +7 -0
- data/CHANGELOG.md +10 -0
- data/Cargo.lock +375 -0
- data/Cargo.toml +27 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +13 -0
- data/README.md +214 -0
- data/ext/nosj/Cargo.toml +25 -0
- data/ext/nosj/extconf.rb +6 -0
- data/ext/nosj/src/gen/errors.rs +61 -0
- data/ext/nosj/src/gen/hash_iter.rs +53 -0
- data/ext/nosj/src/gen/keys.rs +58 -0
- data/ext/nosj/src/gen/mod.rs +171 -0
- data/ext/nosj/src/gen/opts.rs +162 -0
- data/ext/nosj/src/gen/ruby.rs +100 -0
- data/ext/nosj/src/gen/walker.rs +638 -0
- data/ext/nosj/src/lib.rs +56 -0
- data/ext/nosj/src/parse.rs +211 -0
- data/ext/nosj/src/pointer.rs +190 -0
- data/ext/nosj/src/sink.rs +370 -0
- data/ext/nosj/src/state.rs +89 -0
- data/lib/nosj/json.rb +124 -0
- data/lib/nosj/multi_json.rb +51 -0
- data/lib/nosj/native.rb +11 -0
- data/lib/nosj/version.rb +6 -0
- data/lib/nosj.rb +193 -0
- data/sig/nosj.rbs +61 -0
- metadata +92 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
//! Whole-document entry points: `NOSJ.parse` (fused cursor),
|
|
2
|
+
//! `NOSJ.valid?` (null sink), and the unregistered GVL-releasing
|
|
3
|
+
//! indexed parse, plus the shared option decoding, input gating, and
|
|
4
|
+
//! drive-result mapping they all use.
|
|
5
|
+
|
|
6
|
+
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
7
|
+
use magnus::{Error, RString, Ruby, Value};
|
|
8
|
+
|
|
9
|
+
use crate::sink::{NullSink, RubyValueSink, SinkAbort, MAX_NESTING};
|
|
10
|
+
use crate::state::{ensure_marked_shadow, PullState, PULL_STATE};
|
|
11
|
+
|
|
12
|
+
#[cold]
|
|
13
|
+
pub(crate) fn err(ruby: &Ruby, msg: String) -> Error {
|
|
14
|
+
Error::new(ruby.exception_runtime_error(), msg)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Validate that `data` is UTF-8 (or US-ASCII) with intact coderange and
|
|
18
|
+
/// hand out its byte slice.
|
|
19
|
+
pub(crate) fn utf8_input<'a>(ruby: &Ruby, data: &'a RString) -> Result<&'a [u8], Error> {
|
|
20
|
+
let raw = data.as_raw();
|
|
21
|
+
unsafe {
|
|
22
|
+
let enc = rb_sys::rb_enc_get_index(raw);
|
|
23
|
+
if enc != rb_sys::rb_utf8_encindex() && enc != rb_sys::rb_usascii_encindex() {
|
|
24
|
+
return Err(err(ruby, "input must be UTF-8 encoded".into()));
|
|
25
|
+
}
|
|
26
|
+
if rb_sys::rb_enc_str_coderange(raw)
|
|
27
|
+
== rb_sys::ruby_coderange_type::RUBY_ENC_CODERANGE_BROKEN as std::os::raw::c_int
|
|
28
|
+
{
|
|
29
|
+
return Err(err(ruby, "input is not valid UTF-8".into()));
|
|
30
|
+
}
|
|
31
|
+
Ok(data.as_slice())
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// JSON.parse-compatible options, decoded once from the Ruby hash and
|
|
36
|
+
/// shared by every entry point that materializes (or validates) values.
|
|
37
|
+
#[derive(Clone, Copy)]
|
|
38
|
+
pub(crate) struct ParseNativeOpts {
|
|
39
|
+
pub(crate) symbolize: bool,
|
|
40
|
+
pub(crate) freeze: bool,
|
|
41
|
+
pub(crate) max_nesting: usize,
|
|
42
|
+
pub(crate) popts: nosj::ParseOptions,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
impl Default for ParseNativeOpts {
|
|
46
|
+
fn default() -> Self {
|
|
47
|
+
Self {
|
|
48
|
+
symbolize: false,
|
|
49
|
+
freeze: false,
|
|
50
|
+
max_nesting: MAX_NESTING,
|
|
51
|
+
popts: nosj::ParseOptions::default(),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Decode a JSON.parse-compatible options hash: symbolize_names, freeze,
|
|
57
|
+
/// max_nesting, allow_nan, allow_trailing_comma. Unsupported gem options
|
|
58
|
+
/// (object_class, array_class, decimal_class, create_additions) raise.
|
|
59
|
+
pub(crate) fn parse_native_opts(ruby: &Ruby, opts: Value) -> Result<ParseNativeOpts, Error> {
|
|
60
|
+
use magnus::value::ReprValue;
|
|
61
|
+
use magnus::RHash;
|
|
62
|
+
|
|
63
|
+
let mut out = ParseNativeOpts::default();
|
|
64
|
+
if opts.is_nil() {
|
|
65
|
+
return Ok(out);
|
|
66
|
+
}
|
|
67
|
+
let h = RHash::from_value(opts)
|
|
68
|
+
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "options must be a Hash"))?;
|
|
69
|
+
|
|
70
|
+
let truthy = |name: &str| -> bool {
|
|
71
|
+
h.get(ruby.to_symbol(name))
|
|
72
|
+
.is_some_and(|v: Value| v.to_bool())
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
out.symbolize = truthy("symbolize_names");
|
|
76
|
+
out.freeze = truthy("freeze");
|
|
77
|
+
out.popts.allow_nan = truthy("allow_nan");
|
|
78
|
+
out.popts.allow_trailing_comma = truthy("allow_trailing_comma");
|
|
79
|
+
|
|
80
|
+
if let Some(mn) = h.get(ruby.to_symbol("max_nesting")) {
|
|
81
|
+
out.max_nesting =
|
|
82
|
+
if mn.is_nil() || mn.to_bool() && magnus::Integer::from_value(mn).is_none() {
|
|
83
|
+
MAX_NESTING // nil / true: gem default
|
|
84
|
+
} else if !mn.to_bool() {
|
|
85
|
+
usize::MAX // false: unlimited
|
|
86
|
+
} else {
|
|
87
|
+
magnus::Integer::from_value(mn)
|
|
88
|
+
.and_then(|i| i.to_u64().ok())
|
|
89
|
+
.map_or(MAX_NESTING, |n| n as usize)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for unsupported in [
|
|
94
|
+
"object_class",
|
|
95
|
+
"array_class",
|
|
96
|
+
"decimal_class",
|
|
97
|
+
"create_additions",
|
|
98
|
+
] {
|
|
99
|
+
if truthy(unsupported) {
|
|
100
|
+
return Err(Error::new(
|
|
101
|
+
ruby.exception_arg_error(),
|
|
102
|
+
format!("NOSJ.parse does not support the {unsupported} option"),
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
Ok(out)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Pop the root value off the sink stack, or map a drive failure onto
|
|
110
|
+
/// the gem's exceptions. Shared by every driver.
|
|
111
|
+
fn finish_drive(
|
|
112
|
+
ruby: &Ruby,
|
|
113
|
+
result: Result<(), nosj::DriveError<SinkAbort>>,
|
|
114
|
+
stack: &mut Vec<rb_sys::VALUE>,
|
|
115
|
+
max_nesting: usize,
|
|
116
|
+
) -> Result<Value, Error> {
|
|
117
|
+
match result {
|
|
118
|
+
Ok(()) => {
|
|
119
|
+
let raw = stack
|
|
120
|
+
.pop()
|
|
121
|
+
.unwrap_or(rb_sys::special_consts::Qnil as rb_sys::VALUE);
|
|
122
|
+
Ok(unsafe { Value::from_raw(raw) })
|
|
123
|
+
}
|
|
124
|
+
Err(nosj::DriveError::Sink(SinkAbort::Overflow)) => {
|
|
125
|
+
Err(err(ruby, "document too large".into()))
|
|
126
|
+
}
|
|
127
|
+
Err(nosj::DriveError::Sink(SinkAbort::BadBigint)) => {
|
|
128
|
+
Err(err(ruby, "invalid bignum".into()))
|
|
129
|
+
}
|
|
130
|
+
Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(err(
|
|
131
|
+
ruby,
|
|
132
|
+
format!("nesting of {} is too deep", max_nesting.saturating_add(1)),
|
|
133
|
+
)),
|
|
134
|
+
Err(nosj::DriveError::Parse(e)) => Err(err(ruby, e.to_string())),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
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> {
|
|
142
|
+
PULL_STATE.with(|cell| {
|
|
143
|
+
let mut state = cell.borrow_mut();
|
|
144
|
+
ensure_marked_shadow(&mut state.vstack);
|
|
145
|
+
ensure_marked_shadow(&mut state.key_shadow);
|
|
146
|
+
|
|
147
|
+
let PullState {
|
|
148
|
+
bufs,
|
|
149
|
+
keys,
|
|
150
|
+
sym_keys,
|
|
151
|
+
vstack,
|
|
152
|
+
key_shadow,
|
|
153
|
+
..
|
|
154
|
+
} = &mut *state;
|
|
155
|
+
let stack = &mut vstack.as_mut().unwrap().values;
|
|
156
|
+
stack.clear();
|
|
157
|
+
|
|
158
|
+
let mut sink = RubyValueSink {
|
|
159
|
+
stack,
|
|
160
|
+
keys: if o.symbolize { sym_keys } else { keys },
|
|
161
|
+
key_shadow: key_shadow.as_deref_mut().unwrap(),
|
|
162
|
+
depth: 0,
|
|
163
|
+
symbolize: o.symbolize,
|
|
164
|
+
freeze: o.freeze,
|
|
165
|
+
max_nesting: o.max_nesting,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// 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)
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// JSON.parse-compatible entry (see [`parse_native_opts`] for options).
|
|
175
|
+
pub fn parse_native(
|
|
176
|
+
ruby: &Ruby,
|
|
177
|
+
_rb_self: Value,
|
|
178
|
+
data: RString,
|
|
179
|
+
opts: Value,
|
|
180
|
+
) -> Result<Value, Error> {
|
|
181
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
182
|
+
let input = utf8_input(ruby, &data)?;
|
|
183
|
+
materialize(ruby, input, &o)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// `NOSJ.valid?(source, opts)` returns true iff `NOSJ.parse` would
|
|
187
|
+
/// succeed under the same options. Parse refusals (malformed JSON, bad
|
|
188
|
+
/// encoding, too-deep nesting) return false; option and argument-type
|
|
189
|
+
/// errors still raise exactly like `parse`.
|
|
190
|
+
pub fn valid_native(
|
|
191
|
+
ruby: &Ruby,
|
|
192
|
+
_rb_self: Value,
|
|
193
|
+
data: RString,
|
|
194
|
+
opts: Value,
|
|
195
|
+
) -> Result<bool, Error> {
|
|
196
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
197
|
+
let Ok(input) = utf8_input(ruby, &data) else {
|
|
198
|
+
return Ok(false);
|
|
199
|
+
};
|
|
200
|
+
let ok = PULL_STATE.with(|cell| {
|
|
201
|
+
let mut state = cell.borrow_mut();
|
|
202
|
+
let mut sink = NullSink {
|
|
203
|
+
depth: 0,
|
|
204
|
+
max_nesting: o.max_nesting,
|
|
205
|
+
};
|
|
206
|
+
// Safety: coderange verified by utf8_input.
|
|
207
|
+
unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
|
|
208
|
+
.is_ok()
|
|
209
|
+
});
|
|
210
|
+
Ok(ok)
|
|
211
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//! Partial parsing: `NOSJ.dig`, `NOSJ.at_pointer`, and their batch
|
|
2
|
+
//! forms. Pointers resolve against the raw document through the nosj
|
|
3
|
+
//! crate's cursor-mode skipper; only the matched subtree materializes,
|
|
4
|
+
//! through the same sink machinery as a full parse.
|
|
5
|
+
|
|
6
|
+
use magnus::{Error, RString, Ruby, Value};
|
|
7
|
+
|
|
8
|
+
use crate::parse::{err, materialize, parse_native_opts, utf8_input, ParseNativeOpts};
|
|
9
|
+
use crate::state::PULL_STATE;
|
|
10
|
+
|
|
11
|
+
/// Resolve one JSON Pointer against `data`, materializing the matched
|
|
12
|
+
/// subtree; `nil` when the pointer misses.
|
|
13
|
+
fn at_pointer_impl(
|
|
14
|
+
ruby: &Ruby,
|
|
15
|
+
data: RString,
|
|
16
|
+
pointer: &str,
|
|
17
|
+
o: &ParseNativeOpts,
|
|
18
|
+
) -> Result<Value, Error> {
|
|
19
|
+
use magnus::value::ReprValue;
|
|
20
|
+
let input = utf8_input(ruby, &data)?;
|
|
21
|
+
// Resolve first (one PULL_STATE borrow), then materialize (a fresh
|
|
22
|
+
// borrow); the resolved slice borrows `input`, not the buffers.
|
|
23
|
+
let resolved = PULL_STATE.with(|cell| {
|
|
24
|
+
let mut state = cell.borrow_mut();
|
|
25
|
+
// Safety: coderange verified above.
|
|
26
|
+
unsafe { nosj::pointer_utf8_unchecked(input, pointer, &mut state.bufs) }
|
|
27
|
+
});
|
|
28
|
+
match resolved {
|
|
29
|
+
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
30
|
+
Ok(Some(slice)) => materialize(ruby, slice.as_bytes(), o),
|
|
31
|
+
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
32
|
+
Err(Error::new(ruby.exception_arg_error(), e.to_string()))
|
|
33
|
+
}
|
|
34
|
+
Err(e) => Err(err(ruby, e.to_string())),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// `NOSJ.at_pointer(source, "/a/b/0", opts)`: pointer lookup; the
|
|
39
|
+
/// matched subtree materializes under the same options as `parse`.
|
|
40
|
+
pub fn at_pointer_native(
|
|
41
|
+
ruby: &Ruby,
|
|
42
|
+
_rb_self: Value,
|
|
43
|
+
data: RString,
|
|
44
|
+
pointer: RString,
|
|
45
|
+
opts: Value,
|
|
46
|
+
) -> Result<Value, Error> {
|
|
47
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
48
|
+
let ptr = pointer.to_string()?;
|
|
49
|
+
at_pointer_impl(ruby, data, &ptr, &o)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Convert one dig path (String/Symbol keys, Integer indices) into a
|
|
53
|
+
/// JSON Pointer, `~`/`/` escaped. `None` = a negative index: no pointer
|
|
54
|
+
/// equivalent, the path resolves to `nil` by definition.
|
|
55
|
+
fn path_to_pointer(ruby: &Ruby, path: magnus::RArray) -> Result<Option<String>, Error> {
|
|
56
|
+
fn push_escaped_token(ptr: &mut String, key: &str) {
|
|
57
|
+
ptr.push('/');
|
|
58
|
+
for c in key.chars() {
|
|
59
|
+
match c {
|
|
60
|
+
'~' => ptr.push_str("~0"),
|
|
61
|
+
'/' => ptr.push_str("~1"),
|
|
62
|
+
c => ptr.push(c),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let mut ptr = String::new();
|
|
68
|
+
for i in 0..path.len() {
|
|
69
|
+
let item: Value = path.entry(i as isize)?;
|
|
70
|
+
if let Some(int) = magnus::Integer::from_value(item) {
|
|
71
|
+
let idx = int.to_i64()?;
|
|
72
|
+
if idx < 0 {
|
|
73
|
+
return Ok(None);
|
|
74
|
+
}
|
|
75
|
+
ptr.push('/');
|
|
76
|
+
ptr.push_str(&idx.to_string());
|
|
77
|
+
} else if let Some(s) = RString::from_value(item) {
|
|
78
|
+
push_escaped_token(&mut ptr, &s.to_string()?);
|
|
79
|
+
} else if let Some(sym) = magnus::Symbol::from_value(item) {
|
|
80
|
+
push_escaped_token(&mut ptr, &sym.name()?);
|
|
81
|
+
} else {
|
|
82
|
+
return Err(Error::new(
|
|
83
|
+
ruby.exception_arg_error(),
|
|
84
|
+
"path elements must be Strings, Symbols, or Integers",
|
|
85
|
+
));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
Ok(Some(ptr))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// `NOSJ.dig(source, *path)`: Hash#dig-shaped partial parsing.
|
|
92
|
+
/// Negative indices resolve to `nil` (JSON Pointer has no equivalent).
|
|
93
|
+
pub fn dig_native(
|
|
94
|
+
ruby: &Ruby,
|
|
95
|
+
_rb_self: Value,
|
|
96
|
+
data: RString,
|
|
97
|
+
path: magnus::RArray,
|
|
98
|
+
) -> Result<Value, Error> {
|
|
99
|
+
use magnus::value::ReprValue;
|
|
100
|
+
match path_to_pointer(ruby, path)? {
|
|
101
|
+
Some(ptr) => at_pointer_impl(ruby, data, &ptr, &ParseNativeOpts::default()),
|
|
102
|
+
None => Ok(ruby.qnil().as_value()),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Resolve many pointers in ONE forward pass (`nosj::pointers`: the
|
|
107
|
+
/// walk descends only into subtrees some pointer still needs, so a batch
|
|
108
|
+
/// costs about its single deepest query), then materialize each hit.
|
|
109
|
+
/// `None` entries (negative dig indices) come back as `nil` without ever
|
|
110
|
+
/// reaching the resolver.
|
|
111
|
+
fn at_pointers_impl(
|
|
112
|
+
ruby: &Ruby,
|
|
113
|
+
data: RString,
|
|
114
|
+
pointers: &[Option<String>],
|
|
115
|
+
o: &ParseNativeOpts,
|
|
116
|
+
) -> Result<Value, Error> {
|
|
117
|
+
use magnus::value::ReprValue;
|
|
118
|
+
|
|
119
|
+
let input = utf8_input(ruby, &data)?;
|
|
120
|
+
let live: Vec<&str> = pointers.iter().flatten().map(String::as_str).collect();
|
|
121
|
+
|
|
122
|
+
// Resolve first (one PULL_STATE borrow); the resolved slices borrow
|
|
123
|
+
// `input`, not the buffers, so materializing can re-borrow freely.
|
|
124
|
+
let resolved = PULL_STATE.with(|cell| {
|
|
125
|
+
let mut state = cell.borrow_mut();
|
|
126
|
+
// Safety: coderange verified by utf8_input.
|
|
127
|
+
unsafe { nosj::pointers_utf8_unchecked(input, &live, &mut state.bufs) }
|
|
128
|
+
});
|
|
129
|
+
let mut hits = match resolved {
|
|
130
|
+
Ok(hits) => hits.into_iter(),
|
|
131
|
+
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
132
|
+
return Err(Error::new(ruby.exception_arg_error(), e.to_string()));
|
|
133
|
+
}
|
|
134
|
+
Err(e) => return Err(err(ruby, e.to_string())),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
let out = ruby.ary_new_capa(pointers.len());
|
|
138
|
+
for ptr in pointers {
|
|
139
|
+
let hit = if ptr.is_some() {
|
|
140
|
+
hits.next().flatten()
|
|
141
|
+
} else {
|
|
142
|
+
None
|
|
143
|
+
};
|
|
144
|
+
match hit {
|
|
145
|
+
Some(slice) => out.push(materialize(ruby, slice.as_bytes(), o)?)?,
|
|
146
|
+
None => out.push(ruby.qnil())?,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
Ok(out.as_value())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// `NOSJ.at_pointers(source, pointers, opts)`: batch pointer lookup,
|
|
153
|
+
/// positionally aligned results.
|
|
154
|
+
pub fn at_pointers_native(
|
|
155
|
+
ruby: &Ruby,
|
|
156
|
+
_rb_self: Value,
|
|
157
|
+
data: RString,
|
|
158
|
+
pointers: magnus::RArray,
|
|
159
|
+
opts: Value,
|
|
160
|
+
) -> Result<Value, Error> {
|
|
161
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
162
|
+
let mut ptrs: Vec<Option<String>> = Vec::with_capacity(pointers.len());
|
|
163
|
+
for i in 0..pointers.len() {
|
|
164
|
+
let item: Value = pointers.entry(i as isize)?;
|
|
165
|
+
let s = RString::from_value(item)
|
|
166
|
+
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "pointers must be Strings"))?;
|
|
167
|
+
ptrs.push(Some(s.to_string()?));
|
|
168
|
+
}
|
|
169
|
+
at_pointers_impl(ruby, data, &ptrs, &o)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// `NOSJ.dig_many(source, paths, opts)`: batch dig, one resolver
|
|
173
|
+
/// pass for all paths.
|
|
174
|
+
pub fn dig_many_native(
|
|
175
|
+
ruby: &Ruby,
|
|
176
|
+
_rb_self: Value,
|
|
177
|
+
data: RString,
|
|
178
|
+
paths: magnus::RArray,
|
|
179
|
+
opts: Value,
|
|
180
|
+
) -> Result<Value, Error> {
|
|
181
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
182
|
+
let mut ptrs: Vec<Option<String>> = Vec::with_capacity(paths.len());
|
|
183
|
+
for i in 0..paths.len() {
|
|
184
|
+
let item: Value = paths.entry(i as isize)?;
|
|
185
|
+
let path = magnus::RArray::from_value(item)
|
|
186
|
+
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "each path must be an Array"))?;
|
|
187
|
+
ptrs.push(path_to_pointer(ruby, path)?);
|
|
188
|
+
}
|
|
189
|
+
at_pointers_impl(ruby, data, &ptrs, &o)
|
|
190
|
+
}
|