rquickjs_core/value/
iterable.rs

1//! JavaScript iterable types from Rust iterators.
2
3use crate::safe_ref::Mut;
4use crate::{
5    atom::PredefinedAtom,
6    function::{MutFn, This},
7    Ctx, Error, FromJs, Function, IntoJs, Object, Result, Value,
8};
9use core::{iter::FusedIterator, marker::PhantomData};
10
11/// Converts a Rust iterator into a JavaScript iterable object.
12///
13/// The resulting object implements the JavaScript iterable protocol with a
14/// `[Symbol.iterator]` method that returns an iterator following the iterator protocol.
15///
16/// Note: The iterator can only be consumed once. Subsequent iterations will yield no values.
17///
18/// # Example
19/// ```
20/// # use rquickjs::{Runtime, Context, Result, Iterable};
21/// # let rt = Runtime::new().unwrap();
22/// # let ctx = Context::full(&rt).unwrap();
23/// # ctx.with(|ctx| -> Result<()> {
24/// // Create an iterable from a Vec
25/// let iter = Iterable::from(vec![1, 2, 3]);
26/// ctx.globals().set("myIterable", iter)?;
27///
28/// // Use spread operator
29/// let result: Vec<i32> = ctx.eval("[...myIterable]")?;
30/// assert_eq!(result, vec![1, 2, 3]);
31/// # Ok(())
32/// # }).unwrap();
33/// ```
34pub struct Iterable<I>(pub I);
35
36impl<I> From<I> for Iterable<I> {
37    fn from(iter: I) -> Self {
38        Iterable(iter)
39    }
40}
41
42impl<'js, I, T> IntoJs<'js> for Iterable<I>
43where
44    I: IntoIterator<Item = T> + 'js,
45    I::IntoIter: 'js,
46    T: IntoJs<'js> + 'js,
47{
48    fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
49        let iter = Mut::new(Some(self.0.into_iter()));
50
51        let iterator_fn = Function::new(
52            ctx.clone(),
53            MutFn::new(move |ctx: Ctx<'js>| -> Result<Object<'js>> {
54                let iter_obj = Object::new(ctx.clone())?;
55                let iter_taken = iter.lock().take();
56
57                let state = Mut::new(iter_taken);
58                let next_fn = Function::new(
59                    ctx.clone(),
60                    MutFn::new(move |ctx: Ctx<'js>| -> Result<Object<'js>> {
61                        let result = Object::new(ctx.clone())?;
62                        let mut state_ref = state.lock();
63
64                        if let Some(ref mut it) = *state_ref {
65                            if let Some(value) = it.next() {
66                                result.set(PredefinedAtom::Value, value.into_js(&ctx)?)?;
67                                result.set(PredefinedAtom::Done, false)?;
68                            } else {
69                                result.set(PredefinedAtom::Done, true)?;
70                                *state_ref = None;
71                            }
72                        } else {
73                            result.set(PredefinedAtom::Done, true)?;
74                        }
75                        Ok(result)
76                    }),
77                )?;
78
79                iter_obj.set(PredefinedAtom::Next, next_fn)?;
80                Ok(iter_obj)
81            }),
82        )?;
83
84        let obj = Object::new(ctx.clone())?;
85        obj.set(PredefinedAtom::SymbolIterator, iterator_fn)?;
86        Ok(obj.into_value())
87    }
88}
89
90/// An iterator over values from a JavaScript iterable.
91///
92/// This struct wraps a JavaScript iterator object and implements Rust's `Iterator` trait,
93/// allowing you to consume JavaScript iterables from Rust code.
94///
95/// The type parameter `T` specifies what type each value should be converted to.
96/// Use `Value<'js>` to get raw JS values without conversion.
97///
98/// # Example
99/// ```
100/// # use rquickjs::{Runtime, Context, Result, JsIterator, Value};
101/// # let rt = Runtime::new().unwrap();
102/// # let ctx = Context::full(&rt).unwrap();
103/// # ctx.with(|ctx| -> Result<()> {
104/// // Get an iterator with automatic conversion to i32
105/// let iter: JsIterator<i32> = ctx.eval("[1, 2, 3]")?;
106/// let values: Vec<i32> = iter.filter_map(|r| r.ok()).collect();
107/// assert_eq!(values, vec![1, 2, 3]);
108///
109/// // Get raw JS values without conversion
110/// let iter: JsIterator<Value> = ctx.eval("['a', 'b']")?;
111/// for value in iter {
112///     println!("{:?}", value?);
113/// }
114/// # Ok(())
115/// # }).unwrap();
116/// ```
117pub struct JsIterator<'js, T = Value<'js>> {
118    iterator: Object<'js>,
119    done: bool,
120    _marker: PhantomData<T>,
121}
122
123impl<'js, T> JsIterator<'js, T> {
124    /// Returns the underlying JS iterator object.
125    pub fn into_inner(self) -> Object<'js> {
126        self.iterator
127    }
128
129    /// Maps this iterator to yield a different type.
130    ///
131    /// This is useful when you have a `JsIterator<Value>` and want to convert
132    /// values to a specific type.
133    pub fn typed<U: FromJs<'js>>(self) -> JsIterator<'js, U> {
134        JsIterator {
135            iterator: self.iterator,
136            done: self.done,
137            _marker: PhantomData,
138        }
139    }
140}
141
142impl<'js, T: FromJs<'js>> Iterator for JsIterator<'js, T> {
143    type Item = Result<T>;
144
145    fn next(&mut self) -> Option<Self::Item> {
146        if self.done {
147            return None;
148        }
149
150        let next_fn: Function<'js> = match self.iterator.get(PredefinedAtom::Next) {
151            Ok(f) => f,
152            Err(e) => return Some(Err(e)),
153        };
154
155        let result: Object<'js> = match next_fn.call((This(self.iterator.clone()),)) {
156            Ok(r) => r,
157            Err(e) => return Some(Err(e)),
158        };
159
160        let done: bool = match result.get(PredefinedAtom::Done) {
161            Ok(d) => d,
162            Err(e) => return Some(Err(e)),
163        };
164
165        if done {
166            self.done = true;
167            return None;
168        }
169
170        let value: Value<'js> = match result.get(PredefinedAtom::Value) {
171            Ok(v) => v,
172            Err(e) => return Some(Err(e)),
173        };
174
175        Some(T::from_js(self.iterator.ctx(), value))
176    }
177}
178
179impl<'js, T: FromJs<'js>> FusedIterator for JsIterator<'js, T> {}
180
181impl<'js, T: FromJs<'js>> FromJs<'js> for JsIterator<'js, T> {
182    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
183        let obj = Object::from_value(value)?;
184
185        // Try Symbol.iterator first (for iterables like arrays)
186        if let Ok(iter_fn) = obj.get::<_, Function<'js>>(PredefinedAtom::SymbolIterator) {
187            let iterator: Object<'js> = iter_fn.call((This(obj),))?;
188            return Ok(JsIterator {
189                iterator,
190                done: false,
191                _marker: PhantomData,
192            });
193        }
194
195        // Fall back to treating it as an iterator (has `next` method)
196        if obj.contains_key(PredefinedAtom::Next)? {
197            return Ok(JsIterator {
198                iterator: obj,
199                done: false,
200                _marker: PhantomData,
201            });
202        }
203
204        Err(Error::new_from_js(
205            "value",
206            "iterable (object with Symbol.iterator or next)",
207        ))
208    }
209}
210
211#[cfg(test)]
212mod test {
213    use super::*;
214    use crate::*;
215
216    #[test]
217    fn iterable_spread() {
218        test_with(|ctx| {
219            let iter = Iterable::from(vec![1i32, 2, 3]);
220            ctx.globals().set("myIter", iter).unwrap();
221            let result: Vec<i32> = ctx.eval("[...myIter]").unwrap();
222            assert_eq!(result, vec![1, 2, 3]);
223        });
224    }
225
226    #[test]
227    fn iterable_for_of() {
228        test_with(|ctx| {
229            let iter = Iterable::from(vec!["a", "b", "c"]);
230            ctx.globals().set("myIter", iter).unwrap();
231            let result: alloc::string::String = ctx
232                .eval(
233                    r#"
234                let s = "";
235                for (const x of myIter) { s += x; }
236                s
237            "#,
238                )
239                .unwrap();
240            assert_eq!(result, "abc");
241        });
242    }
243
244    #[test]
245    fn iterable_from_range() {
246        test_with(|ctx| {
247            let iter = Iterable::from(0..5);
248            ctx.globals().set("myIter", iter).unwrap();
249            let result: Vec<i32> = ctx.eval("[...myIter]").unwrap();
250            assert_eq!(result, vec![0, 1, 2, 3, 4]);
251        });
252    }
253
254    #[test]
255    fn iterable_single_use() {
256        test_with(|ctx| {
257            let iter = Iterable::from(vec![1i32, 2]);
258            ctx.globals().set("myIter", iter).unwrap();
259            // First iteration consumes the iterator
260            let first: Vec<i32> = ctx.eval("[...myIter]").unwrap();
261            assert_eq!(first, vec![1, 2]);
262            // Second iteration returns empty (iterator exhausted)
263            let second: Vec<i32> = ctx.eval("[...myIter]").unwrap();
264            assert_eq!(second, Vec::<i32>::new());
265        });
266    }
267
268    #[test]
269    fn js_iter_from_array() {
270        test_with(|ctx| {
271            let iter: JsIterator<i32> = ctx.eval("[1, 2, 3][Symbol.iterator]()").unwrap();
272            let values: Vec<i32> = iter.filter_map(|r| r.ok()).collect();
273            assert_eq!(values, vec![1, 2, 3]);
274        });
275    }
276
277    #[test]
278    fn js_iter_from_iterable() {
279        test_with(|ctx| {
280            // Pass an iterable (array), not an iterator
281            let iter: JsIterator<i32> = ctx.eval("[4, 5, 6]").unwrap();
282            let values: Vec<i32> = iter.filter_map(|r| r.ok()).collect();
283            assert_eq!(values, vec![4, 5, 6]);
284        });
285    }
286
287    #[test]
288    fn js_iter_from_generator() {
289        test_with(|ctx| {
290            let iter: JsIterator<i32> = ctx
291                .eval(
292                    r#"
293                (function*() {
294                    yield 10;
295                    yield 20;
296                    yield 30;
297                })()
298            "#,
299                )
300                .unwrap();
301            let values: Vec<i32> = iter.filter_map(|r| r.ok()).collect();
302            assert_eq!(values, vec![10, 20, 30]);
303        });
304    }
305
306    #[test]
307    fn js_iter_roundtrip() {
308        test_with(|ctx| {
309            // Rust -> JS -> Rust roundtrip
310            let rust_iter = Iterable::from(vec![100i32, 200, 300]);
311            ctx.globals().set("myIter", rust_iter).unwrap();
312            let js_iter: JsIterator<i32> = ctx.eval("myIter").unwrap();
313            let values: Vec<i32> = js_iter.filter_map(|r| r.ok()).collect();
314            assert_eq!(values, vec![100, 200, 300]);
315        });
316    }
317
318    #[test]
319    fn js_iter_raw_values() {
320        test_with(|ctx| {
321            // Get raw Value without conversion
322            let iter: JsIterator<Value> = ctx.eval("[1, 'two', 3]").unwrap();
323            let values: Vec<Value> = iter.filter_map(|r| r.ok()).collect();
324            assert_eq!(values.len(), 3);
325            assert!(values[0].is_int());
326            assert!(values[1].is_string());
327            assert!(values[2].is_int());
328        });
329    }
330
331    #[test]
332    fn js_iter_typed_conversion() {
333        test_with(|ctx| {
334            // Start with raw values, then convert
335            let iter: JsIterator<Value> = ctx.eval("[1, 2, 3]").unwrap();
336            let typed = iter.typed::<i32>();
337            let values: Vec<i32> = typed.filter_map(|r| r.ok()).collect();
338            assert_eq!(values, vec![1, 2, 3]);
339        });
340    }
341
342    #[test]
343    fn js_iter_strings() {
344        test_with(|ctx| {
345            let iter: JsIterator<alloc::string::String> =
346                ctx.eval("['hello', 'world', 'rust']").unwrap();
347            let values: Vec<alloc::string::String> = iter.filter_map(|r| r.ok()).collect();
348            assert_eq!(values, vec!["hello", "world", "rust"]);
349        });
350    }
351
352    #[test]
353    fn js_iter_floats() {
354        test_with(|ctx| {
355            let iter: JsIterator<f64> = ctx.eval("[1.5, 2.7, 3.54]").unwrap();
356            let values: Vec<f64> = iter.filter_map(|r| r.ok()).collect();
357            assert_eq!(values, vec![1.5, 2.7, 3.54]);
358        });
359    }
360
361    #[test]
362    fn js_iter_bools() {
363        test_with(|ctx| {
364            let iter: JsIterator<bool> = ctx.eval("[true, false, true]").unwrap();
365            let values: Vec<bool> = iter.filter_map(|r| r.ok()).collect();
366            assert_eq!(values, vec![true, false, true]);
367        });
368    }
369
370    #[test]
371    fn js_iter_objects() {
372        test_with(|ctx| {
373            let iter: JsIterator<Object> = ctx.eval("[{a: 1}, {b: 2}]").unwrap();
374            let objects: Vec<Object> = iter.filter_map(|r| r.ok()).collect();
375            assert_eq!(objects.len(), 2);
376            assert_eq!(objects[0].get::<_, i32>("a").unwrap(), 1);
377            assert_eq!(objects[1].get::<_, i32>("b").unwrap(), 2);
378        });
379    }
380
381    #[test]
382    fn js_iter_map_entries() {
383        test_with(|ctx| {
384            // Map.entries() returns [key, value] pairs
385            let iter: JsIterator<Array> =
386                ctx.eval("new Map([['a', 1], ['b', 2]]).entries()").unwrap();
387            let entries: Vec<Array> = iter.filter_map(|r| r.ok()).collect();
388            assert_eq!(entries.len(), 2);
389            assert_eq!(entries[0].get::<alloc::string::String>(0).unwrap(), "a");
390            assert_eq!(entries[0].get::<i32>(1).unwrap(), 1);
391        });
392    }
393
394    #[test]
395    fn js_iter_set() {
396        test_with(|ctx| {
397            let iter: JsIterator<i32> = ctx.eval("new Set([1, 2, 3])").unwrap();
398            let values: Vec<i32> = iter.filter_map(|r| r.ok()).collect();
399            assert_eq!(values, vec![1, 2, 3]);
400        });
401    }
402}