diff --git a/screeps-game-api/CHANGELOG.md b/screeps-game-api/CHANGELOG.md index d3243e90..00f6c436 100644 --- a/screeps-game-api/CHANGELOG.md +++ b/screeps-game-api/CHANGELOG.md @@ -6,6 +6,8 @@ Unreleased - Take `&T` in `Room::create_construction_site` and `look_for_at` rather than `T` for `T: HasPosition` (breaking) (#105) - Add `Neg` implementation for `Direction` allowing unary minus to reverse direction (#113) +- Add `JsVec` structure for transparently wrapping typed JavaScript arrays without immediately + unwrapping them. (#114) 0.3.0 (2018-11-12) ================== diff --git a/screeps-game-api/src/js_collections/js_vec.rs b/screeps-game-api/src/js_collections/js_vec.rs new file mode 100644 index 00000000..1f534fa4 --- /dev/null +++ b/screeps-game-api/src/js_collections/js_vec.rs @@ -0,0 +1,404 @@ +//! [`JsVec`] +use std::marker::PhantomData; + +use stdweb::{Array, InstanceOf, JsSerialize, Reference, ReferenceType, Value}; + +use { + traits::{FromExpectedType, IntoExpectedType, TryFrom, TryInto}, + ConversionError, +}; + +// - InstanceOf +// - AsRef +// - ReferenceType +// - Into +// - TryInto +// - TryFrom +// - TryFrom<&Reference> +// - TryFrom +// - TryFrom<&Value> + +/// Reference to a JavaScript array which is expected to contain a specific type of item. +/// +/// All `TryFrom` / `TryInto` conversions use type checking, but `FromExpectedType` / +/// `IntoExpectedType` possibly don't. +/// +/// The implementation for `Into>` uses `IntoExpectedType` internally. +/// +/// Trait notes: implements `TryInto>`, but `Vec` does not implement `TryFrom>` +/// due to coherence issues. +pub struct JsVec { + inner: Array, + phantom: PhantomData>, +} + +impl JsVec { + pub fn len(&self) -> usize { + self.inner.len() + } +} + +impl JsVec +where + T: FromExpectedType, +{ + /// Gets an item, panicking if the types don't match and `check-all-casts` is enabled. + pub fn get(&self, idx: usize) -> Option { + // this assumes u32::max_value() == usize::max_value() + // (otherwise cast below could overflow). + if idx >= self.len() { + None + } else { + Some(js_unwrap_ref!(@{self.inner.as_ref()}[@{idx as u32}])) + } + } + + /// Gets an item, returning an error if the types don't match and `check-all-casts` is enabled. + pub fn try_get(&self, idx: usize) -> Result, ConversionError> { + // this assumes u32::max_value() == usize::max_value() + // (otherwise cast below could overflow). + if idx >= self.len() { + Ok(None) + } else { + (js! { + return @{self.inner.as_ref()}[@{idx as u32}]; + }) + .into_expected_type() + .map(Some) + } + } + + /// Iterates over elements, panicking if any are not the expected type and + /// `check-all-casts` is enabled. + pub fn iter(&self) -> Iter { + Iter::new(self) + } + + /// Iterates over elements, returning an error if any are not the expected type and + /// `check-all-casts` is enabled. + /// + /// Use [`IntoIterator::into_iter`] to iterate expecting all types match. + pub fn try_iter(&self) -> TryIter { + TryIter::new(self) + } + + /// Iterates over elements, consuming self and returning a result if any are not the expected + /// type. + /// + /// Use [`IntoIterator::into_iter`] to iterate expecting all types match. + pub fn try_into_iter(self) -> TryIntoIter { + TryIntoIter::new(self) + } + + /// Turns this remote JS array into a local `Vec`, returning an error if any elements are + /// not the expected type and `check-all-casts` is enabled. + pub fn try_local(&self) -> Result, ConversionError> { + self.try_iter().collect() + } + + /// Turns this remote JS array into a local `Vec`, panicking if any elements are not the + /// expected type and `check-all-casts` is enabled. + pub fn local(&self) -> Vec { + self.iter().collect() + } +} + +pub struct IntoIter { + index: u32, + inner: JsVec, +} + +impl IntoIter { + pub fn new(vec: JsVec) -> Self { + IntoIter { + inner: vec, + index: 0, + } + } +} + +pub struct TryIntoIter { + index: u32, + inner: JsVec, +} + +impl TryIntoIter { + pub fn new(vec: JsVec) -> Self { + TryIntoIter { + inner: vec, + index: 0, + } + } +} + +pub struct Iter<'a, T> { + index: u32, + inner: &'a JsVec, +} + +impl<'a, T> Iter<'a, T> { + pub fn new(vec: &'a JsVec) -> Self { + Iter { + inner: vec, + index: 0, + } + } +} + +pub struct TryIter<'a, T> { + index: u32, + inner: &'a JsVec, +} + +impl<'a, T> TryIter<'a, T> { + pub fn new(vec: &'a JsVec) -> Self { + TryIter { + inner: vec, + index: 0, + } + } +} + +impl_js_vec_iterators_from_expected_type_panic!(Iter<'a>, IntoIter); +impl_js_vec_iterators_from_expected_type_with_result!(TryIter<'a>, TryIntoIter); + +impl IntoIterator for JsVec +where + T: FromExpectedType, +{ + type Item = T; + type IntoIter = IntoIter; + fn into_iter(self) -> Self::IntoIter { + IntoIter::new(self) + } +} + +impl<'a, T> IntoIterator for &'a JsVec +where + T: FromExpectedType, +{ + type Item = T; + type IntoIter = Iter<'a, T>; + fn into_iter(self) -> Self::IntoIter { + Iter::new(self) + } +} + +impl AsRef for JsVec { + fn as_ref(&self) -> &Array { + &self.inner + } +} + +impl From> for Array { + fn from(jsv: JsVec) -> Array { + jsv.inner + } +} + +impl TryFrom> for Array { + type Error = ConversionError; + + fn try_from(jsv: JsVec) -> Result { + Ok(jsv.into()) + } +} + +impl From> for Reference { + fn from(jsv: JsVec) -> Reference { + jsv.inner.into() + } +} + +impl TryFrom> for Reference { + type Error = ConversionError; + + fn try_from(jsv: JsVec) -> Result { + Ok(jsv.into()) + } +} + +impl InstanceOf for JsVec +where + T: InstanceOf, +{ + fn instance_of(reference: &Reference) -> bool { + (js! { + let arr = @{reference}; + if (!(arr instanceof Array)) { + return false; + } + for (let item of arr) { + if (!(@{|r: Reference| T::instance_of(&r)}(item))) { + return false; + } + } + return true; + }) + .try_into() + .expect("expected JsVec instance_of js code returning a bool to return a bool") + } +} + +impl TryFrom for JsVec +where + T: InstanceOf, +{ + type Error = ConversionError; + + fn try_from(arr: Array) -> Result, Self::Error> { + if arr.len() == 0 { + return Ok(JsVec { + inner: arr, + phantom: PhantomData, + }); + } + + // Type check array elements + if !Self::instance_of(arr.as_ref()) { + return Err(ConversionError::Custom( + "reference is of a different type".into(), + )); + } + Ok(JsVec { + inner: arr, + phantom: PhantomData, + }) + } +} + +impl TryFrom for JsVec +where + T: InstanceOf, +{ + type Error = ConversionError; + + fn try_from(r: Reference) -> Result, Self::Error> { + let arr: Array = r.try_into()?; + arr.try_into() + } +} + +impl TryFrom for JsVec +where + T: InstanceOf, +{ + type Error = ConversionError; + + fn try_from(r: Value) -> Result, Self::Error> { + let arr: Array = r.try_into()?; + arr.try_into() + } +} + +impl TryInto> for JsVec +where + T: TryFrom, +{ + type Error = ConversionError; + + fn try_into(self) -> Result, Self::Error> { + self.inner.try_into() + } +} + +impl AsRef for JsVec { + fn as_ref(&self) -> &Reference { + self.inner.as_ref() + } +} + +impl FromExpectedType for JsVec +where + T: InstanceOf, +{ + fn from_expected_type(arr: Array) -> Result { + #[cfg(feature = "check-all-casts")] + { + arr.try_into() + } + #[cfg(not(feature = "check-all-casts"))] + { + Ok(JsVec { + inner: arr, + phantom: PhantomData, + }) + } + } +} + +impl ReferenceType for JsVec +where + T: InstanceOf, +{ + unsafe fn from_reference_unchecked(reference: Reference) -> Self { + JsVec { + inner: Array::from_reference_unchecked(reference), + phantom: PhantomData, + } + } +} + +impl FromExpectedType for JsVec +where + T: InstanceOf, +{ + fn from_expected_type(r: Reference) -> Result { + #[cfg(feature = "check-all-casts")] + { + r.try_into() + } + #[cfg(not(feature = "check-all-casts"))] + { + Ok(unsafe { Self::from_reference_unchecked(r) }) + } + } +} + +impl<'a, T> From<&'a [T]> for JsVec +where + T: JsSerialize, +{ + fn from(v: &'a [T]) -> Self { + JsVec { + inner: v.into(), + phantom: PhantomData, + } + } +} + +impl<'a, T> From<&'a mut [T]> for JsVec +where + T: JsSerialize, +{ + fn from(v: &'a mut [T]) -> Self { + (&*v).into() + } +} + +impl<'a, T> From<&'a Vec> for JsVec +where + T: JsSerialize, +{ + fn from(v: &'a Vec) -> Self { + (&**v).into() + } +} + +impl<'a, T> From<&'a mut Vec> for JsVec +where + T: JsSerialize, +{ + fn from(v: &'a mut Vec) -> Self { + (&**v).into() + } +} + +impl From> for JsVec +where + T: JsSerialize, +{ + fn from(v: Vec) -> Self { + (&*v).into() + } +} diff --git a/screeps-game-api/src/js_collections/mod.rs b/screeps-game-api/src/js_collections/mod.rs new file mode 100644 index 00000000..6be7cc93 --- /dev/null +++ b/screeps-game-api/src/js_collections/mod.rs @@ -0,0 +1,4 @@ +//! Typed JavaScript collection wrappers. +mod js_vec; + +pub use self::js_vec::*; diff --git a/screeps-game-api/src/lib.rs b/screeps-game-api/src/lib.rs index a9483ebd..88a617ae 100644 --- a/screeps-game-api/src/lib.rs +++ b/screeps-game-api/src/lib.rs @@ -39,6 +39,7 @@ mod macros; pub mod constants; pub mod game; +pub mod js_collections; pub mod memory; pub mod objects; pub mod pathfinder; @@ -48,6 +49,7 @@ pub mod traits; pub use { constants::*, + js_collections::JsVec, objects::*, positions::{LocalRoomName, LocalRoomPosition}, traits::{FromExpectedType, IntoExpectedType}, diff --git a/screeps-game-api/src/macros.rs b/screeps-game-api/src/macros.rs index 3f761c18..873dd188 100644 --- a/screeps-game-api/src/macros.rs +++ b/screeps-game-api/src/macros.rs @@ -523,6 +523,94 @@ macro_rules! impl_serialize_as_u32 { }; } +/// Implements `Iterator` for `js_vec::IntoIter` or `js_vec::Iter`, using `FromExpectedType` and +/// panicking on incorrect types. +macro_rules! impl_js_vec_iterators_from_expected_type_panic { + ($($name:ident $(<$single_life_param:lifetime>)*),* $(,)*) => { + $( + impl<$($single_life_param, )* T> Iterator for $name<$($single_life_param, )* T> + where + T: FromExpectedType, + { + type Item = T; + + /// Gets the next item. + /// + /// # Panics + /// + /// Panics if the type is incorrect. + fn next(&mut self) -> Option { + if self.index as usize >= self.inner.len() { + None + } else { + let index = self.index; + self.index += 1; + + Some(js_unwrap_ref!(@{AsRef::::as_ref(&self.inner)}[@{index}])) + } + } + + fn size_hint(&self) -> (usize, Option) { + let length = self.inner.len(); + (length, Some(length)) + } + } + + impl<$($single_life_param, )* T> ::std::iter::ExactSizeIterator + for $name<$($single_life_param, )* T> + where + T: FromExpectedType, + { + fn len(&self) -> usize { + self.inner.len() + } + } + )* + } +} + +/// Implements `Iterator` for `js_vec::IntoIter` or `js_vec::Iter`. +macro_rules! impl_js_vec_iterators_from_expected_type_with_result { + ($($name:ident $(<$single_life_param:lifetime>)*),* $(,)*) => { + $( + impl<$($single_life_param, )* T> Iterator for $name<$($single_life_param, )* T> + where + T: FromExpectedType, + { + type Item = Result; + + fn next(&mut self) -> Option { + if self.index as usize >= self.inner.len() { + None + } else { + let index = self.index; + self.index += 1; + + Some(FromExpectedType::from_expected_type( + js!(@{AsRef::::as_ref(&self.inner)}[@{index}]) + )) + } + } + + fn size_hint(&self) -> (usize, Option) { + let length = self.inner.len(); + (length, Some(length)) + } + } + + impl<$($single_life_param, )* T> ::std::iter::ExactSizeIterator + for $name<$($single_life_param, )* T> + where + T: FromExpectedType, + { + fn len(&self) -> usize { + self.inner.len() + } + } + )* + } +} + /// Get a value from memory given a path, returning `None` if any thing along the way does not /// exist. ///