Skip to main content

vox_codegen/targets/typescript/
wire.rs

1//! Generate TypeScript wire protocol definitions.
2//!
3//! Walks Facet shapes and emits:
4//! - interfaces and tagged unions for all named types
5//! - discriminant constants for all named enums (auto-derived)
6//! - narrowed per-variant type aliases for all named enums (auto-derived)
7
8use std::collections::HashSet;
9
10use facet_core::{Def, ScalarType, Shape, StructKind, Type, UserType};
11use vox_types::{
12    EnumInfo, ShapeKind, StructInfo, VariantKind, classify_shape, classify_variant, is_bytes,
13};
14
15fn enum_discriminator_field(variants: &[facet_core::Variant]) -> &'static str {
16    if variants
17        .iter()
18        .any(|variant| match classify_variant(variant) {
19            VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
20                fields.iter().any(|field| field.name == "tag")
21            }
22            VariantKind::Unit | VariantKind::Newtype { .. } => false,
23        })
24    {
25        "$tag"
26    } else {
27        "tag"
28    }
29}
30
31/// A wire type to generate TypeScript definitions for.
32pub struct WireType {
33    /// The facet Shape to generate from.
34    pub shape: &'static Shape,
35}
36
37pub struct WireTypeGenConfig {
38    pub types: Vec<WireType>,
39}
40
41/// Generate a complete TypeScript module with wire protocol type definitions.
42pub fn generate_wire(config: &WireTypeGenConfig) -> Result<String, Box<dyn std::error::Error>> {
43    let mut out = String::new();
44    out.push_str("// @generated by cargo xtask codegen --typescript\n");
45    out.push_str("// DO NOT EDIT — regenerate with `cargo xtask codegen --typescript`\n\n");
46
47    let named_types = collect_wire_named_types(&config.types);
48
49    for (name, shape) in &named_types {
50        if let Some((_, inner)) = transparent_named_alias(shape) {
51            out.push_str(&format!(
52                "export type {name} = {};\n\n",
53                wire_ts_type(inner)
54            ));
55            continue;
56        }
57
58        match classify_shape(shape) {
59            ShapeKind::Struct(StructInfo { fields, .. }) => {
60                out.push_str(&format!("export interface {name} {{\n"));
61                for field in fields {
62                    out.push_str(&format!(
63                        "  {}: {};\n",
64                        field.name,
65                        wire_ts_type(field.shape())
66                    ));
67                }
68                out.push_str("}\n\n");
69            }
70            ShapeKind::Enum(EnumInfo { variants, .. }) => {
71                let discriminator = enum_discriminator_field(variants);
72                out.push_str(&format!("export type {name} =\n"));
73                for (i, variant) in variants.iter().enumerate() {
74                    let variant_type = match classify_variant(variant) {
75                        VariantKind::Unit => {
76                            format!("{{ {discriminator}: \"{}\" }}", variant.name)
77                        }
78                        VariantKind::Newtype { inner } => {
79                            format!(
80                                "{{ {discriminator}: \"{}\"; value: {} }}",
81                                variant.name,
82                                wire_ts_type(inner)
83                            )
84                        }
85                        VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
86                            let field_strs = fields
87                                .iter()
88                                .map(|f| format!("{}: {}", f.name, wire_ts_type(f.shape())))
89                                .collect::<Vec<_>>()
90                                .join("; ");
91                            format!(
92                                "{{ {discriminator}: \"{}\"; {} }}",
93                                variant.name, field_strs
94                            )
95                        }
96                    };
97                    let sep = if i < variants.len() - 1 { "" } else { ";" };
98                    out.push_str(&format!("  | {variant_type}{sep}\n"));
99                }
100                out.push('\n');
101            }
102            _ => {}
103        }
104    }
105
106    // Auto-generate discriminant constants for every named enum
107    for (name, shape) in &named_types {
108        if let ShapeKind::Enum(EnumInfo { variants, .. }) = classify_shape(shape) {
109            out.push_str(&format!("export const {name}Discriminant = {{\n"));
110            for (i, variant) in variants.iter().enumerate() {
111                out.push_str(&format!("  {}: {},\n", variant.name, i));
112            }
113            out.push_str("} as const;\n\n");
114        }
115    }
116
117    // Auto-generate narrowed per-variant type aliases for every named enum
118    for (name, shape) in &named_types {
119        if let ShapeKind::Enum(EnumInfo { variants, .. }) = classify_shape(shape) {
120            for variant in variants {
121                out.push_str(&format!(
122                    "export type {}{} = Extract<{}, {{ tag: \"{}\" }}>;\n",
123                    name, variant.name, name, variant.name,
124                ));
125            }
126            out.push('\n');
127        }
128    }
129
130    Ok(out)
131}
132
133/// Collect named types from wire type shapes in dependency order.
134fn collect_wire_named_types(types: &[WireType]) -> Vec<(String, &'static Shape)> {
135    let mut seen = HashSet::new();
136    let mut result = Vec::new();
137
138    for wire_type in types {
139        visit(wire_type.shape, &mut seen, &mut result);
140    }
141
142    result
143}
144
145fn visit(
146    shape: &'static Shape,
147    seen: &mut HashSet<String>,
148    types: &mut Vec<(String, &'static Shape)>,
149) {
150    if let Some((name, inner)) = transparent_named_alias(shape) {
151        if !seen.contains(name) {
152            seen.insert(name.to_string());
153            visit(inner, seen, types);
154            types.push((name.to_string(), shape));
155        }
156        return;
157    }
158
159    match classify_shape(shape) {
160        ShapeKind::Struct(StructInfo {
161            name: Some(name),
162            fields,
163            ..
164        }) if seen.insert(name.to_string()) => {
165            for field in fields {
166                visit(field.shape(), seen, types);
167            }
168            types.push((name.to_string(), shape));
169        }
170        ShapeKind::Enum(EnumInfo {
171            name: Some(name),
172            variants,
173        }) if seen.insert(name.to_string()) => {
174            for variant in variants {
175                match classify_variant(variant) {
176                    VariantKind::Newtype { inner } => visit(inner, seen, types),
177                    VariantKind::Struct { fields } | VariantKind::Tuple { fields } => {
178                        for field in fields {
179                            visit(field.shape(), seen, types);
180                        }
181                    }
182                    VariantKind::Unit => {}
183                }
184            }
185            types.push((name.to_string(), shape));
186        }
187        ShapeKind::List { element } => visit(element, seen, types),
188        ShapeKind::Option { inner } => visit(inner, seen, types),
189        ShapeKind::Array { element, .. } => visit(element, seen, types),
190        ShapeKind::Map { key, value } => {
191            visit(key, seen, types);
192            visit(value, seen, types);
193        }
194        ShapeKind::Set { element } => visit(element, seen, types),
195        ShapeKind::Tuple { elements } => {
196            for param in elements {
197                visit(param.shape, seen, types);
198            }
199        }
200        ShapeKind::Pointer { pointee } => visit(pointee, seen, types),
201        ShapeKind::Result { ok, err } => {
202            visit(ok, seen, types);
203            visit(err, seen, types);
204        }
205        _ => {}
206    }
207}
208
209fn transparent_named_alias(shape: &'static Shape) -> Option<(&'static str, &'static Shape)> {
210    if !shape.is_transparent() {
211        return None;
212    }
213    let name = extract_type_name(shape)?;
214    let inner = shape.inner?;
215    Some((name, inner))
216}
217
218/// The declared name of a shape, or `None` for structurally anonymous types
219/// (tuples, arrays, slices) which have no name to give.
220///
221/// This used to test the *name* for a leading `(` or `[`, which leans on how an
222/// anonymous type's identifier happens to be synthesised. `Type`/`Def` say it
223/// outright.
224fn extract_type_name(shape: &'static Shape) -> Option<&'static str> {
225    let anonymous = matches!(shape.def, Def::Array(_) | Def::Slice(_))
226        || matches!(shape.ty, Type::User(UserType::Struct(s)) if s.kind == StructKind::Tuple);
227
228    if anonymous || shape.type_identifier.is_empty() {
229        return None;
230    }
231    Some(shape.type_identifier)
232}
233
234/// Convert a Shape to a TypeScript type string for wire protocol types.
235fn wire_ts_type(shape: &'static Shape) -> String {
236    if let Some((name, _)) = transparent_named_alias(shape) {
237        return name.to_string();
238    }
239
240    match classify_shape(shape) {
241        ShapeKind::Struct(StructInfo {
242            name: Some(name), ..
243        }) => name.to_string(),
244        ShapeKind::Enum(EnumInfo {
245            name: Some(name), ..
246        }) => name.to_string(),
247
248        ShapeKind::List { .. } if is_bytes(shape) => "Uint8Array".into(),
249        ShapeKind::List { element } => {
250            if matches!(
251                classify_shape(element),
252                ShapeKind::Enum(EnumInfo { name: None, .. })
253            ) {
254                format!("({})[]", wire_ts_type(element))
255            } else {
256                format!("{}[]", wire_ts_type(element))
257            }
258        }
259        ShapeKind::Option { inner } => format!("{} | null", wire_ts_type(inner)),
260        ShapeKind::Scalar(scalar) => wire_ts_scalar_type(scalar),
261        ShapeKind::Slice { .. } if is_bytes(shape) => "Uint8Array".into(),
262        ShapeKind::Slice { element } => format!("{}[]", wire_ts_type(element)),
263        ShapeKind::Pointer { pointee } if is_bytes(pointee) => "Uint8Array".into(),
264        ShapeKind::Pointer { pointee } => wire_ts_type(pointee),
265        ShapeKind::Opaque => "Uint8Array".into(),
266
267        ShapeKind::Struct(StructInfo {
268            name: None, fields, ..
269        }) => {
270            let inner = fields
271                .iter()
272                .map(|f| format!("{}: {}", f.name, wire_ts_type(f.shape())))
273                .collect::<Vec<_>>()
274                .join("; ");
275            format!("{{ {inner} }}")
276        }
277        ShapeKind::Enum(EnumInfo {
278            name: None,
279            variants,
280        }) => {
281            let discriminator = enum_discriminator_field(variants);
282            variants
283                .iter()
284                .map(|v| match classify_variant(v) {
285                    VariantKind::Unit => format!("{{ {discriminator}: \"{}\" }}", v.name),
286                    VariantKind::Newtype { inner } => {
287                        format!(
288                            "{{ {discriminator}: \"{}\"; value: {} }}",
289                            v.name,
290                            wire_ts_type(inner)
291                        )
292                    }
293                    VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
294                        let field_strs = fields
295                            .iter()
296                            .map(|f| format!("{}: {}", f.name, wire_ts_type(f.shape())))
297                            .collect::<Vec<_>>()
298                            .join("; ");
299                        format!("{{ {discriminator}: \"{}\"; {} }}", v.name, field_strs)
300                    }
301                })
302                .collect::<Vec<_>>()
303                .join(" | ")
304        }
305
306        ShapeKind::Tuple { elements } => {
307            let inner = elements
308                .iter()
309                .map(|p| wire_ts_type(p.shape))
310                .collect::<Vec<_>>()
311                .join(", ");
312            format!("[{inner}]")
313        }
314        ShapeKind::Map { key, value } => {
315            format!("Map<{}, {}>", wire_ts_type(key), wire_ts_type(value))
316        }
317        ShapeKind::Set { element } => format!("Set<{}>", wire_ts_type(element)),
318        ShapeKind::Array { element, len } => format!("[{}; {}]", wire_ts_type(element), len),
319
320        _ => "unknown".into(),
321    }
322}
323
324fn wire_ts_scalar_type(scalar: ScalarType) -> String {
325    match scalar {
326        ScalarType::Bool => "boolean".into(),
327        ScalarType::U8
328        | ScalarType::U16
329        | ScalarType::U32
330        | ScalarType::I8
331        | ScalarType::I16
332        | ScalarType::I32
333        | ScalarType::F32
334        | ScalarType::F64 => "number".into(),
335        ScalarType::U64
336        | ScalarType::U128
337        | ScalarType::I64
338        | ScalarType::I128
339        | ScalarType::USize
340        | ScalarType::ISize => "bigint".into(),
341        ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
342            "string".into()
343        }
344        ScalarType::Unit => "void".into(),
345        _ => "unknown".into(),
346    }
347}