Skip to main content

boa_engine/value/conversions/nullable/
mod.rs

1//! A value that can be `null` in JavaScript, but not `undefined`.
2
3use crate::value::{TryFromJs, TryIntoJs};
4use boa_engine::{Context, JsResult, JsValue};
5
6#[cfg(test)]
7mod tests;
8
9/// A value that can be `null` in JavaScript, but not `undefined`.
10///
11/// This is used to distinguish between values in JavaScript that can be
12/// `null` (i.e. [`Nullable<T>`]) vs. those of which that can be `undefined`
13/// (i.e. [`Option<T>`]).
14///
15/// [`Nullable<T>`] tries to be close in API surface to the standard
16/// [`Option<T>`], though with a much smaller API scope, and can be
17/// transformed into and from [`Option<T>`] for free.
18///
19/// ## Why a new type?
20/// There are in the standard 2 different values that are "null coalescing":
21/// 1. `undefined`, which is its own type (`typeof void 0 == "undefined"`),
22/// 2. `null`, which is different from `undefined` (`undefined !== null`) but
23///    is of type object (`typeof null === "object"`) for legacy reasons.
24///
25/// [`Option<T>`] sets up the first case, so we needed to add a new type for
26/// the second case.
27///
28/// ## Would it be bad to use `Option::<T>::None` as both undefined _or_ null?
29/// Many values in the standard can be `null` but not `undefined`, or the other
30/// way around. Coalescing these two into `Option::None` means that any
31/// conversions using boa's traits (such as `TryFromJs`) would result in
32/// indistinguishable values. The only way to respect the standard in this case
33/// would be to get `JsValue` and manually verify it's not undefined/null, then
34/// do the conversion. This new type makes this process much simpler.
35///
36/// It also means that there is an asymmetry between `JsValue` to [`Option<T>`]
37/// then [`Option<T>`] back to `JsValue`, leading to unintuitive errors. Having
38/// a type [`Nullable<T>`] that acts like [`Option<T>`] but (de-)serialize to
39/// `null` clarifies all usage.
40///
41/// ## How can I do `EitherNullOrUndefined<T>`?
42/// The best way is to use `Nullable<Option<T>>` and convert into [`Option<T>`]
43/// using [`Nullable::flatten()`]. Please note that the reverse
44/// (`Nullable<Option<T>>`) results in the same deserializing behaviour but
45/// does not implement `flatten()`.
46///
47/// Please note that JavaScript cannot make a distinction between [`Option<T>`] and
48/// `Option<Option<T>>`. This cannot be resolved using this type, as it suffers
49/// from the same limitation. There is no way to distinguish between `Null` and
50/// `NonNull(Null)`. `Nullable<Nullable<T>>` does not provide additional
51/// information.
52///
53/// # Examples
54/// ```
55/// # use boa_engine::{Context, JsValue};
56/// # use boa_engine::value::Nullable;
57/// # let context = &mut Context::default();
58/// let maybe_10: Nullable<u8> = JsValue::new(10).try_js_into(context).unwrap();
59/// assert_eq!(maybe_10, Nullable::NonNull(10u8));
60///
61/// let maybe_not: Nullable<u8> = JsValue::null().try_js_into(context).unwrap();
62/// assert_eq!(maybe_not, Nullable::Null);
63/// ```
64///
65/// ```
66/// # use boa_engine::{Context, JsResult, JsValue};
67/// # use boa_engine::value::Nullable;
68/// # let context = &mut Context::default();
69/// let mut v: JsResult<Nullable<Option<u8>>> =
70///     JsValue::undefined().try_js_into(context);
71/// assert_eq!(v, Ok(Nullable::NonNull(None)));
72///
73/// v = JsValue::null().try_js_into(context);
74/// assert_eq!(v, Ok(Nullable::Null));
75///
76/// v = JsValue::from(42).try_js_into(context);
77/// assert_eq!(v, Ok(Nullable::NonNull(Some(42))));
78/// assert_eq!(v.unwrap().flatten(), Some(42));
79/// ```
80#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Hash, Ord, Eq)]
81pub enum Nullable<T> {
82    /// The value was not defined in JavaScript.
83    Null,
84
85    /// The value was defined in JavaScript.
86    NonNull(T),
87}
88
89impl<T> Nullable<T> {
90    /// Returns `true` if this value is `Nullable::Null`.
91    pub const fn is_null(&self) -> bool {
92        matches!(self, Nullable::Null)
93    }
94
95    /// Returns `true` if this value is `Nullable::NotNull`.
96    pub const fn is_not_null(&self) -> bool {
97        matches!(self, Nullable::NonNull(_))
98    }
99
100    /// Returns an iterator over the possibly contained value.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// # use boa_engine::value::Nullable;
106    /// let x = Nullable::NonNull(4);
107    /// assert_eq!(x.iter().next(), Some(&4));
108    ///
109    /// let x: Nullable<u32> = Nullable::Null;
110    /// assert_eq!(x.iter().next(), None);
111    /// ```
112    #[inline]
113    pub fn iter(&self) -> Iter<'_, T> {
114        self.into_iter()
115    }
116
117    /// Converts from `&Nullable<T>` to `Nullable<&T>`.
118    ///
119    /// # Examples
120    /// ```
121    /// # use boa_engine::value::Nullable;
122    /// let text: Nullable<String> = Nullable::NonNull("Hello, world!".to_string());
123    ///
124    /// // First, cast `Nullable<String>` to `Nullable<&String>` with `as_ref`,
125    /// // then consume *that* with `map`, leaving `text` on the stack.
126    /// let text_length: Nullable<usize> = text.as_ref().map(|s| s.len());
127    /// println!("still can print text: {text:?}");
128    /// ```
129    #[inline]
130    pub const fn as_ref(&self) -> Nullable<&T> {
131        match self {
132            Nullable::Null => Nullable::Null,
133            Nullable::NonNull(t) => Nullable::NonNull(t),
134        }
135    }
136
137    /// Maps a [`Nullable<T>`] to [`Nullable<U>`] by applying a function to a contained
138    /// value.
139    /// # Examples
140    ///
141    /// ```
142    /// # use boa_engine::value::Nullable;
143    /// let maybe_some_string = Nullable::NonNull(String::from("Hello, World!"));
144    /// // `Nullable::map` takes self *by value*, consuming `maybe_some_string`
145    /// let maybe_some_len = maybe_some_string.map(|s| s.len());
146    /// assert_eq!(maybe_some_len, Nullable::NonNull(13));
147    ///
148    /// let x: Nullable<&str> = Nullable::Null;
149    /// assert_eq!(x.map(|s| s.len()), Nullable::Null);
150    /// ```
151    #[inline]
152    pub fn map<U, F>(self, f: F) -> Nullable<U>
153    where
154        F: FnOnce(T) -> U,
155    {
156        match self {
157            Nullable::NonNull(x) => Nullable::NonNull(f(x)),
158            Nullable::Null => Nullable::Null,
159        }
160    }
161
162    /// Returns the contained [`Nullable::NonNull`] value or a default.
163    ///
164    /// Consumes the `self` argument then, if [`Nullable::NonNull`], returns the contained
165    /// value, otherwise if [`Nullable::Null`], returns the [default value] for that
166    /// type.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// # use boa_engine::value::Nullable;
172    /// let x: Nullable<u32> = Nullable::Null;
173    /// let y: Nullable<u32> = Nullable::NonNull(12);
174    ///
175    /// assert_eq!(x.unwrap_or_default(), 0);
176    /// assert_eq!(y.unwrap_or_default(), 12);
177    /// ```
178    ///
179    /// [default value]: Default::default
180    /// [`parse`]: str::parse
181    #[inline]
182    pub fn unwrap_or_default(self) -> T
183    where
184        T: Default,
185    {
186        match self {
187            Nullable::NonNull(x) => x,
188            Nullable::Null => T::default(),
189        }
190    }
191}
192
193impl<'a, T> IntoIterator for &'a Nullable<T> {
194    type Item = &'a T;
195    type IntoIter = Iter<'a, T>;
196
197    #[inline]
198    fn into_iter(self) -> Iter<'a, T> {
199        Iter(match self {
200            Nullable::Null => None,
201            Nullable::NonNull(v) => Some(v),
202        })
203    }
204}
205
206impl<T> IntoIterator for Nullable<T> {
207    type Item = T;
208    type IntoIter = IntoIter<T>;
209
210    /// Returns a consuming iterator over the possibly contained value.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// # use boa_engine::value::Nullable;
216    /// let x = Nullable::NonNull("string");
217    /// let v: Vec<&str> = x.into_iter().collect();
218    /// assert_eq!(v, ["string"]);
219    ///
220    /// let x = Nullable::Null;
221    /// let v: Vec<&str> = x.into_iter().collect();
222    /// assert!(v.is_empty());
223    /// ```
224    #[inline]
225    fn into_iter(self) -> IntoIter<T> {
226        IntoIter(self.into())
227    }
228}
229
230impl<T: TryFromJs> TryFromJs for Nullable<T> {
231    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
232        if value.is_null() {
233            Ok(Nullable::Null)
234        } else {
235            T::try_from_js(value, context).map(Nullable::NonNull)
236        }
237    }
238}
239
240impl<T: TryIntoJs> TryIntoJs for Nullable<T> {
241    fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue> {
242        match self {
243            Nullable::Null => Ok(JsValue::null()),
244            Nullable::NonNull(t) => t.try_into_js(context),
245        }
246    }
247}
248
249impl<T> From<Nullable<T>> for Option<T> {
250    #[inline]
251    fn from(value: Nullable<T>) -> Self {
252        match value {
253            Nullable::Null => None,
254            Nullable::NonNull(t) => Some(t),
255        }
256    }
257}
258
259impl<T> From<Option<T>> for Nullable<T> {
260    #[inline]
261    fn from(value: Option<T>) -> Self {
262        match value {
263            None => Nullable::Null,
264            Some(t) => Nullable::NonNull(t),
265        }
266    }
267}
268
269/// An iterator over a [`Nullable`] value reference.
270#[derive(Debug)]
271pub struct Iter<'a, A>(Option<&'a A>);
272
273impl<'a, T: 'a> Iterator for Iter<'a, T> {
274    type Item = &'a T;
275
276    fn next(&mut self) -> Option<Self::Item> {
277        self.0.take()
278    }
279}
280
281impl<'a, T: 'a> DoubleEndedIterator for Iter<'a, T> {
282    fn next_back(&mut self) -> Option<Self::Item> {
283        self.0.take()
284    }
285}
286
287impl<A> ExactSizeIterator for Iter<'_, A> {
288    #[inline]
289    fn len(&self) -> usize {
290        usize::from(self.0.is_some())
291    }
292}
293
294/// An owning iterator over a [`Nullable`] value.
295#[derive(Debug)]
296pub struct IntoIter<A>(Option<A>);
297
298impl<A> Iterator for IntoIter<A> {
299    type Item = A;
300    fn next(&mut self) -> Option<Self::Item> {
301        self.0.take()
302    }
303}
304
305impl<A> DoubleEndedIterator for IntoIter<A> {
306    fn next_back(&mut self) -> Option<A> {
307        self.0.take()
308    }
309}
310
311impl<A> ExactSizeIterator for IntoIter<A> {
312    #[inline]
313    fn len(&self) -> usize {
314        usize::from(self.0.is_some())
315    }
316}
317
318impl<T> Nullable<Option<T>> {
319    /// Converts from `Nullable<Option<T>>` to [`Option<T>`].
320    #[inline]
321    pub fn flatten(self) -> Option<T> {
322        match self {
323            Nullable::Null | Nullable::NonNull(None) => None,
324            Nullable::NonNull(Some(t)) => Some(t),
325        }
326    }
327}