Skip to main content

ferrijs_std/utils/
any_of.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use rquickjs::{
4    class::{Trace, Tracer},
5    Ctx, FromJs, IntoJs, JsLifetime, Result, Value,
6};
7
8macro_rules! define_any_of {
9    ($name:ident, $($variant:ident),+) => {
10        #[derive(Debug, Clone)]
11        pub enum $name<$($variant),+> {
12            $(
13                $variant($variant),
14            )+
15        }
16
17        define_any_of_from_js!($name, $($variant),+);
18
19        impl<'js, $($variant: IntoJs<'js>),+> IntoJs<'js> for $name<$($variant),+> {
20            fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
21                match self {
22                    $(
23                        Self::$variant(val) => val.into_js(ctx),
24                    )+
25                }
26            }
27        }
28
29        unsafe impl<'js, $($variant: JsLifetime<'js>),+> JsLifetime<'js> for $name<$($variant),+> {
30            type Changed<'to> = $name<$($variant::Changed<'to>),+>;
31        }
32
33        impl<'js, $($variant: Trace<'js>),+> Trace<'js> for $name<$($variant),+> {
34            fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
35                match self {
36                    $(
37                        Self::$variant(val) => val.trace(tracer),
38                    )+
39                }
40            }
41        }
42
43        define_any_of_methods!($name, $($variant),+);
44    };
45}
46
47macro_rules! define_any_of_from_js {
48    ($name:ident, $first:ident, $($rest:ident),+) => {
49        impl<'js, $first: FromJs<'js>, $($rest: FromJs<'js>),+> FromJs<'js> for $name<$first, $($rest),+> {
50            fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
51                define_any_of_from_js_impl!($name, ctx, value, $first, $($rest),+)
52            }
53        }
54    };
55}
56
57macro_rules! define_any_of_from_js_impl {
58    ($name:ident, $ctx:ident, $value:ident, $first:ident) => {
59        $first::from_js($ctx, $value).map($name::$first)
60    };
61
62    ($name:ident, $ctx:ident, $value:ident, $first:ident, $($rest:ident),+) => {
63        $first::from_js($ctx, $value.clone()).map($name::$first).or_else(|error| {
64            if error.is_from_js() {
65                define_any_of_from_js_impl!($name, $ctx, $value, $($rest),+)
66            } else {
67                Err(error)
68            }
69        })
70    };
71}
72
73macro_rules! define_any_of_variant_methods {
74    ($variant:ident, $is_fn:ident, $as_fn:ident, $as_mut_fn:ident, $into_fn:ident) => {
75        #[allow(dead_code)]
76        pub fn $is_fn(&self) -> bool {
77            matches!(self, Self::$variant(_))
78        }
79
80        #[allow(dead_code)]
81        pub fn $as_fn(&self) -> Option<&$variant> {
82            match self {
83                Self::$variant(val) => Some(val),
84                _ => None,
85            }
86        }
87
88        #[allow(dead_code)]
89        pub fn $as_mut_fn(&mut self) -> Option<&mut $variant> {
90            match self {
91                Self::$variant(val) => Some(val),
92                _ => None,
93            }
94        }
95
96        #[allow(dead_code)]
97        pub fn $into_fn(self) -> std::result::Result<$variant, Self> {
98            match self {
99                Self::$variant(val) => Ok(val),
100                other => Err(other),
101            }
102        }
103    };
104
105    (A) => {
106        define_any_of_variant_methods!(A, is_a, as_a, as_a_mut, into_a);
107    };
108    (B) => {
109        define_any_of_variant_methods!(B, is_b, as_b, as_b_mut, into_b);
110    };
111    (C) => {
112        define_any_of_variant_methods!(C, is_c, as_c, as_c_mut, into_c);
113    };
114    (D) => {
115        define_any_of_variant_methods!(D, is_d, as_d, as_d_mut, into_d);
116    };
117    (E) => {
118        define_any_of_variant_methods!(E, is_e, as_e, as_e_mut, into_e);
119    };
120    (F) => {
121        define_any_of_variant_methods!(F, is_f, as_f, as_f_mut, into_f);
122    };
123    (G) => {
124        define_any_of_variant_methods!(G, is_g, as_g, as_g_mut, into_g);
125    };
126    (H) => {
127        define_any_of_variant_methods!(H, is_h, as_h, as_h_mut, into_h);
128    };
129}
130
131macro_rules! define_any_of_methods {
132    ($name:ident, $($variant:ident),+) => {
133        impl<$($variant),+> $name<$($variant),+> {
134            $(
135                define_any_of_variant_methods!($variant);
136            )+
137        }
138    };
139}
140
141define_any_of!(AnyOf2, A, B);
142define_any_of!(AnyOf3, A, B, C);
143define_any_of!(AnyOf4, A, B, C, D);
144define_any_of!(AnyOf5, A, B, C, D, E);
145define_any_of!(AnyOf6, A, B, C, D, E, F);
146define_any_of!(AnyOf7, A, B, C, D, E, F, G);
147define_any_of!(AnyOf8, A, B, C, D, E, F, G, H);
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use rquickjs::{Context, Runtime};
153
154    #[test]
155    fn test_any_of_string_number() {
156        let rt = Runtime::new().unwrap();
157        let ctx = Context::full(&rt).unwrap();
158
159        ctx.with(|ctx| {
160            // Test string conversion
161            let val: Value = ctx.eval("'hello'").unwrap();
162            let any: AnyOf2<String, i32> = AnyOf2::from_js(&ctx, val).unwrap();
163            assert!(any.is_a());
164            assert_eq!(any.as_a().unwrap(), "hello");
165            assert!(!any.is_b());
166            assert!(any.as_b().is_none());
167
168            // Test number conversion
169            let val: Value = ctx.eval("42").unwrap();
170            let any: AnyOf2<String, i32> = AnyOf2::from_js(&ctx, val).unwrap();
171            assert!(!any.is_a());
172            assert!(any.is_b());
173            assert_eq!(*any.as_b().unwrap(), 42);
174        });
175    }
176
177    #[test]
178    fn test_any_of_fallback() {
179        let rt = Runtime::new().unwrap();
180        let ctx = Context::full(&rt).unwrap();
181
182        ctx.with(|ctx| {
183            // Test that it tries in order
184            let val: Value = ctx.eval("true").unwrap();
185            let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
186            assert!(any.is_c());
187            assert!(*any.as_c().unwrap());
188            assert!(!any.is_a());
189            assert!(!any.is_b());
190        });
191    }
192
193    #[test]
194    fn test_any_of_into_js() {
195        let rt = Runtime::new().unwrap();
196        let ctx = Context::full(&rt).unwrap();
197
198        ctx.with(|ctx| {
199            let any: AnyOf2<String, i32> = AnyOf2::A("test".to_string());
200            let val: Value = any.into_js(&ctx).unwrap();
201            let result: String = val.get().unwrap();
202            assert_eq!(result, "test");
203
204            let any: AnyOf2<String, i32> = AnyOf2::B(99);
205            let val: Value = any.into_js(&ctx).unwrap();
206            let result: i32 = val.get().unwrap();
207            assert_eq!(result, 99);
208        });
209    }
210
211    #[test]
212    fn test_any_of_methods() {
213        let rt = Runtime::new().unwrap();
214        let ctx = Context::full(&rt).unwrap();
215
216        ctx.with(|ctx| {
217            // Test all methods for variant A
218            let val: Value = ctx.eval("'test'").unwrap();
219            let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
220            assert!(any.is_a());
221            assert_eq!(any.as_a().unwrap(), "test");
222            assert_eq!(any.into_a().unwrap(), "test");
223
224            // Test all methods for variant B
225            let val: Value = ctx.eval("42").unwrap();
226            let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
227            assert!(any.is_b());
228            assert_eq!(*any.as_b().unwrap(), 42);
229            assert_eq!(any.into_b().unwrap(), 42);
230
231            // Test all methods for variant C
232            let val: Value = ctx.eval("true").unwrap();
233            let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
234            assert!(any.is_c());
235            assert!(*any.as_c().unwrap());
236            assert!(any.into_c().unwrap());
237        });
238    }
239
240    #[test]
241    fn test_any_of_mutable_methods() {
242        let rt = Runtime::new().unwrap();
243        let ctx = Context::full(&rt).unwrap();
244
245        ctx.with(|ctx| {
246            let val: Value = ctx.eval("42").unwrap();
247            let mut any: AnyOf4<String, i32, bool, f64> = AnyOf4::from_js(&ctx, val).unwrap();
248
249            if let Some(n) = any.as_b_mut() {
250                *n = 100;
251            }
252
253            assert_eq!(any.into_b().unwrap(), 100);
254        });
255    }
256
257    #[test]
258    fn test_any_of_error_propagation() {
259        use rquickjs::{Array, Object};
260
261        let rt = Runtime::new().unwrap();
262        let ctx = Context::full(&rt).unwrap();
263
264        ctx.with(|ctx| {
265            // Test that conversion errors cause fallback to next type
266            let val: Value = ctx.eval("42").unwrap();
267            let any: AnyOf2<String, i32> = AnyOf2::from_js(&ctx, val).unwrap();
268            assert!(any.is_b());
269
270            // Test that all types fail results in an error
271            let val: Value = ctx.eval("null").unwrap();
272            let result: Result<AnyOf2<Object, Array>> = AnyOf2::from_js(&ctx, val);
273            assert!(result.is_err());
274        });
275    }
276
277    #[test]
278    fn test_any_of_conversion_order() {
279        let rt = Runtime::new().unwrap();
280        let ctx = Context::full(&rt).unwrap();
281
282        ctx.with(|ctx| {
283            // Test that conversion happens in order A, B, C, D, E
284            // Since 42 can be converted to f64, i32, etc., but String comes first and fails,
285            // it should try the next successful conversion
286            let val: Value = ctx.eval("42").unwrap();
287
288            // String should fail, so it tries i32 which succeeds
289            let any: AnyOf3<String, i32, f64> = AnyOf3::from_js(&ctx, val).unwrap();
290            assert!(any.is_b());
291
292            // If we flip the order, f64 would be tried first (but both work)
293            let val: Value = ctx.eval("3.14").unwrap();
294            let any: AnyOf3<String, f64, i32> = AnyOf3::from_js(&ctx, val).unwrap();
295            assert!(any.is_b()); // f64 should succeed first
296        });
297    }
298}