gear_mesh_generator/
branded.rs1use crate::GeneratorConfig;
4use gear_mesh_core::{GearMeshType, TypeKind};
5
6pub struct BrandedTypeGenerator {
8 config: GeneratorConfig,
9}
10
11impl BrandedTypeGenerator {
12 pub fn new(config: GeneratorConfig) -> Self {
14 Self { config }
15 }
16
17 pub fn generate_helpers() -> String {
19 r#"// Branded Type utilities
20type Brand<T, B> = T & { readonly __brand: B };
21
22// Type guard helper
23export function isBranded<T, B extends string>(
24 value: unknown,
25 brand: B,
26 typeCheck: (v: unknown) => v is T
27): value is Brand<T, B> {
28 return typeCheck(value);
29}
30"#
31 .to_string()
32 }
33
34 pub fn generate(ty: &GearMeshType, inner_ts_type: &str) -> Option<String> {
36 if !ty.attributes.branded {
37 return None;
38 }
39
40 if let TypeKind::Newtype(_) = &ty.kind {
41 let name = &ty.name;
42 Some(format!(
43 r#"export type {name} = Brand<{inner_ts_type}, "{name}">;
44
45export const {name} = (value: {inner_ts_type}): {name} => value as {name};
46
47export function is{name}(value: unknown): value is {name} {{
48 return typeof value === '{ts_typeof}';
49}}
50"#,
51 name = name,
52 inner_ts_type = inner_ts_type,
53 ts_typeof = typescript_typeof(inner_ts_type),
54 ))
55 } else {
56 None
57 }
58 }
59
60 pub fn generate_zod_schema(&self, ty: &GearMeshType) -> Option<String> {
62 if !ty.attributes.branded {
63 return None;
64 }
65
66 if let TypeKind::Newtype(newtype) = &ty.kind {
67 let name = &ty.name;
68 let inner_type = &newtype.inner;
69
70 let base_schema = match inner_type.name.as_str() {
72 "i8" | "i16" | "i32" | "u8" | "u16" | "u32" | "f32" | "f64" => "z.number()",
73 "i64" | "i128" | "u64" | "u128" | "isize" | "usize" => {
74 if self.config.use_bigint {
75 "z.bigint()"
76 } else {
77 "z.number()"
78 }
79 }
80 "String" | "str" => "z.string()",
81 "bool" => "z.boolean()",
82 _ => "z.unknown()",
83 };
84
85 Some(format!(
86 r#"export const {}Schema = {}.brand<"{}">();
87"#,
88 name, base_schema, name
89 ))
90 } else {
91 None
92 }
93 }
94}
95
96fn typescript_typeof(ts_type: &str) -> &'static str {
97 match ts_type {
98 "number" | "bigint" => "number",
99 "string" => "string",
100 "boolean" => "boolean",
101 _ => "object",
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use gear_mesh_core::{NewtypeType, TypeAttributes, TypeRef};
109
110 #[test]
111 fn test_generate_branded() {
112 let ty = GearMeshType {
113 name: "UserId".to_string(),
114 kind: TypeKind::Newtype(NewtypeType {
115 inner: TypeRef::new("i32"),
116 }),
117 docs: None,
118 generics: vec![],
119 attributes: TypeAttributes {
120 branded: true,
121 ..Default::default()
122 },
123 };
124
125 let output = BrandedTypeGenerator::generate(&ty, "number");
126 assert!(output.is_some());
127 let code = output.unwrap();
128 assert!(code.contains("export type UserId = Brand<number, \"UserId\">"));
129 }
130
131 #[test]
132 fn test_generate_branded_zod_bigint() {
133 let ty = GearMeshType {
134 name: "BigId".to_string(),
135 kind: TypeKind::Newtype(NewtypeType {
136 inner: TypeRef::new("i64"),
137 }),
138 docs: None,
139 generics: vec![],
140 attributes: TypeAttributes {
141 branded: true,
142 ..Default::default()
143 },
144 };
145
146 let config = GeneratorConfig::new().with_bigint(true);
147 let generator = BrandedTypeGenerator::new(config);
148 let output = generator.generate_zod_schema(&ty);
149
150 assert!(output.is_some());
151 let code = output.unwrap();
152 assert!(code.contains("z.bigint().brand<\"BigId\">"));
153 }
154}