Skip to main content

specta_typescript/
branded.rs

1use std::borrow::Cow;
2
3use specta::datatype::DataType;
4
5/// Create a branded tuple struct type that exports to TypeScript with a custom name.
6///
7/// This macro generates a single-field tuple struct and implements the `Type` trait
8/// for it, allowing you to create "branded" types that maintain distinct identities
9/// in TypeScript.
10///
11/// # Examples
12///
13/// Basic usage:
14/// ```ignore
15/// branded!(pub struct AccountId(String));
16/// ```
17///
18/// With custom TypeScript name:
19/// ```ignore
20/// branded!(pub struct AccountId(String) as "accountId");
21/// ```
22///
23/// With attributes:
24/// ```ignore
25/// branded!(#[derive(Serialize)] pub struct UserId(String));
26/// ```
27///
28/// With generics:
29/// ```ignore
30/// branded!(pub struct Id<T>(T) as "id");
31/// ```
32///
33/// # Requirements
34///
35/// This macro requires that the `specta` crate is in scope and available as a dependency.
36///
37/// # Notes
38///
39/// - The struct must be a tuple struct with exactly one field
40/// - The `Type` implementation is currently a `todo!()` placeholder
41/// - The `as "name"` syntax is optional; if omitted, the struct name is used
42#[macro_export]
43macro_rules! branded {
44    // Pattern with generics and optional TypeScript name
45    (
46        $(#[$attr:meta])*
47        $vis:vis struct $ident:ident<$($generic:ident),+ $(,)?> ( $ty:ty ) $(as $ts_name:literal)?
48    ) => {
49        $(#[$attr])*
50        $vis struct $ident<$($generic),+>($ty);
51
52        impl<$($generic: specta::Type),+> specta::Type for $ident<$($generic),+> {
53            fn definition(types: &mut specta::TypeCollection) -> specta::datatype::DataType {
54                let ty = <$ty as specta::Type>::definition(types);
55                let brand: &'static str = branded!(@brand $ident $( $ts_name )?);
56
57                specta::datatype::DataType::Reference(
58                    specta::datatype::Reference::opaque(
59                        $crate::Branded::new(std::borrow::Cow::Borrowed(brand), ty)
60                    )
61                )
62            }
63        }
64    };
65
66    // Pattern without generics
67    (
68        $(#[$attr:meta])*
69        $vis:vis struct $ident:ident ( $ty:ty ) $(as $ts_name:literal)?
70    ) => {
71        $(#[$attr])*
72        $vis struct $ident($ty);
73
74        impl specta::Type for $ident {
75            fn definition(types: &mut specta::TypeCollection) -> specta::datatype::DataType {
76                let ty = <$ty as specta::Type>::definition(types);
77                let brand: &'static str = branded!(@brand $ident $( $ts_name )?);
78
79                specta::datatype::DataType::Reference(
80                    specta::datatype::Reference::opaque(
81                        $crate::Branded::new(std::borrow::Cow::Borrowed(brand), ty)
82                    )
83                )
84            }
85        }
86    };
87
88    // Internal
89     (@brand $ident:ident $ts_name:literal) => {
90         $ts_name
91     };
92     (@brand $ident:ident) => {
93         stringify!($ident)
94     };
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98/// Runtime payload for a TypeScript branded type.
99pub struct Branded {
100    brand: Cow<'static, str>,
101    ty: DataType,
102}
103
104impl Branded {
105    /// Construct a branded type from a brand label and inner type.
106    pub fn new(brand: impl Into<Cow<'static, str>>, ty: DataType) -> Self {
107        Self {
108            brand: brand.into(),
109            ty,
110        }
111    }
112
113    /// Get the brand label.
114    pub fn brand(&self) -> &Cow<'static, str> {
115        &self.brand
116    }
117
118    /// Get the inner data type.
119    pub fn ty(&self) -> &DataType {
120        &self.ty
121    }
122}