diff --git a/core/src/lib.rs b/core/src/lib.rs index 88b558cd..9f0cc653 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -24,9 +24,10 @@ pub use class::Class; pub use persistent::{Outlive, Persistent}; pub use result::{CatchResultExt, CaughtError, CaughtResult, Error, Result, ThrowResultExt}; pub use value::{ - array, atom, convert, function, module, object, promise, Array, Atom, BigInt, Coerced, - Exception, Filter, FromAtom, FromIteratorJs, FromJs, Function, IntoAtom, IntoJs, IteratorJs, - Module, Null, Object, Promise, String, Symbol, Type, Undefined, Value, + array, atom, convert, function, iterator, module, object, promise, Array, Atom, BigInt, + Coerced, Exception, Filter, FromAtom, FromIteratorJs, FromJs, Function, IntoAtom, IntoJs, + Iterable, Iterator, IteratorJs, Module, Null, Object, Promise, String, Symbol, Type, Undefined, + Value, }; #[cfg(feature = "allocator")] diff --git a/core/src/value.rs b/core/src/value.rs index 88915d06..b2ce4895 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -7,6 +7,7 @@ mod bigint; pub mod convert; pub(crate) mod exception; pub mod function; +pub mod iterator; pub mod module; pub mod object; pub mod promise; @@ -19,6 +20,7 @@ pub use bigint::BigInt; pub use convert::{Coerced, FromAtom, FromIteratorJs, FromJs, IntoAtom, IntoJs, IteratorJs}; pub use exception::Exception; pub use function::{Constructor, Function}; +pub use iterator::{Iterable, Iterator}; pub use module::Module; pub use object::{Filter, Object}; pub use promise::Promise; diff --git a/core/src/value/iterator.rs b/core/src/value/iterator.rs new file mode 100644 index 00000000..642c4391 --- /dev/null +++ b/core/src/value/iterator.rs @@ -0,0 +1,356 @@ +use std::{iter::Iterator as StdIterator, marker::PhantomData, ops::Deref}; + +use crate::{ + atom::PredefinedAtom, + function::{Func, IntoArgs, MutFn, This}, + Ctx, Error, FromJs, Function, IntoJs, Object, Result, Value, +}; + +mod into_iter; +mod iterable; + +pub use into_iter::{IterFn, IterFnMut}; +pub use iterable::Iterable; + +/// A trait for converting a Rust object into a JavaScript iterator. +pub trait IntoJsIter<'js> { + type Item: IntoJs<'js>; + + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result>; +} + +/// A javascript iterator. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Iterator<'js>(pub(crate) Object<'js>); + +impl<'js> Iterator<'js> { + /// Create a new iterable iterator from a Rust object which implements [`IntoJsIter`]. + /// + pub fn new(ctx: Ctx<'js>, mut it: T) -> Result + where + T: IntoJsIter<'js, Item = I> + 'js, + I: IntoJs<'js>, + { + let iterator = Object::new(ctx.clone())?; + iterator.set("position", 0usize)?; + iterator.set( + PredefinedAtom::SymbolIterator, + Func::from(|it: This>| -> Result> { Ok(it.0) }), + )?; + iterator.set( + "next", + Function::new( + ctx, + MutFn::from( + move |ctx: Ctx<'js>, this: This>| -> Result> { + let position = this.get::<_, usize>("position")?; + let res = Object::new(ctx.clone())?; + if let Some(value) = it.next(ctx, position)? { + res.set("value", value)?; + this.set("position", position + 1)?; + } else { + res.set(PredefinedAtom::Done, true)?; + } + Ok(res) + }, + ), + ), + )?; + + Ok(Self(iterator)) + } + + /// Get the next value from the iterator. + pub fn next(&self) -> Result>> { + let next_fn = self.0.get::<_, Function>(PredefinedAtom::Next)?; + let next = (This(self.0.clone()), 2).apply::>(&next_fn)?; + if let Ok(done) = next.get::<_, bool>(PredefinedAtom::Done) { + if done { + return Ok(None); + } + } + let value = next.get::<_, Value<'_>>("value")?; + Ok(Some(value)) + } + + /// Reference to value + #[inline] + pub fn as_value(&self) -> &Value<'js> { + self.0.as_value() + } + + /// Convert into value + #[inline] + pub fn into_value(self) -> Value<'js> { + self.0.into_value() + } + + /// Convert from value + pub fn from_value(value: Value<'js>) -> Option { + Self::from_object(Object::from_value(value).ok()?) + } + + /// Reference as an object + #[inline] + pub fn as_object(&self) -> &Object<'js> { + &self.0 + } + + /// Convert into an object + #[inline] + pub fn into_object(self) -> Object<'js> { + self.0 + } + + /// Convert from an object + pub fn from_object(object: Object<'js>) -> Option { + if object.is_iterator() { + Some(Self(object)) + } else { + None + } + } +} + +impl<'js> Deref for Iterator<'js> { + type Target = Object<'js>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'js> AsRef> for Iterator<'js> { + fn as_ref(&self) -> &Object<'js> { + &self.0 + } +} + +impl<'js> AsRef> for Iterator<'js> { + fn as_ref(&self) -> &Value<'js> { + self.0.as_ref() + } +} + +impl<'js> From> for Value<'js> { + fn from(value: Iterator<'js>) -> Self { + value.into_value() + } +} + +impl<'js> FromJs<'js> for Iterator<'js> { + fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + if let Some(v) = Self::from_value(value) { + Ok(v) + } else { + Err(Error::new_from_js(ty_name, "Iterator")) + } + } +} + +impl<'js> IntoJs<'js> for Iterator<'js> { + fn into_js(self, _ctx: &Ctx<'js>) -> Result> { + Ok(self.into_value()) + } +} + +impl<'js> Object<'js> { + /// Returns whether the object is an iterator. + pub fn is_iterator(&self) -> bool { + self.get::<_, Function>("next").is_ok() + } + + /// Interpret as [`Iterator`] + /// + /// # Safety + /// You should be sure that the object actually is the required type. + pub unsafe fn ref_iterator(&self) -> &Iterator<'js> { + &*(self as *const _ as *const Iterator) + } + + /// Try reinterpret as [`Iterator`] + pub fn as_iterator(&self) -> Option<&Iterator<'js>> { + self.is_iterator().then_some(unsafe { self.ref_iterator() }) + } +} + +/// A rust iterator over the values of a js iterator. +pub struct IteratorIter<'js, T> { + iterator: Iterator<'js>, + marker: PhantomData, +} + +impl<'js, T> StdIterator for IteratorIter<'js, T> +where + T: FromJs<'js>, +{ + type Item = Result; + + fn next(&mut self) -> Option { + let next = self.iterator.next().transpose()?.ok()?; + Some(T::from_js(self.iterator.ctx(), next)) + } +} + +impl<'js> IntoIterator for Iterator<'js> { + type Item = Result>; + type IntoIter = IteratorIter<'js, Value<'js>>; + + fn into_iter(self) -> Self::IntoIter { + IteratorIter { + iterator: self, + marker: PhantomData, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::*; + + #[test] + fn js_iterator_from_rust() { + test_with(|ctx| { + let iterator: Iterator = ctx + .eval( + r#" + const array = ['a', 'b', 'c']; + const iterator = array[Symbol.iterator](); + iterator + "#, + ) + .unwrap(); + assert_eq!( + iterator + .next() + .unwrap() + .unwrap() + .as_string() + .unwrap() + .to_string() + .unwrap(), + "a" + ); + assert_eq!( + iterator + .next() + .unwrap() + .unwrap() + .as_string() + .unwrap() + .to_string() + .unwrap(), + "b" + ); + assert_eq!( + iterator + .next() + .unwrap() + .unwrap() + .as_string() + .unwrap() + .to_string() + .unwrap(), + "c" + ); + assert!(iterator.next().unwrap().is_none()); + }); + } + + #[test] + fn js_iterator_from_rust_iter() { + test_with(|ctx| { + let values = ctx + .eval::( + r#" + const array = ['a', 'b', 'c']; + const iterator = array[Symbol.iterator](); + iterator + "#, + ) + .unwrap() + .into_iter() + .collect::>>() + .unwrap(); + assert_eq!(values.len(), 3); + assert_eq!(values[0].as_string().unwrap().to_string().unwrap(), "a"); + assert_eq!(values[1].as_string().unwrap().to_string().unwrap(), "b"); + assert_eq!(values[2].as_string().unwrap().to_string().unwrap(), "c"); + }); + } + + #[test] + fn rust_iterator_from_js() { + test_with(|ctx| { + let iterator = Iterator::new( + ctx.clone(), + IterFn::from(|_, position| { + if position < 3 { + Ok(Some(position)) + } else { + Ok(None) + } + }), + ) + .unwrap(); + ctx.globals().set("myiterator", iterator).unwrap(); + let res: String = ctx + .eval( + r#" + const res = []; + for (let i of myiterator) { + res.push(i); + } + res.join(',') + "#, + ) + .unwrap(); + assert_eq!(res.to_string().unwrap(), "0,1,2"); + }); + } + + #[test] + fn rust_iterator_from_rust() { + test_with(|ctx| { + let iterator = Iterator::new( + ctx.clone(), + IterFn::from(|_, position| { + if position < 3 { + Ok(Some(position)) + } else { + Ok(None) + } + }), + ) + .unwrap(); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 0); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 1); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 2); + assert!(iterator.next().unwrap().is_none()); + }); + } + + #[test] + fn rust_iterator_trait() { + test_with(|ctx| { + let data = vec![1, 2, 3]; + let iterator = Iterator::new(ctx.clone(), data.into_iter()).unwrap(); + ctx.globals().set("myiterator", iterator).unwrap(); + let res: String = ctx + .eval( + r#" + const res = []; + for (let i of myiterator) { + res.push(i); + } + res.join(',') + "#, + ) + .unwrap(); + assert_eq!(res.to_string().unwrap(), "1,2,3"); + }); + } +} diff --git a/core/src/value/iterator/into_iter.rs b/core/src/value/iterator/into_iter.rs new file mode 100644 index 00000000..f0e08305 --- /dev/null +++ b/core/src/value/iterator/into_iter.rs @@ -0,0 +1,70 @@ +use std::iter::Iterator as StdIterator; + +use super::IntoJsIter; +use crate::{Ctx, IntoJs, Result}; + +impl<'js, T, I> IntoJsIter<'js> for T +where + T: StdIterator, + I: IntoJs<'js>, +{ + type Item = I; + + fn next(&mut self, _ctx: Ctx<'_>, _position: usize) -> Result> { + Ok(self.next()) + } +} + +/// Helper type for creating an iterator from a closure which implements [`Fn`] +pub struct IterFn(F); + +impl IterFn { + pub fn new(f: F) -> Self { + IterFn(f) + } +} + +impl From for IterFn { + fn from(value: F) -> Self { + IterFn::new(value) + } +} + +impl<'js, F, I> IntoJsIter<'js> for IterFn +where + F: Fn(Ctx<'js>, usize) -> Result>, + I: IntoJs<'js>, +{ + type Item = I; + + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result> { + self.0(ctx, position) + } +} + +/// Helper type for creating an iterator from a closure which implements [`FnMut`] +pub struct IterFnMut(F); + +impl IterFnMut { + pub fn new(f: F) -> Self { + IterFnMut(f) + } +} + +impl From for IterFnMut { + fn from(value: F) -> Self { + IterFnMut::new(value) + } +} + +impl<'js, F, I> IntoJsIter<'js> for IterFnMut +where + F: FnMut(Ctx<'js>, usize) -> Result>, + I: IntoJs<'js>, +{ + type Item = I; + + fn next(&mut self, ctx: Ctx<'js>, position: usize) -> Result> { + self.0(ctx, position) + } +} diff --git a/core/src/value/iterator/iterable.rs b/core/src/value/iterator/iterable.rs new file mode 100644 index 00000000..b210df75 --- /dev/null +++ b/core/src/value/iterator/iterable.rs @@ -0,0 +1,199 @@ +use std::ops::Deref; + +use crate::{ + atom::PredefinedAtom, + function::{IntoArgs, This}, + Ctx, Error, FromJs, Function, IntoJs, Iterator, Object, Result, Value, +}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Iterable<'js>(pub(crate) Object<'js>); + +impl<'js> Iterable<'js> { + /// Get the iterator from the iterable. + /// + /// This is equivalent to calling `iterable[Symbol.iterator]()` in JavaScript. + pub fn iterator(&self) -> Result> { + let iter_fn = self.0.get::<_, Function>(PredefinedAtom::SymbolIterator)?; + let iterable = (This(self.0.clone()), 2).apply::>(&iter_fn)?; + Ok(Iterator(iterable)) + } + + /// Reference to value + #[inline] + pub fn as_value(&self) -> &Value<'js> { + self.0.as_value() + } + + /// Convert into value + #[inline] + pub fn into_value(self) -> Value<'js> { + self.0.into_value() + } + + /// Convert from value + pub fn from_value(value: Value<'js>) -> Option { + Self::from_object(Object::from_value(value).ok()?) + } + + /// Reference as an object + #[inline] + pub fn as_object(&self) -> &Object<'js> { + &self.0 + } + + /// Convert into an object + #[inline] + pub fn into_object(self) -> Object<'js> { + self.0 + } + + /// Convert from an object + pub fn from_object(object: Object<'js>) -> Option { + if object.is_iterable() { + Some(Self(object)) + } else { + None + } + } +} + +impl<'js> Deref for Iterable<'js> { + type Target = Object<'js>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'js> AsRef> for Iterable<'js> { + fn as_ref(&self) -> &Object<'js> { + &self.0 + } +} + +impl<'js> AsRef> for Iterable<'js> { + fn as_ref(&self) -> &Value<'js> { + self.0.as_ref() + } +} + +impl<'js> From> for Value<'js> { + fn from(value: Iterable<'js>) -> Self { + value.into_value() + } +} + +impl<'js> FromJs<'js> for Iterable<'js> { + fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + if let Some(v) = Self::from_value(value) { + Ok(v) + } else { + Err(Error::new_from_js(ty_name, "Iterable")) + } + } +} + +impl<'js> IntoJs<'js> for Iterable<'js> { + fn into_js(self, _ctx: &Ctx<'js>) -> Result> { + Ok(self.into_value()) + } +} + +impl<'js> Object<'js> { + /// Returns whether the object is iterable. + pub fn is_iterable(&self) -> bool { + self.contains_key(PredefinedAtom::SymbolIterator) + .unwrap_or(false) + } + + /// Interpret as [`Iterable`] + /// + /// # Safety + /// You should be sure that the object actually is the required type. + pub unsafe fn ref_iterable(&self) -> &Iterable<'js> { + &*(self as *const _ as *const Iterable) + } + + /// Try reinterpret as [`Iterable`] + pub fn as_iterable(&self) -> Option<&Iterable<'js>> { + self.is_iterable().then_some(unsafe { self.ref_iterable() }) + } +} + +#[cfg(test)] +mod test { + use crate::{ + atom::PredefinedAtom, + function::{Func, This}, + *, + }; + + #[test] + fn from_javascript() { + test_with(|ctx| { + let iterable: Iterable = ctx + .eval( + r#" + const myIterable = {}; + myIterable[Symbol.iterator] = function* () { + yield 1; + yield 2; + yield 3; + }; + myIterable + "#, + ) + .unwrap(); + let iterator = iterable.iterator().unwrap(); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 1); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 2); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 3); + assert!(iterator.next().unwrap().is_none()); + }); + } + + #[test] + fn from_rust() { + fn closure<'js>(ctx: Ctx<'js>) { + let myiterable = Object::new(ctx.clone()).unwrap(); + myiterable.set("position", 0usize).unwrap(); + myiterable + .set( + PredefinedAtom::SymbolIterator, + Func::from(|it: This>| -> Result> { Ok(it.0) }), + ) + .unwrap(); + myiterable + .set( + PredefinedAtom::Next, + Func::from( + move |ctx: Ctx<'js>, this: This>| -> Result> { + let position = this.get::<_, usize>("position")?; + let res = Object::new(ctx.clone())?; + if position >= 3 { + res.set(PredefinedAtom::Done, true)?; + } else { + res.set("value", position)?; + this.set("position", position + 1)?; + } + Ok(res) + }, + ), + ) + .unwrap(); + let iterator = Iterable::from_object(myiterable) + .unwrap() + .iterator() + .unwrap(); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 0); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 1); + assert_eq!(iterator.next().unwrap().unwrap().as_int().unwrap(), 2); + assert!(iterator.next().unwrap().is_none()); + } + + test_with(closure); + } +}