screeps/
js_collections.rs

1//! Typed JavaScript collection wrappers.
2use std::{fmt, marker::PhantomData, str::FromStr};
3
4use js_sys::{Array, JsString, Object};
5use wasm_bindgen::prelude::*;
6
7use crate::{game, local::RawObjectIdParseError, prelude::*};
8
9#[wasm_bindgen]
10extern "C" {
11    #[derive(Clone)]
12    #[wasm_bindgen(extends = Object)]
13    pub(crate) type ObjectExt;
14
15    #[wasm_bindgen(method, structural, indexing_setter)]
16    pub(crate) fn set(this: &ObjectExt, prop: &str, val: &JsValue);
17
18    #[wasm_bindgen(method, structural, indexing_setter)]
19    pub(crate) fn set_value(this: &ObjectExt, prop: &JsValue, val: &JsValue);
20
21    #[wasm_bindgen(method, structural, indexing_getter)]
22    pub(crate) fn get_value(this: &ObjectExt, prop: &JsValue) -> JsValue;
23}
24
25pub trait JsCollectionIntoValue {
26    fn into_value(self) -> JsValue;
27}
28
29pub trait JsCollectionFromValue {
30    fn from_value(val: JsValue) -> Self;
31}
32
33/// Container holding a reference to an [`Object`] in JavaScript as well as
34/// expected types for both the keys and values.
35pub struct JsHashMap<K, V> {
36    map: Object,
37    _phantom: PhantomData<(K, V)>,
38}
39
40impl<K, V> JsHashMap<K, V>
41where
42    K: JsCollectionFromValue,
43{
44    pub fn keys(&self) -> impl Iterator<Item = K> {
45        let array = Object::keys(self.map.unchecked_ref());
46
47        OwnedArrayIter::new(array)
48    }
49}
50
51impl<K, V> JsHashMap<K, V>
52where
53    V: JsCollectionFromValue,
54{
55    pub fn values(&self) -> impl Iterator<Item = V> {
56        let array = Object::values(self.map.unchecked_ref());
57
58        OwnedArrayIter::new(array)
59    }
60}
61
62impl<K, V> JsHashMap<K, V>
63where
64    K: JsCollectionFromValue,
65    V: JsCollectionFromValue,
66{
67    pub fn entries(&self) -> impl Iterator<Item = (K, V)> {
68        let array = Object::entries(self.map.unchecked_ref());
69
70        OwnedArrayIter::new(array)
71    }
72}
73
74impl<K, V> JsHashMap<K, V>
75where
76    K: JsCollectionIntoValue,
77    V: JsCollectionFromValue,
78{
79    pub fn get(&self, key: K) -> Option<V> {
80        let key = key.into_value();
81        let val = JsCast::unchecked_ref::<ObjectExt>(&self.map).get_value(&key);
82        if val.is_null() || val.is_undefined() {
83            return None;
84        }
85        let val = V::from_value(val);
86
87        Some(val)
88    }
89}
90
91impl<K, V> JsHashMap<K, V>
92where
93    K: JsCollectionIntoValue,
94    V: JsCollectionIntoValue,
95{
96    pub fn set(&self, key: K, value: V) {
97        let key = key.into_value();
98        let value = value.into_value();
99        JsCast::unchecked_ref::<ObjectExt>(&self.map).set_value(&key, &value);
100    }
101}
102
103impl<K, V> From<Object> for JsHashMap<K, V> {
104    fn from(map: Object) -> Self {
105        Self {
106            map,
107            _phantom: Default::default(),
108        }
109    }
110}
111
112impl<K, V> From<JsValue> for JsHashMap<K, V> {
113    fn from(val: JsValue) -> Self {
114        Self {
115            map: val.into(),
116            _phantom: Default::default(),
117        }
118    }
119}
120
121#[derive(Debug, Clone)]
122pub struct OwnedArrayIter<T> {
123    range: std::ops::Range<u32>,
124    array: Array,
125    _phantom: PhantomData<T>,
126}
127
128impl<T> OwnedArrayIter<T> {
129    pub fn new(array: Array) -> Self {
130        OwnedArrayIter {
131            range: 0..array.length(),
132            array,
133            _phantom: Default::default(),
134        }
135    }
136}
137
138impl<T> std::iter::Iterator for OwnedArrayIter<T>
139where
140    T: JsCollectionFromValue,
141{
142    type Item = T;
143
144    fn next(&mut self) -> Option<Self::Item> {
145        let index = self.range.next()?;
146        let val = self.array.get(index);
147        let val = T::from_value(val);
148        Some(val)
149    }
150
151    #[inline]
152    fn size_hint(&self) -> (usize, Option<usize>) {
153        self.range.size_hint()
154    }
155}
156
157impl<T> std::iter::DoubleEndedIterator for OwnedArrayIter<T>
158where
159    T: JsCollectionFromValue,
160{
161    fn next_back(&mut self) -> Option<Self::Item> {
162        let index = self.range.next_back()?;
163        let val = self.array.get(index);
164        let val = T::from_value(val);
165        Some(val)
166    }
167}
168
169impl<T> std::iter::FusedIterator for OwnedArrayIter<T> where T: JsCollectionFromValue {}
170
171impl<T> std::iter::ExactSizeIterator for OwnedArrayIter<T> where T: JsCollectionFromValue {}
172
173/// Represents a reference to an Object ID string in JavaScript memory, typed
174/// according to the object type Rust expects for the object after resolving.
175///
176/// Use [`ObjectId`] if a value stored in Rust memory is preferred; the
177/// JavaScript representation can be harder to work with in Rust code due to
178/// lack of visibility on the underlying string and lack of most trait
179/// implementations, and consumes more memory, but is faster to resolve and may
180/// be useful with objects you plan to resolve frequently.
181///
182/// This object ID is typed, but not strictly, and can be converted into
183/// referring into another type of object with [`JsObjectId::into_type`].
184///
185/// [`ObjectId`]: crate::local::ObjectId
186// Copy, Clone, Debug, PartialEq, Eq, Hash, PartialEq, Eq implemented manually
187// below
188pub struct JsObjectId<T> {
189    pub raw: JsString,
190    phantom: PhantomData<T>,
191}
192
193// traits implemented manually so they don't depend on `T` implementing them.
194impl<T> Clone for JsObjectId<T> {
195    fn clone(&self) -> JsObjectId<T> {
196        JsObjectId {
197            raw: self.raw.clone(),
198            phantom: PhantomData,
199        }
200    }
201}
202impl<T> fmt::Debug for JsObjectId<T> {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        self.raw.fmt(f)
205    }
206}
207impl<T> PartialEq for JsObjectId<T> {
208    fn eq(&self, o: &JsObjectId<T>) -> bool {
209        self.raw.eq(&o.raw)
210    }
211}
212impl<T> Eq for JsObjectId<T> {}
213
214impl<T> FromStr for JsObjectId<T> {
215    type Err = RawObjectIdParseError;
216
217    fn from_str(s: &str) -> Result<Self, Self::Err> {
218        let js_string: JsString = s.into();
219        Ok(js_string.into())
220    }
221}
222
223impl<T> fmt::Display for JsObjectId<T> {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        String::from(&self.raw).fmt(f)
226    }
227}
228
229impl<T> JsObjectId<T> {
230    /// Changes the type this [`JsObjectId`] points to, unchecked.
231    ///
232    /// This will allow changing to any type - `JsObjectId` makes no guarantees
233    /// about its ID matching the type of any object in the game that it
234    /// actually points to.
235    pub fn into_type<U>(self) -> JsObjectId<U> {
236        JsObjectId {
237            raw: self.raw,
238            phantom: PhantomData,
239        }
240    }
241
242    /// Resolves this ID into an object, assuming the type `T` is the correct
243    /// type of object that this ID refers to. If the ID has been converted to
244    /// an invalid type, using the returned object in a way not valid for its
245    /// type will cause a panic.
246    ///
247    /// Will return `None` if this object no longer exists, or is in a room we
248    /// don't have vision for.
249    pub fn resolve(&self) -> Option<T>
250    where
251        T: MaybeHasId + JsCast,
252    {
253        game::get_object_by_js_id_typed(self)
254    }
255}
256
257impl<T> From<JsString> for JsObjectId<T> {
258    fn from(raw: JsString) -> Self {
259        JsObjectId {
260            raw,
261            phantom: PhantomData,
262        }
263    }
264}
265
266impl<T> From<JsObjectId<T>> for JsString {
267    fn from(id: JsObjectId<T>) -> Self {
268        id.raw
269    }
270}
271
272impl<T> From<JsObjectId<T>> for String {
273    fn from(id: JsObjectId<T>) -> Self {
274        id.to_string()
275    }
276}
277
278//
279// Utility conversions for collections.
280//
281impl JsCollectionIntoValue for JsString {
282    fn into_value(self) -> JsValue {
283        self.unchecked_into()
284    }
285}
286
287impl JsCollectionFromValue for JsString {
288    fn from_value(val: JsValue) -> JsString {
289        val.unchecked_into()
290    }
291}
292
293impl JsCollectionIntoValue for String {
294    fn into_value(self) -> JsValue {
295        self.into()
296    }
297}
298
299impl JsCollectionFromValue for String {
300    fn from_value(val: JsValue) -> String {
301        let val: JsString = val.unchecked_into();
302
303        val.into()
304    }
305}
306
307impl JsCollectionIntoValue for u8 {
308    fn into_value(self) -> JsValue {
309        JsValue::from_f64(self as f64)
310    }
311}
312
313impl JsCollectionFromValue for u8 {
314    fn from_value(val: JsValue) -> u8 {
315        if let Some(val) = val.as_string() {
316            val.parse::<u8>().expect("expected parseable u8 string")
317        } else {
318            val.as_f64().expect("expected number value") as u8
319        }
320    }
321}
322
323impl JsCollectionIntoValue for u32 {
324    fn into_value(self) -> JsValue {
325        JsValue::from_f64(self as f64)
326    }
327}
328
329impl JsCollectionFromValue for u32 {
330    fn from_value(val: JsValue) -> u32 {
331        if let Some(val) = val.as_string() {
332            val.parse::<u32>().expect("expected parseable u32 string")
333        } else {
334            val.as_f64().expect("expected number value") as u32
335        }
336    }
337}
338
339impl<K, V> JsCollectionFromValue for (K, V)
340where
341    K: JsCollectionFromValue,
342    V: JsCollectionFromValue,
343{
344    fn from_value(val: JsValue) -> Self {
345        let val: &Array = val.dyn_ref().expect("expected tuple of length 2");
346        let k = K::from_value(val.get(0));
347        let v = V::from_value(val.get(1));
348        (k, v)
349    }
350}