Skip to content

Commit feb39a0

Browse files
committed
feat(neon): Add Array extractor
1 parent 303eb3c commit feb39a0

4 files changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use std::{error, fmt, mem::MaybeUninit};
2+
3+
use crate::{
4+
context::{internal::ContextInternal, Context, Cx},
5+
handle::Handle,
6+
result::{JsResult, NeonResult, Throw},
7+
sys,
8+
types::{
9+
extract::{private, TryFromJs, TryIntoJs, TypeExpected},
10+
private::ValueInternal,
11+
JsArray, JsValue,
12+
},
13+
};
14+
15+
/// Extracts a [JavaScript array](JsArray) into a Rust collection or converts a collection to a JS array.
16+
///
17+
/// Any collection that implements [`FromIterator`] and [`IntoIterator`] can be extracted. Extraction
18+
/// fails with [`ArrayError`] if the value is not an array or an element fails to be extracted.
19+
///
20+
/// # Example
21+
///
22+
/// ```
23+
/// # use std::collections::HashSet;
24+
/// # use neon::types::extract::Array;
25+
/// #[neon::export]
26+
/// fn list_of_strings(Array(arr): Array<Vec<String>>) -> Array<Vec<String>> {
27+
/// Array(arr)
28+
/// }
29+
///
30+
/// #[neon::export]
31+
/// fn double(Array(arr): Array<Vec<f64>>) -> Array<impl Iterator<Item = f64>> {
32+
/// Array(arr.into_iter().map(|x| x * 2.0))
33+
/// }
34+
///
35+
/// #[neon::export]
36+
/// fn dedupe(set: Array<HashSet<String>>) -> Array<HashSet<String>> {
37+
/// set
38+
/// }
39+
/// ```
40+
///
41+
/// **Note**: Only native JS arrays are accepted. For typed arrays use [`Uint8Array`](super::Uint8Array),
42+
/// [`Float64Array`](super::Float64Array), etc.
43+
pub struct Array<T>(pub T);
44+
45+
impl<T> private::Sealed for Array<T> {}
46+
47+
impl<'cx, T> TryFromJs<'cx> for Array<T>
48+
where
49+
T: FromIterator<T::Item>,
50+
T: IntoIterator,
51+
T::Item: TryFromJs<'cx>,
52+
{
53+
type Error = ArrayError<<T::Item as TryFromJs<'cx>>::Error>;
54+
55+
fn try_from_js(
56+
cx: &mut Cx<'cx>,
57+
v: Handle<'cx, JsValue>,
58+
) -> NeonResult<Result<Self, Self::Error>> {
59+
let env = cx.env().to_raw();
60+
let v = v.to_local();
61+
let len = unsafe {
62+
let mut len = 0;
63+
64+
match sys::get_array_length(env, v, &mut len) {
65+
Err(sys::Status::PendingException) => return Err(Throw::new()),
66+
Err(sys::Status::ArrayExpected) => return Ok(Err(ArrayError::array())),
67+
res => res.unwrap(),
68+
}
69+
70+
len
71+
};
72+
73+
(0..len)
74+
.map(|i| {
75+
let item = unsafe {
76+
let mut item = MaybeUninit::uninit();
77+
78+
match sys::get_element(env, v, i, item.as_mut_ptr()) {
79+
Err(sys::Status::PendingException) => return Err(Throw::new()),
80+
res => res.unwrap(),
81+
}
82+
83+
Handle::new_internal(JsValue::from_local(cx.env(), item.assume_init()))
84+
};
85+
86+
match T::Item::try_from_js(cx, item) {
87+
Ok(Ok(item)) => Ok(Ok(item)),
88+
Ok(Err(err)) => Ok(Err(ArrayError::item(err))),
89+
Err(err) => Err(err),
90+
}
91+
})
92+
.collect::<Result<Result<T, Self::Error>, Throw>>()
93+
.map(|v| v.map(Array))
94+
}
95+
}
96+
97+
impl<'cx, T> TryIntoJs<'cx> for Array<T>
98+
where
99+
T: IntoIterator,
100+
T::Item: TryIntoJs<'cx>,
101+
{
102+
type Value = JsArray;
103+
104+
fn try_into_js(self, cx: &mut Cx<'cx>) -> JsResult<'cx, Self::Value> {
105+
let iter = self.0.into_iter();
106+
let env = cx.env().to_raw();
107+
let (len, _) = iter.size_hint();
108+
let arr = unsafe {
109+
let mut arr = MaybeUninit::uninit();
110+
111+
match sys::create_array_with_length(env, len, arr.as_mut_ptr()) {
112+
Err(sys::Status::PendingException) => return Err(Throw::new()),
113+
res => res.unwrap(),
114+
}
115+
116+
arr.assume_init()
117+
};
118+
119+
for (i, item) in iter.enumerate() {
120+
let item = item.try_into_js(cx)?.to_local();
121+
let Ok(i) = u32::try_from(i) else {
122+
return cx.throw_error("Exceeded maximum length of an array");
123+
};
124+
125+
unsafe {
126+
match sys::set_element(env, arr, i, item) {
127+
Err(sys::Status::PendingException) => return Err(Throw::new()),
128+
res => res.unwrap(),
129+
}
130+
}
131+
}
132+
133+
unsafe { Ok(Handle::new_internal(JsArray::from_local(cx.env(), arr))) }
134+
}
135+
}
136+
137+
/// Error when extracting an [`Array<T>`]
138+
#[derive(Debug)]
139+
pub enum ArrayError<E> {
140+
/// Value was not a JavaScript array.
141+
Array(TypeExpected<JsArray>),
142+
/// An element failed to convert to `T::Item`.
143+
Item(E),
144+
}
145+
146+
impl<E> ArrayError<E> {
147+
fn array() -> Self {
148+
Self::Array(TypeExpected::<JsArray>::new())
149+
}
150+
151+
fn item(err: E) -> Self {
152+
Self::Item(err)
153+
}
154+
}
155+
156+
impl<E> fmt::Display for ArrayError<E>
157+
where
158+
E: fmt::Display,
159+
{
160+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
161+
match self {
162+
ArrayError::Array(err) => write!(f, "{}", err),
163+
ArrayError::Item(err) => write!(f, "{}", err),
164+
}
165+
}
166+
}
167+
168+
impl<E> error::Error for ArrayError<E> where E: error::Error {}
169+
170+
impl<'cx, E> TryIntoJs<'cx> for ArrayError<E>
171+
where
172+
E: TryIntoJs<'cx>,
173+
{
174+
type Value = JsValue;
175+
176+
fn try_into_js(self, cx: &mut Cx<'cx>) -> JsResult<'cx, Self::Value> {
177+
match self {
178+
ArrayError::Array(err) => err.try_into_js(cx).map(|v| v.upcast()),
179+
ArrayError::Item(err) => err.try_into_js(cx).map(|v| v.upcast()),
180+
}
181+
}
182+
}
183+
184+
impl<'cx, E> private::Sealed for ArrayError<E> where E: TryIntoJs<'cx> {}

crates/neon/src/types_impl/extract/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ use crate::{
107107
};
108108

109109
pub use self::{
110+
array::{Array, ArrayError},
110111
boxed::Boxed,
111112
buffer::{
112113
ArrayBuffer, BigInt64Array, BigUint64Array, Buffer, Float32Array, Float64Array, Int16Array,
@@ -124,6 +125,7 @@ pub use self::json::Json;
124125
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
125126
pub mod json;
126127

128+
mod array;
127129
mod boxed;
128130
mod buffer;
129131
mod container;

test/napi/lib/extract.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,13 @@ describe("Extractors", () => {
100100
assert.strictEqual(await addon.sleepWith(1.5), 1.5);
101101
assert.strictEqual(await addon.sleepWithSync(1.5), 1.5);
102102
});
103+
104+
it("Array", () => {
105+
assert.deepStrictEqual(addon.extractArrayVec([1, 2, 3]), [1, 2, 3]);
106+
assert.deepStrictEqual(addon.extractArrayDouble([1, 2, 3]), [2, 4, 6]);
107+
assert.deepStrictEqual(
108+
addon.extractArrayDedupe(["a", "a", "b", "c", "c"]).sort(),
109+
["a", "b", "c"]
110+
);
111+
});
103112
});

test/napi/src/js/extract.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use either::Either;
22
use neon::{prelude::*, types::extract::*};
3+
use std::collections::HashSet;
34

45
pub fn extract_values(mut cx: FunctionContext) -> JsResult<JsArray> {
56
#[allow(clippy::type_complexity)]
@@ -196,3 +197,18 @@ fn sleep_with(n: f64) -> impl for<'cx> TryIntoJs<'cx> {
196197
fn sleep_with_sync(n: f64) -> impl for<'cx> TryIntoJs<'cx> {
197198
sleep_with(n)
198199
}
200+
201+
#[neon::export]
202+
fn extract_array_vec(Array(arr): Array<Vec<f64>>) -> Array<Vec<f64>> {
203+
Array(arr)
204+
}
205+
206+
#[neon::export]
207+
fn extract_array_double(Array(arr): Array<Vec<f64>>) -> Array<impl Iterator<Item = f64>> {
208+
Array(arr.into_iter().map(|x| x * 2.0))
209+
}
210+
211+
#[neon::export]
212+
fn extract_array_dedupe(set: Array<HashSet<String>>) -> Array<HashSet<String>> {
213+
set
214+
}

0 commit comments

Comments
 (0)