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        }) => {
180            if !seen.contains(name) {
181                seen.insert(name.to_string());
182                for field in fields {
183                    visit(field.shape(), seen, types);
184                }
185                types.push((name.to_string(), shape));
186            }
187        }
188        ShapeKind::Enum(EnumInfo {
189            name: Some(name),
190            variants,
191        }) => {
192            if !seen.contains(name) {
193                seen.insert(name.to_string());
194                for variant in variants {
195                    match classify_variant(variant) {
196                        VariantKind::Newtype { inner } => visit(inner, seen, types),
197                        VariantKind::Struct { fields } | VariantKind::Tuple { fields } => {
198                            for field in fields {
199                                visit(field.shape(), seen, types);
200                            }
201                        }
202                        VariantKind::Unit => {}
203                    }
204                }
205                types.push((name.to_string(), shape));
206            }
207        }
208        ShapeKind::List { element } => visit(element, seen, types),
209        ShapeKind::Option { inner } => visit(inner, seen, types),
210        ShapeKind::Array { element, .. } => visit(element, seen, types),
211        ShapeKind::Map { key, value } => {
212            visit(key, seen, types);
213            visit(value, seen, types);
214        }
215        ShapeKind::Set { element } => visit(element, seen, types),
216        ShapeKind::Tuple { elements } => {
217            for param in elements {
218                visit(param.shape, seen, types);
219            }
220        }
221        ShapeKind::Pointer { pointee } => visit(pointee, seen, types),
222        ShapeKind::Result { ok, err } => {
223            visit(ok, seen, types);
224            visit(err, seen, types);
225        }
226        _ => {}
227    }
228}
229
230fn transparent_named_alias(shape: &'static Shape) -> Option<(&'static str, &'static Shape)> {
231    if !shape.is_transparent() {
232        return None;
233    }
234    let name = extract_type_name(shape.type_identifier)?;
235    let inner = shape.inner?;
236    Some((name, inner))
237}
238
239fn extract_type_name(type_identifier: &'static str) -> Option<&'static str> {
240    if type_identifier.is_empty()
241        || type_identifier.starts_with('(')
242        || type_identifier.starts_with('[')
243    {
244        return None;
245    }
246    Some(type_identifier)
247}
248
249/// Convert a Shape to a TypeScript type string for wire protocol types.
250fn wire_ts_type(shape: &'static Shape) -> String {
251    if let Some((name, _)) = transparent_named_alias(shape) {
252        return name.to_string();
253    }
254
255    match classify_shape(shape) {
256        ShapeKind::Struct(StructInfo {
257            name: Some(name), ..
258        }) => name.to_string(),
259        ShapeKind::Enum(EnumInfo {
260            name: Some(name), ..
261        }) => name.to_string(),
262
263        ShapeKind::List { .. } if is_bytes(shape) => "Uint8Array".into(),
264        ShapeKind::List { element } => {
265            if matches!(
266                classify_shape(element),
267                ShapeKind::Enum(EnumInfo { name: None, .. })
268            ) {
269                format!("({})[]", wire_ts_type(element))
270            } else {
271                format!("{}[]", wire_ts_type(element))
272            }
273        }
274        ShapeKind::Option { inner } => format!("{} | null", wire_ts_type(inner)),
275        ShapeKind::Scalar(scalar) => wire_ts_scalar_type(scalar),
276        ShapeKind::Slice { .. } if is_bytes(shape) => "Uint8Array".into(),
277        ShapeKind::Slice { element } => format!("{}[]", wire_ts_type(element)),
278        ShapeKind::Pointer { pointee } if is_bytes(pointee) => "Uint8Array".into(),
279        ShapeKind::Pointer { pointee } => wire_ts_type(pointee),
280        ShapeKind::Opaque => "Uint8Array".into(),
281
282        ShapeKind::Struct(StructInfo {
283            name: None, fields, ..
284        }) => {
285            let inner = fields
286                .iter()
287                .map(|f| format!("{}: {}", f.name, wire_ts_type(f.shape())))
288                .collect::<Vec<_>>()
289                .join("; ");
290            format!("{{ {inner} }}")
291        }
292        ShapeKind::Enum(EnumInfo {
293            name: None,
294            variants,
295        }) => variants
296            .iter()
297            .map(|v| match classify_variant(v) {
298                VariantKind::Unit => format!("{{ tag: \"{}\" }}", v.name),
299                VariantKind::Newtype { inner } => {
300                    format!("{{ tag: \"{}\"; value: {} }}", v.name, wire_ts_type(inner))
301                }
302                VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
303                    let field_strs = fields
304                        .iter()
305                        .map(|f| format!("{}: {}", f.name, wire_ts_type(f.shape())))
306                        .collect::<Vec<_>>()
307                        .join("; ");
308                    format!("{{ tag: \"{}\"; {} }}", v.name, field_strs)
309                }
310            })
311            .collect::<Vec<_>>()
312            .join(" | "),
313
314        ShapeKind::Tuple { elements } => {
315            let inner = elements
316                .iter()
317                .map(|p| wire_ts_type(p.shape))
318                .collect::<Vec<_>>()
319                .join(", ");
320            format!("[{inner}]")
321        }
322        ShapeKind::Map { key, value } => {
323            format!("Map<{}, {}>", wire_ts_type(key), wire_ts_type(value))
324        }
325        ShapeKind::Set { element } => format!("Set<{}>", wire_ts_type(element)),
326        ShapeKind::Array { element, len } => format!("[{}; {}]", wire_ts_type(element), len),
327
328        _ => "unknown".into(),
329    }
330}
331
332fn wire_ts_scalar_type(scalar: ScalarType) -> String {
333    match scalar {
334        ScalarType::Bool => "boolean".into(),
335        ScalarType::U8
336        | ScalarType::U16
337        | ScalarType::U32
338        | ScalarType::I8
339        | ScalarType::I16
340        | ScalarType::I32
341        | ScalarType::F32
342        | ScalarType::F64 => "number".into(),
343        ScalarType::U64
344        | ScalarType::U128
345        | ScalarType::I64
346        | ScalarType::I128
347        | ScalarType::USize
348        | ScalarType::ISize => "bigint".into(),
349        ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
350            "string".into()
351        }
352        ScalarType::Unit => "void".into(),
353        _ => "unknown".into(),
354    }
355}