vox_codegen/targets/typescript/
wire.rs1use 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
20pub struct WireType {
22 pub shape: &'static Shape,
24}
25
26pub struct WireTypeGenConfig {
27 pub types: Vec<WireType>,
28}
29
30pub 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 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 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 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
148fn 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
249fn 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}