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 `as "name"` syntax is optional; if omitted, the struct name is used
41#[macro_export]
42macro_rules! branded {
43    // Pattern with generics and optional TypeScript name
44    (
45        $(#[$attr:meta])*
46        $vis:vis struct $ident:ident<$($generic:ident),+ $(,)?> ( $ty:ty ) $(as $ts_name:literal)?
47    ) => {
48        $(#[$attr])*
49        $vis struct $ident<$($generic),+>($ty);
50
51        impl<$($generic: specta::Type),+> specta::Type for $ident<$($generic),+> {
52            fn definition(types: &mut specta::Types) -> specta::datatype::DataType {
53                let ty = <$ty as specta::Type>::definition(types);
54                let brand: &'static str = branded!(@brand $ident $( $ts_name )?);
55
56                specta::datatype::DataType::Reference(
57                    specta::datatype::Reference::opaque(
58                        $crate::Branded::new(std::borrow::Cow::Borrowed(brand), ty)
59                    )
60                )
61            }
62        }
63    };
64
65    // Pattern without generics
66    (
67        $(#[$attr:meta])*
68        $vis:vis struct $ident:ident ( $ty:ty ) $(as $ts_name:literal)?
69    ) => {
70        $(#[$attr])*
71        $vis struct $ident($ty);
72
73        impl specta::Type for $ident {
74            fn definition(types: &mut specta::Types) -> specta::datatype::DataType {
75                let ty = <$ty as specta::Type>::definition(types);
76                let brand: &'static str = branded!(@brand $ident $( $ts_name )?);
77
78                specta::datatype::DataType::Reference(
79                    specta::datatype::Reference::opaque(
80                        $crate::Branded::new(std::borrow::Cow::Borrowed(brand), ty)
81                    )
82                )
83            }
84        }
85    };
86
87    // Internal
88     (@brand $ident:ident $ts_name:literal) => {
89         $ts_name
90     };
91     (@brand $ident:ident) => {
92         stringify!($ident)
93     };
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97/// Runtime payload for a TypeScript branded type.
98pub struct Branded {
99    brand: Cow<'static, str>,
100    ty: DataType,
101}
102
103impl Branded {
104    /// Construct a branded type from a brand label and inner type.
105    pub fn new(brand: impl Into<Cow<'static, str>>, ty: DataType) -> Self {
106        Self {
107            brand: brand.into(),
108            ty,
109        }
110    }
111
112    /// Get the brand label.
113    pub fn brand(&self) -> &Cow<'static, str> {
114        &self.brand
115    }
116
117    /// Get the inner data type.
118    pub fn ty(&self) -> &DataType {
119        &self.ty
120    }
121}