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}