gear_mesh_generator/
branded.rs

1//! Helpers for generating Branded Types.
2
3use crate::GeneratorConfig;
4use gear_mesh_core::{GearMeshType, TypeKind};
5
6/// Generator for Branded Types
7pub struct BrandedTypeGenerator {
8    config: GeneratorConfig,
9}
10
11impl BrandedTypeGenerator {
12    /// Creates a new generator.
13    pub fn new(config: GeneratorConfig) -> Self {
14        Self { config }
15    }
16
17    /// Generates helper code for Branded Types.
18    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    /// Generates code used for Branded Types from a Type.
35    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    /// Generates a Zod schema for a Branded Type.
61    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            // 内部型に応じたZodスキーマを生成
71            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}