arri_repr/
exportable.rs

1/// Provides functionality for exporting types into Arri schemas.
2///
3/// This module defines the `Exportable` trait, which allows types to be
4/// converted into serializable schemas. It also includes macros and utilities
5/// for handling generic types, type schemas, and feature-specific schemas.
6use crate::{
7    EmptySchema, PropertiesSchema, RefSchema, TaggedUnionSchema, ValuesSchema, type_utils,
8};
9use crate::{Serializable, TypeSchema, Types, elements::ElementsSchema};
10use std::cell::{Cell, RefCell};
11use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque};
12use std::ffi::{OsStr, OsString};
13use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
14use std::num::{
15    NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroIsize, NonZeroU8, NonZeroU16, NonZeroU32,
16    NonZeroU64, NonZeroUsize,
17};
18use std::path::{Path, PathBuf};
19use std::ptr::NonNull;
20use std::rc::Rc;
21use std::sync::atomic::{
22    AtomicBool, AtomicI8, AtomicI16, AtomicI32, AtomicI64, AtomicU8, AtomicU16, AtomicU32,
23    AtomicU64,
24};
25use std::sync::{Arc, Mutex, RwLock};
26use std::time::{Duration, Instant, SystemTime};
27
28thread_local! {
29    /// Tracks types currently being exported to prevent infinite recursion.
30    static RECURSION_TRACKER: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
31}
32
33/// A trait for types that can be exported into Arri schemas.
34pub trait Exportable {
35    /// Retrieves the type name of the implementing type.
36    fn get_type_name() -> String {
37        type_utils::get_type_name::<Self>()
38    }
39
40    /// Exports the type into a serializable schema.
41    ///
42    /// This method ensures that recursive types are handled correctly.
43    fn export() -> Box<dyn Serializable> {
44        Self::export_with_recursion_check()
45    }
46
47    /// Internal method for exporting the type.
48    ///
49    /// This method should be implemented by types to define their specific
50    /// export logic.
51    fn export_internal() -> impl Serializable;
52
53    /// Exports the type with recursion tracking to prevent infinite loops.
54    fn export_with_recursion_check() -> Box<dyn Serializable> {
55        let type_name = Self::get_type_name();
56
57        let is_recursive = RECURSION_TRACKER.with(|tracker| {
58            let mut tracker = tracker.borrow_mut();
59            let is_recursive = tracker.contains(&type_name);
60
61            if !is_recursive {
62                // Add the type to the recursion tracker.
63                tracker.insert(type_name.clone());
64            }
65
66            is_recursive
67        });
68
69        if is_recursive {
70            return Box::new(RefSchema::new(type_utils::get_type_name_from(type_name)));
71        }
72
73        let result = Self::export_internal();
74
75        // Remove the type from the recursion tracker after exporting.
76        RECURSION_TRACKER.with(|tracker| {
77            let mut tracker = tracker.borrow_mut();
78            tracker.remove(&type_name);
79        });
80
81        Box::new(result)
82    }
83}
84
85/// A macro for defining exportable types and schemas.
86///
87/// This macro provides a convenient way to define type schemas, generic
88/// implementations, and feature-specific schemas for types.
89macro_rules! exportable {
90    // --- Main entry points ---
91    // Handle individual blocks
92    (generic: { $($gen:tt)* }) => {
93        exportable!(@parse_impls $($gen)*);
94    };
95    (typeschema: { $($ty:tt)* }) => {
96        exportable!(@parse_typeschema $($ty)*);
97    };
98    (features: { $($feat:tt)* }) => {
99        exportable!(@parse_features $($feat)*);
100    };
101
102    // Handle combination of blocks
103    (
104        typeschema: { $($ty:tt)* },
105        generic: { $($gen:tt)* },
106        features: { $($feat:tt)* }
107    ) => {
108        exportable!(typeschema: { $($ty)* });
109        exportable!(generic: { $($gen)* });
110        exportable!(features: { $($feat)* });
111    };
112
113    (
114        typeschema: { $($ty:tt)* },
115        generic: { $($gen:tt)* }
116    ) => {
117        exportable!(typeschema: { $($ty)* });
118        exportable!(generic: { $($gen)* });
119    };
120
121    // --- Parse feature blocks ---
122    (@parse_features $feature:literal => { $($body:item)* }, $($rest:tt)*) => {
123        exportable!(@parse_features $feature => { $($body)* });
124        exportable!(@parse_features $($rest)*);
125    };
126    (@parse_features $feature:literal => { $($body:item)* }) => {
127        $(
128            #[cfg(feature = $feature)]
129            $body
130        )*
131    };
132    (@parse_features) => {};
133
134    // --- TypeSchema implementation parsers ---
135    // TypeSchema with identifier
136    (@parse_typeschema $ty:ty => $to:ident, $($rest:tt)*) => {
137        exportable!(@parse_typeschema $ty => {
138            TypeSchema::new(Types::$to)
139        }, $($rest)*);
140    };
141
142    (@parse_typeschema static $ty:ty => $to:expr, $($rest:tt)*) => {
143        exportable!(@parse_typeschema $ty => { $to }, $($rest)*);
144    };
145
146    // TypeSchema with block
147    (@parse_typeschema $ty:ty => $implementation:block, $($rest:tt)*) => {
148        impl Exportable for $ty {
149            fn export_internal() -> impl Serializable {
150                $implementation
151            }
152        }
153        exportable!(@parse_typeschema $($rest)*);
154    };
155
156    // Termination case for typeschema
157    (@parse_typeschema) => {};
158
159    // --- Generic implementation parsers ---
160    // Generic implementation with expression - with trait bounds
161    (@parse_impls $type:ident < $($type_param:ident $(: $trait_bound:path)?),* $(,)? > => $implementation:expr, $($rest:tt)*) => {
162        impl<$($type_param: 'static + Exportable $(+ $trait_bound)?),*> Exportable for $type<$($type_param),*> {
163            fn export_internal() -> impl Serializable {
164                $implementation
165            }
166            fn get_type_name() -> String {
167                format!(
168                    "::ronky::--virtual--::generic::{}",
169                    vec![$($type_param::get_type_name()),*].join("")
170                )
171            }
172        }
173        exportable!(@parse_impls $($rest)*);
174    };
175
176    // Generic implementation with block
177    (@parse_impls $type:ident < $($type_param:ident $(: $trait_bound:path)?),* $(,)? > => $implementation:block, $($rest:tt)*) => {
178        exportable!(@parse_impls $type < $($type_param $(: $trait_bound)?),* > => {
179            $implementation
180        }, $($rest)*);
181    };
182
183    // Termination case for generic implementations
184    (@parse_impls) => {};
185}
186
187/// A type alias for slices of a given type.
188///
189/// This alias is used to simplify the interaction with slices in the macro.
190type SliceOf<T> = [T];
191
192exportable! {
193    typeschema: {
194        static () => EmptySchema::new(),
195        char => String,
196        String => String,
197        &str => String,
198        bool => Boolean,
199        f32 => Float32,
200        f64 => Float64,
201        i8 => Int8,
202        u8 => Uint8,
203        i16 => Int16,
204        u16 => Uint16,
205        i32 => Int32,
206        u32 => Uint32,
207        i64 => Int64,
208        u64 => Uint64,
209        AtomicBool => Boolean,
210        AtomicI8 => Int8,
211        AtomicU8 => Uint8,
212        AtomicI16 => Int16,
213        AtomicU16 => Uint16,
214        AtomicI32 => Int32,
215        AtomicU32 => Uint32,
216        AtomicI64 => Int64,
217        AtomicU64 => Uint64,
218        NonZeroI8 => Int8,
219        NonZeroU8 => Uint8,
220        NonZeroI16 => Int16,
221        NonZeroU16 => Uint16,
222        NonZeroI32 => Int32,
223        NonZeroU32 => Uint32,
224        NonZeroI64 => Int64,
225        NonZeroU64 => Uint64,
226        NonZeroIsize => Int64,
227        NonZeroUsize => Uint64,
228        OsStr => String,
229        OsString => String,
230        PathBuf => String,
231        Path => String,
232        IpAddr => String,
233        Ipv4Addr => String,
234        Ipv6Addr => String,
235        SocketAddr => String,
236        Duration => Int64,
237        Instant => Int64,
238        SystemTime => Int64,
239    },
240    generic: {
241        // Ignored types
242        Option<T> => T::export(), // Option is a special case, this gets handled in the proc macro
243        Rc<T> => T::export(),
244        Arc<T> => T::export(),
245        Cell<T> => T::export(),
246        RefCell<T> => T::export(),
247        Mutex<T> => T::export(),
248        RwLock<T> => T::export(),
249        NonNull<T> => T::export(),
250
251        // General exports
252        Box<T> => T::export_with_recursion_check(),
253        Result<T, E> => {
254            let mut schema = TaggedUnionSchema::new();
255            let mut ok_props = PropertiesSchema::new();
256            let mut err_props = PropertiesSchema::new();
257
258            ok_props.set_property("value", Box::new(T::export()));
259            err_props.set_property("value", Box::new(E::export()));
260
261            schema.add_mapping("Ok", Box::new(ok_props));
262            schema.add_mapping("Err", Box::new(err_props));
263            schema
264        },
265
266        // Element Schema's
267        SliceOf<T> => ElementsSchema::new(Box::new(T::export())),
268        Vec<T> => ElementsSchema::new(Box::new(T::export())),
269        VecDeque<T> => ElementsSchema::new(Box::new(T::export())),
270        LinkedList<T> => ElementsSchema::new(Box::new(T::export())),
271        HashSet<T> => ElementsSchema::new(Box::new(T::export())),
272        BTreeSet<T> => ElementsSchema::new(Box::new(T::export())),
273        BinaryHeap<T> => ElementsSchema::new(Box::new(T::export())),
274
275        // Values Schema's
276        HashMap<K: ToString, V> => ValuesSchema::new(Box::new(V::export())),
277        BTreeMap<K: ToString, V> => ValuesSchema::new(Box::new(V::export())),
278    },
279    features: {
280        "chrono" => {
281            use chrono::{DateTime, FixedOffset, Utc, Local, NaiveTime, NaiveDate, NaiveDateTime, TimeZone};
282
283            exportable! {
284                typeschema: {
285                    DateTime<Utc> => Timestamp,
286                    DateTime<Local> => Timestamp,
287                    DateTime<FixedOffset> => Timestamp,
288                    NaiveDate => Timestamp,
289                    NaiveTime => Timestamp,
290                    NaiveDateTime => Timestamp,
291                    chrono::Duration => Int64,
292                },
293                generic: {
294                    DateTime<Tz: TimeZone> => TypeSchema::new(Types::Timestamp),
295                }
296            }
297        },
298        "time" => {
299            exportable! {
300                typeschema: {
301                    time::OffsetDateTime => Timestamp,
302                    time::PrimitiveDateTime => Timestamp,
303                    time::Date => Timestamp,
304                    time::Time => Timestamp,
305                    time::Duration => Int64,
306                }
307            }
308        },
309        "uuid" => {
310            exportable! {
311                typeschema: {
312                    uuid::Uuid => String,
313                }
314            }
315        },
316        "bigdecimal" => {
317            exportable! {
318                typeschema: {
319                    bigdecimal::BigDecimal => String,
320                }
321            }
322        },
323        "num-bigint" => {
324            exportable! {
325                typeschema: {
326                    num_bigint::BigInt => String,
327                }
328            }
329        },
330        "num-bigfloat" => {
331            exportable! {
332                typeschema: {
333                    num_bigfloat::BigFloat => String,
334                }
335            }
336        },
337        "rust_decimal" => {
338            exportable! {
339                typeschema: {
340                    rust_decimal::Decimal => String,
341                }
342            }
343        },
344        "decimal" => {
345            exportable! {
346                typeschema: {
347                    decimal::d128 => String,
348                }
349            }
350        },
351        "url" => {
352            exportable! {
353                typeschema: {
354                    url::Url => String,
355                }
356            }
357        },
358        "bytes" => {
359            exportable! {
360                typeschema: {
361                    bytes::Bytes => String,
362                    bytes::BytesMut => String,
363                }
364            }
365        },
366        "dashmap" => {
367            use dashmap::{DashMap, DashSet};
368
369            exportable! {
370                generic: {
371                    DashMap<K: ToString, V> => ValuesSchema::new(Box::new(V::export())),
372                    DashSet<T> => ElementsSchema::new(Box::new(T::export())),
373                }
374            }
375        },
376        "indexmap" => {
377            use indexmap::{IndexMap, IndexSet};
378
379            exportable! {
380                generic: {
381                    IndexMap<K: ToString, V> => ValuesSchema::new(Box::new(V::export())),
382                    IndexSet<T> => ElementsSchema::new(Box::new(T::export())),
383                }
384            }
385        },
386        "smallvec" => {
387            use smallvec::SmallVec;
388
389            exportable! {
390                generic: {
391                    SmallVec<T: smallvec::Array> => ElementsSchema::new(Box::new(T::export())),
392                }
393            }
394        }
395    }
396}