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 }) 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
243fn 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}