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}