vox_codegen/targets/typescript/
schema.rs1use facet_core::{Facet, Shape};
8use heck::ToLowerCamelCase;
9use vox_types::{
10 Schema, SchemaHash, SchemaKind, ServiceDescriptor, ShapeKind, TypeRef, VariantPayload,
11 VariantSchema, VoxError, classify_shape,
12};
13
14pub fn generate_send_schema_table(service: &ServiceDescriptor) -> String {
23 use crate::render::hex_u64;
24
25 let service_name_lower = service.service_name.to_lower_camel_case();
26
27 let mut schema_ids_seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
28
29 let mut all_schemas: Vec<Schema> = Vec::new();
31
32 fn extract_into(shape: &'static Shape, all_schemas: &mut Vec<Schema>) -> TypeRef<SchemaHash> {
34 let extracted = vox_types::extract_schemas(shape).expect("schema extraction");
35 let root = extracted.root.clone();
36 all_schemas.extend(extracted.schemas.iter().cloned());
37 root
38 }
39
40 fn type_id_of(type_ref: &TypeRef<SchemaHash>) -> SchemaHash {
41 match type_ref {
42 TypeRef::Concrete { type_id, .. } => *type_id,
43 TypeRef::Var { .. } => panic!("schema root cannot be a type variable"),
44 }
45 }
46
47 struct MethodSchemaInfo {
49 method_id: u64,
50 args_root: TypeRef<SchemaHash>,
51 response_root: TypeRef<SchemaHash>,
52 }
53
54 let mut method_schema_infos: Vec<MethodSchemaInfo> = Vec::new();
55
56 let result_template_root = extract_into(
57 <Result<bool, u32> as Facet<'static>>::SHAPE,
58 &mut all_schemas,
59 );
60 let result_type_id = type_id_of(&result_template_root);
61 let vox_error_template_root = extract_into(
62 <VoxError<std::convert::Infallible> as Facet<'static>>::SHAPE,
63 &mut all_schemas,
64 );
65 let vox_error_type_id = type_id_of(&vox_error_template_root);
66
67 for method in service.methods {
68 let method_id = crate::method_id(method);
69
70 let args_root = extract_into(method.args_shape, &mut all_schemas);
73
74 let (ok_ref, err_ref) = match classify_shape(method.return_shape) {
77 ShapeKind::Result { ok, err } => (
78 extract_into(ok, &mut all_schemas),
79 extract_into(err, &mut all_schemas),
80 ),
81 _ => {
82 let ok = extract_into(method.return_shape, &mut all_schemas);
83 let err = extract_into(
84 <std::convert::Infallible as Facet<'static>>::SHAPE,
85 &mut all_schemas,
86 );
87 (ok, err)
88 }
89 };
90
91 let vox_error_ref = TypeRef::generic(vox_error_type_id, vec![err_ref]);
92
93 method_schema_infos.push(MethodSchemaInfo {
94 method_id,
95 args_root,
96 response_root: TypeRef::generic(result_type_id, vec![ok_ref, vox_error_ref]),
97 });
98 }
99
100 let mut deduped_schemas: Vec<&Schema> = Vec::new();
102 for schema in &all_schemas {
103 let id = schema.id.0;
104 if schema_ids_seen.insert(id) {
105 deduped_schemas.push(schema);
106 }
107 }
108
109 let mut out = String::new();
111
112 out.push_str("// Schema objects for wire schema exchange (TypeScript \u{2192} Rust)\n");
113 out.push_str("// Generated from Rust Facet shapes \u{2014} do not modify.\n");
114 out.push_str(&format!(
115 "export const {service_name_lower}_send_schemas: import(\"@bearcove/vox-core\").ServiceSendSchemas = {{\n"
116 ));
117
118 out.push_str(" schemas: new Map<bigint, import(\"@bearcove/vox-postcard\").Schema>([\n");
120 for schema in &deduped_schemas {
121 let id_hex = hex_u64(schema.id.0);
122 let schema_ts = render_schema(schema);
123 out.push_str(&format!(" [{id_hex}n, {schema_ts}],\n"));
124 }
125 out.push_str(" ]),\n");
126
127 out.push_str(
129 " methods: new Map<bigint, import(\"@bearcove/vox-core\").MethodSendSchemas>([\n",
130 );
131 for info in &method_schema_infos {
132 let id_hex = hex_u64(info.method_id);
133 let args_root_ref_ts = render_type_ref(&info.args_root);
134 let response_root_ref_ts = render_type_ref(&info.response_root);
135 out.push_str(&format!(
136 " [{}n, {{ argsRootRef: {}, responseRootRef: {} }}],\n",
137 id_hex, args_root_ref_ts, response_root_ref_ts,
138 ));
139 }
140 out.push_str(" ]),\n");
141 out.push_str("};\n\n");
142 out
143}
144
145pub fn generate_descriptor(service: &ServiceDescriptor) -> String {
150 use crate::render::hex_u64;
151
152 let mut out = String::new();
153 let service_name_lower = service.service_name.to_lower_camel_case();
154
155 for method in service.methods {
156 let method_name = method.method_name.to_lower_camel_case();
157 let id = crate::method_id(method);
158 let method_descriptor_name = format!("{service_name_lower}_{method_name}_method");
159
160 out.push_str(&format!(
161 "export const {method_descriptor_name}: MethodDescriptor = {{\n"
162 ));
163 out.push_str(&format!(" name: '{method_name}',\n"));
164 out.push_str(&format!(" id: {}n,\n", hex_u64(id)));
165 out.push_str("};\n\n");
166 }
167
168 out.push_str("// Service descriptor for runtime dispatch metadata\n");
169 out.push_str(&format!(
170 "export const {service_name_lower}_descriptor: ServiceDescriptor = {{\n"
171 ));
172 out.push_str(&format!(" service_name: '{}',\n", service.service_name));
173 out.push_str(&format!(" send_schemas: {service_name_lower}Methods,\n"));
174 out.push_str(&format!(" registry: {service_name_lower}Registry,\n"));
175 out.push_str(" methods: new Map<bigint, MethodDescriptor>([\n");
176
177 for method in service.methods {
178 let method_name = method.method_name.to_lower_camel_case();
179 let method_descriptor_name = format!("{service_name_lower}_{method_name}_method");
180 out.push_str(&format!(
181 " [{method_descriptor_name}.id, {method_descriptor_name}],\n"
182 ));
183 }
184
185 out.push_str(" ]),\n");
186 out.push_str("};\n\n");
187 out
188}
189
190use vox_types::{ChannelDirection, FieldSchema, PrimitiveType};
195
196pub(crate) fn render_schema(schema: &Schema) -> String {
197 use crate::render::hex_u64;
198
199 let id_hex = hex_u64(schema.id.0);
200 let type_params = if schema.type_params.is_empty() {
201 "[]".to_string()
202 } else {
203 let params: Vec<String> = schema
204 .type_params
205 .iter()
206 .map(|p| format!("'{}'", p.as_str()))
207 .collect();
208 format!("[{}]", params.join(", "))
209 };
210 let kind = render_schema_kind(&schema.kind);
211 format!("{{ id: {id_hex}n, type_params: {type_params}, kind: {kind} }}")
212}
213
214fn render_schema_kind(kind: &SchemaKind) -> String {
215 match kind {
216 SchemaKind::Struct { name, fields } => {
217 let fields_ts: Vec<String> = fields.iter().map(render_field_schema).collect();
218 format!(
219 "{{ tag: 'struct', name: '{}', fields: [{}] }}",
220 name,
221 fields_ts.join(", ")
222 )
223 }
224 SchemaKind::Enum { name, variants } => {
225 let variants_ts: Vec<String> = variants.iter().map(render_variant_schema).collect();
226 format!(
227 "{{ tag: 'enum', name: '{}', variants: [{}] }}",
228 name,
229 variants_ts.join(", ")
230 )
231 }
232 SchemaKind::Tuple { elements } => {
233 let elems: Vec<String> = elements.iter().map(render_type_ref).collect();
234 format!("{{ tag: 'tuple', elements: [{}] }}", elems.join(", "))
235 }
236 SchemaKind::List { element } => {
237 format!("{{ tag: 'list', element: {} }}", render_type_ref(element))
238 }
239 SchemaKind::Map { key, value } => {
240 format!(
241 "{{ tag: 'map', key: {}, value: {} }}",
242 render_type_ref(key),
243 render_type_ref(value)
244 )
245 }
246 SchemaKind::Array { element, length } => {
247 format!(
248 "{{ tag: 'array', element: {}, length: {} }}",
249 render_type_ref(element),
250 length
251 )
252 }
253 SchemaKind::Option { element } => {
254 format!("{{ tag: 'option', element: {} }}", render_type_ref(element))
255 }
256 SchemaKind::Channel { direction, element } => {
257 let dir = match direction {
258 ChannelDirection::Tx => "tx",
259 ChannelDirection::Rx => "rx",
260 };
261 format!(
262 "{{ tag: 'channel', direction: '{}', element: {} }}",
263 dir,
264 render_type_ref(element)
265 )
266 }
267 SchemaKind::Primitive { primitive_type } => {
268 format!(
269 "{{ tag: 'primitive', primitive_type: '{}' }}",
270 render_primitive_type(primitive_type)
271 )
272 }
273 }
274}
275
276pub(crate) fn render_type_ref(type_ref: &TypeRef) -> String {
277 use crate::render::hex_u64;
278
279 match type_ref {
280 TypeRef::Concrete { type_id, args } => {
281 let id_hex = hex_u64(type_id.0);
282 let args_ts: Vec<String> = args.iter().map(render_type_ref).collect();
283 format!(
284 "{{ tag: 'concrete', type_id: {id_hex}n, args: [{}] }}",
285 args_ts.join(", ")
286 )
287 }
288 TypeRef::Var { name } => {
289 format!("{{ tag: 'var', name: '{}' }}", name.as_str())
290 }
291 }
292}
293
294fn render_field_schema(field: &FieldSchema) -> String {
295 format!(
296 "{{ name: '{}', type_ref: {}, required: {} }}",
297 field.name,
298 render_type_ref(&field.type_ref),
299 field.required
300 )
301}
302
303fn render_variant_schema(variant: &VariantSchema) -> String {
304 format!(
305 "{{ name: '{}', index: {}, payload: {} }}",
306 variant.name,
307 variant.index,
308 render_variant_payload(&variant.payload)
309 )
310}
311
312fn render_variant_payload(payload: &VariantPayload) -> String {
313 match payload {
314 VariantPayload::Unit => "{ tag: 'unit' }".to_string(),
315 VariantPayload::Newtype { type_ref } => {
316 format!(
317 "{{ tag: 'newtype', type_ref: {} }}",
318 render_type_ref(type_ref)
319 )
320 }
321 VariantPayload::Tuple { types } => {
322 let types_ts: Vec<String> = types.iter().map(render_type_ref).collect();
323 format!("{{ tag: 'tuple', types: [{}] }}", types_ts.join(", "))
324 }
325 VariantPayload::Struct { fields } => {
326 let fields_ts: Vec<String> = fields.iter().map(render_field_schema).collect();
327 format!("{{ tag: 'struct', fields: [{}] }}", fields_ts.join(", "))
328 }
329 }
330}
331
332fn render_primitive_type(pt: &PrimitiveType) -> &'static str {
333 match pt {
334 PrimitiveType::Bool => "bool",
335 PrimitiveType::U8 => "u8",
336 PrimitiveType::U16 => "u16",
337 PrimitiveType::U32 => "u32",
338 PrimitiveType::U64 => "u64",
339 PrimitiveType::U128 => "u128",
340 PrimitiveType::I8 => "i8",
341 PrimitiveType::I16 => "i16",
342 PrimitiveType::I32 => "i32",
343 PrimitiveType::I64 => "i64",
344 PrimitiveType::I128 => "i128",
345 PrimitiveType::F32 => "f32",
346 PrimitiveType::F64 => "f64",
347 PrimitiveType::Char => "char",
348 PrimitiveType::String => "string",
349 PrimitiveType::Unit => "unit",
350 PrimitiveType::Never => "never",
351 PrimitiveType::Bytes => "bytes",
352 PrimitiveType::Payload => "payload",
353 }
354}