Skip to main content

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