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