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