1use std::collections::HashSet;
18
19use facet_core::{Field, ScalarType, Shape};
20use heck::ToLowerCamelCase;
21use roam_types::{
22 EnumInfo, ServiceDescriptor, ShapeKind, StructInfo, VariantKind, classify_shape,
23 classify_variant, is_bytes,
24};
25
26pub fn generate_schema(shape: &'static Shape) -> String {
28 let mut state = SchemaGenState::default();
29 generate_schema_with_field(shape, None, &mut state)
30}
31
32#[derive(Default)]
33struct SchemaGenState {
34 use_named_refs: bool,
37 root_name: Option<String>,
39 active: HashSet<usize>,
41}
42
43fn named_shape_name(shape: &'static Shape) -> Option<&'static str> {
44 match classify_shape(shape) {
45 ShapeKind::Struct(StructInfo {
46 name: Some(name), ..
47 })
48 | ShapeKind::Enum(EnumInfo {
49 name: Some(name), ..
50 }) => Some(name),
51 _ => None,
52 }
53}
54
55fn extract_initial_credit(shape: &'static Shape) -> u32 {
56 shape
57 .const_params
58 .iter()
59 .find(|cp| cp.name == "N")
60 .map(|cp| cp.value as u32)
61 .unwrap_or(16)
62}
63
64fn generate_schema_with_field(
65 shape: &'static Shape,
66 field: Option<&Field>,
67 state: &mut SchemaGenState,
68) -> String {
69 if state.use_named_refs
70 && let Some(name) = named_shape_name(shape)
71 && state.root_name.as_deref() != Some(name)
72 {
73 return format!("{{ kind: 'ref', name: '{name}' }}");
74 }
75
76 let shape_ptr = shape as *const Shape as usize;
77 if !state.active.insert(shape_ptr) {
78 if let Some(name) = named_shape_name(shape) {
79 return format!("{{ kind: 'ref', name: '{name}' }}");
80 }
81 panic!(
82 "encountered recursive anonymous shape in TypeScript schema generation; \
83 recursive shapes must be named to generate refs"
84 );
85 }
86
87 let bytes_schema = if field.is_some_and(|f| f.has_builtin_attr("trailing")) {
88 "{ kind: 'bytes', trailing: true }"
89 } else {
90 "{ kind: 'bytes' }"
91 };
92
93 if is_bytes(shape) {
95 state.active.remove(&shape_ptr);
96 return bytes_schema.into();
97 }
98
99 let rendered = match classify_shape(shape) {
100 ShapeKind::Scalar(scalar) => generate_scalar_schema(scalar),
101 ShapeKind::Tx { inner } => {
102 format!(
103 "{{ kind: 'tx', initial_credit: {}, element: {} }}",
104 extract_initial_credit(shape),
105 generate_schema_with_field(inner, None, state)
106 )
107 }
108 ShapeKind::Rx { inner } => {
109 format!(
110 "{{ kind: 'rx', initial_credit: {}, element: {} }}",
111 extract_initial_credit(shape),
112 generate_schema_with_field(inner, None, state)
113 )
114 }
115 ShapeKind::List { element } => {
116 format!(
117 "{{ kind: 'vec', element: {} }}",
118 generate_schema_with_field(element, None, state)
119 )
120 }
121 ShapeKind::Option { inner } => {
122 format!(
123 "{{ kind: 'option', inner: {} }}",
124 generate_schema_with_field(inner, None, state)
125 )
126 }
127 ShapeKind::Array { element, .. } | ShapeKind::Slice { element } => {
128 format!(
129 "{{ kind: 'vec', element: {} }}",
130 generate_schema_with_field(element, None, state)
131 )
132 }
133 ShapeKind::Map { key, value } => {
134 format!(
135 "{{ kind: 'map', key: {}, value: {} }}",
136 generate_schema_with_field(key, None, state),
137 generate_schema_with_field(value, None, state)
138 )
139 }
140 ShapeKind::Set { element } => {
141 format!(
142 "{{ kind: 'vec', element: {} }}",
143 generate_schema_with_field(element, None, state)
144 )
145 }
146 ShapeKind::Tuple { elements } => {
147 let element_schemas: Vec<_> = elements
148 .iter()
149 .map(|p| generate_schema_with_field(p.shape, None, state))
150 .collect();
151 format!(
152 "{{ kind: 'tuple', elements: [{}] }}",
153 element_schemas.join(", ")
154 )
155 }
156 ShapeKind::Struct(StructInfo { fields, .. }) => {
157 let field_schemas: Vec<_> = fields
158 .iter()
159 .map(|f| {
160 format!(
161 "'{}': {}",
162 f.name,
163 generate_schema_with_field(f.shape(), Some(f), state)
164 )
165 })
166 .collect();
167 format!(
168 "{{ kind: 'struct', fields: {{ {} }} }}",
169 field_schemas.join(", ")
170 )
171 }
172 ShapeKind::Enum(EnumInfo { variants, .. }) => {
173 let variant_schemas: Vec<_> = variants
174 .iter()
175 .map(|variant| generate_enum_variant(variant, state))
176 .collect();
177 format!(
178 "{{ kind: 'enum', variants: [{}] }}",
179 variant_schemas.join(", ")
180 )
181 }
182 ShapeKind::Pointer { pointee } => generate_schema_with_field(pointee, None, state),
183 ShapeKind::Result { ok, err } => {
184 format!(
186 "{{ kind: 'enum', variants: [{{ name: 'Ok', fields: {} }}, {{ name: 'Err', fields: {} }}] }}",
187 generate_schema_with_field(ok, None, state),
188 generate_schema_with_field(err, None, state)
189 )
190 }
191 ShapeKind::TupleStruct { fields } => {
192 let inner: Vec<String> = fields
193 .iter()
194 .map(|f| generate_schema_with_field(f.shape(), Some(f), state))
195 .collect();
196 format!("{{ kind: 'tuple', elements: [{}] }}", inner.join(", "))
197 }
198 ShapeKind::Opaque => bytes_schema.into(),
199 };
200
201 state.active.remove(&shape_ptr);
202 rendered
203}
204
205fn generate_enum_variant(variant: &facet_core::Variant, state: &mut SchemaGenState) -> String {
207 match classify_variant(variant) {
208 VariantKind::Unit => {
209 format!("{{ name: '{}', fields: null }}", variant.name)
210 }
211 VariantKind::Newtype { inner } => {
212 let field = variant.data.fields.first();
213 format!(
214 "{{ name: '{}', fields: {} }}",
215 variant.name,
216 generate_schema_with_field(inner, field, state)
217 )
218 }
219 VariantKind::Tuple { fields } => {
220 let field_schemas: Vec<_> = fields
221 .iter()
222 .map(|f| generate_schema_with_field(f.shape(), Some(f), state))
223 .collect();
224 format!(
225 "{{ name: '{}', fields: [{}] }}",
226 variant.name,
227 field_schemas.join(", ")
228 )
229 }
230 VariantKind::Struct { fields } => {
231 let field_schemas: Vec<_> = fields
232 .iter()
233 .map(|f| {
234 format!(
235 "'{}': {}",
236 f.name,
237 generate_schema_with_field(f.shape(), Some(f), state)
238 )
239 })
240 .collect();
241 format!(
242 "{{ name: '{}', fields: {{ {} }} }}",
243 variant.name,
244 field_schemas.join(", ")
245 )
246 }
247 }
248}
249
250fn generate_scalar_schema(scalar: ScalarType) -> String {
252 match scalar {
253 ScalarType::Bool => "{ kind: 'bool' }".into(),
254 ScalarType::U8 => "{ kind: 'u8' }".into(),
255 ScalarType::U16 => "{ kind: 'u16' }".into(),
256 ScalarType::U32 => "{ kind: 'u32' }".into(),
257 ScalarType::U64 | ScalarType::USize => "{ kind: 'u64' }".into(),
258 ScalarType::I8 => "{ kind: 'i8' }".into(),
259 ScalarType::I16 => "{ kind: 'i16' }".into(),
260 ScalarType::I32 => "{ kind: 'i32' }".into(),
261 ScalarType::I64 | ScalarType::ISize => "{ kind: 'i64' }".into(),
262 ScalarType::U128 | ScalarType::I128 => {
263 panic!(
264 "u128/i128 types are not supported in TypeScript codegen - use smaller integer types or encode as bytes"
265 )
266 }
267 ScalarType::F32 => "{ kind: 'f32' }".into(),
268 ScalarType::F64 => "{ kind: 'f64' }".into(),
269 ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
270 "{ kind: 'string' }".into()
271 }
272 ScalarType::Unit => "{ kind: 'struct', fields: {} }".into(),
273 _ => "{ kind: 'bytes' }".into(),
274 }
275}
276
277fn generate_roam_error_schema(err_schema: &str) -> String {
285 format!(
286 "{{ kind: 'enum', variants: [\
287 {{ name: 'User', fields: {err_schema} }}, \
288 {{ name: 'UnknownMethod', fields: null }}, \
289 {{ name: 'InvalidPayload', fields: null }}, \
290 {{ name: 'Cancelled', fields: null }}\
291 ] }}"
292 )
293}
294
295fn generate_result_schema(ok_schema: &str, err_schema: &str) -> String {
300 let roam_error = generate_roam_error_schema(err_schema);
301 format!(
302 "{{ kind: 'enum', variants: [{{ name: 'Ok', fields: {ok_schema} }}, {{ name: 'Err', fields: {roam_error} }}] }}"
303 )
304}
305
306pub fn generate_descriptor(service: &ServiceDescriptor) -> String {
311 use super::types::collect_named_types;
312 use crate::render::hex_u64;
313
314 let mut out = String::new();
315 let service_name_lower = service.service_name.to_lower_camel_case();
316 let named_types = collect_named_types(service);
317
318 out.push_str("// Named schema registry (for recursive / shared named types)\n");
319 out.push_str(&format!(
320 "const {service_name_lower}_schema_registry: SchemaRegistry = new Map<string, Schema>([\n"
321 ));
322 for (name, shape) in &named_types {
323 let mut state = SchemaGenState {
324 use_named_refs: true,
325 root_name: Some(name.clone()),
326 active: HashSet::new(),
327 };
328 let schema = generate_schema_with_field(shape, None, &mut state);
329 out.push_str(&format!(" [\"{name}\", {schema}],\n"));
330 }
331 out.push_str("]);\n\n");
332
333 out.push_str("// Service descriptor for runtime schema-driven dispatch\n");
334 out.push_str(&format!(
335 "export const {service_name_lower}_descriptor: ServiceDescriptor = {{\n"
336 ));
337 out.push_str(&format!(" service_name: '{}',\n", service.service_name));
338 out.push_str(&format!(
339 " schema_registry: {service_name_lower}_schema_registry,\n"
340 ));
341 out.push_str(" methods: [\n");
342
343 for method in service.methods {
344 let method_name = method.method_name.to_lower_camel_case();
345 let id = crate::method_id(method);
346
347 let mut args_state = SchemaGenState {
349 use_named_refs: true,
350 root_name: None,
351 active: HashSet::new(),
352 };
353 let arg_schemas: Vec<_> = method
354 .args
355 .iter()
356 .map(|a| generate_schema_with_field(a.shape, None, &mut args_state))
357 .collect();
358 let args_schema = format!(
359 "{{ kind: 'tuple', elements: [{}] }}",
360 arg_schemas.join(", ")
361 );
362
363 let result_schema = match classify_shape(method.return_shape) {
365 ShapeKind::Result { ok, err } => {
366 let mut ok_state = SchemaGenState {
367 use_named_refs: true,
368 root_name: None,
369 active: HashSet::new(),
370 };
371 let mut err_state = SchemaGenState {
372 use_named_refs: true,
373 root_name: None,
374 active: HashSet::new(),
375 };
376 let ok_schema = generate_schema_with_field(ok, None, &mut ok_state);
377 let err_schema = generate_schema_with_field(err, None, &mut err_state);
378 generate_result_schema(&ok_schema, &err_schema)
379 }
380 _ => {
381 let mut ok_state = SchemaGenState {
383 use_named_refs: true,
384 root_name: None,
385 active: HashSet::new(),
386 };
387 let ok_schema =
388 generate_schema_with_field(method.return_shape, None, &mut ok_state);
389 generate_result_schema(&ok_schema, "null")
390 }
391 };
392
393 out.push_str(" {\n");
394 out.push_str(&format!(" name: '{method_name}',\n"));
395 out.push_str(&format!(" id: {}n,\n", hex_u64(id)));
396 out.push_str(&format!(" args: {args_schema},\n"));
397 out.push_str(&format!(" result: {result_schema},\n"));
398 out.push_str(" },\n");
399 }
400
401 out.push_str(" ],\n");
402 out.push_str("};\n\n");
403 out
404}