vox_codegen/targets/typescript/
wire.rs1use 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
31pub struct WireType {
33 pub shape: &'static Shape,
35}
36
37pub struct WireTypeGenConfig {
38 pub types: Vec<WireType>,
39}
40
41pub 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 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 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
133fn 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
218fn 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
234fn 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}