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