roam_codegen/targets/typescript/
schema.rs1use facet_core::{Field, ScalarType, Shape};
18use heck::ToLowerCamelCase;
19use roam_types::{
20 EnumInfo, ServiceDescriptor, ShapeKind, StructInfo, VariantKind, classify_shape,
21 classify_variant, is_bytes,
22};
23
24pub fn generate_schema(shape: &'static Shape) -> String {
26 generate_schema_with_field(shape, None)
27}
28
29fn generate_schema_with_field(shape: &'static Shape, field: Option<&Field>) -> String {
30 let bytes_schema = if field.is_some_and(|f| f.has_builtin_attr("trailing")) {
31 "{ kind: 'bytes', trailing: true }"
32 } else {
33 "{ kind: 'bytes' }"
34 };
35
36 if is_bytes(shape) {
38 return bytes_schema.into();
39 }
40
41 match classify_shape(shape) {
42 ShapeKind::Scalar(scalar) => generate_scalar_schema(scalar),
43 ShapeKind::Tx { inner } => {
44 format!(
45 "{{ kind: 'tx', element: {} }}",
46 generate_schema_with_field(inner, None)
47 )
48 }
49 ShapeKind::Rx { inner } => {
50 format!(
51 "{{ kind: 'rx', element: {} }}",
52 generate_schema_with_field(inner, None)
53 )
54 }
55 ShapeKind::List { element } => {
56 format!(
57 "{{ kind: 'vec', element: {} }}",
58 generate_schema_with_field(element, None)
59 )
60 }
61 ShapeKind::Option { inner } => {
62 format!(
63 "{{ kind: 'option', inner: {} }}",
64 generate_schema_with_field(inner, None)
65 )
66 }
67 ShapeKind::Array { element, .. } | ShapeKind::Slice { element } => {
68 format!(
69 "{{ kind: 'vec', element: {} }}",
70 generate_schema_with_field(element, None)
71 )
72 }
73 ShapeKind::Map { key, value } => {
74 format!(
75 "{{ kind: 'map', key: {}, value: {} }}",
76 generate_schema_with_field(key, None),
77 generate_schema_with_field(value, None)
78 )
79 }
80 ShapeKind::Set { element } => {
81 format!(
82 "{{ kind: 'vec', element: {} }}",
83 generate_schema_with_field(element, None)
84 )
85 }
86 ShapeKind::Tuple { elements } => {
87 let element_schemas: Vec<_> = elements
88 .iter()
89 .map(|p| generate_schema_with_field(p.shape, None))
90 .collect();
91 format!(
92 "{{ kind: 'tuple', elements: [{}] }}",
93 element_schemas.join(", ")
94 )
95 }
96 ShapeKind::Struct(StructInfo { fields, .. }) => {
97 let field_schemas: Vec<_> = fields
98 .iter()
99 .map(|f| {
100 format!(
101 "'{}': {}",
102 f.name,
103 generate_schema_with_field(f.shape(), Some(f))
104 )
105 })
106 .collect();
107 format!(
108 "{{ kind: 'struct', fields: {{ {} }} }}",
109 field_schemas.join(", ")
110 )
111 }
112 ShapeKind::Enum(EnumInfo { variants, .. }) => {
113 let variant_schemas: Vec<_> = variants.iter().map(generate_enum_variant).collect();
114 format!(
115 "{{ kind: 'enum', variants: [{}] }}",
116 variant_schemas.join(", ")
117 )
118 }
119 ShapeKind::Pointer { pointee } => generate_schema(pointee),
120 ShapeKind::Result { ok, err } => {
121 format!(
123 "{{ kind: 'enum', variants: [{{ name: 'Ok', fields: {} }}, {{ name: 'Err', fields: {} }}] }}",
124 generate_schema_with_field(ok, None),
125 generate_schema_with_field(err, None)
126 )
127 }
128 ShapeKind::TupleStruct { fields } => {
129 let inner: Vec<String> = fields
130 .iter()
131 .map(|f| generate_schema_with_field(f.shape(), Some(f)))
132 .collect();
133 format!("{{ kind: 'tuple', elements: [{}] }}", inner.join(", "))
134 }
135 ShapeKind::Opaque => bytes_schema.into(),
136 }
137}
138
139fn generate_enum_variant(variant: &facet_core::Variant) -> String {
141 match classify_variant(variant) {
142 VariantKind::Unit => {
143 format!("{{ name: '{}', fields: null }}", variant.name)
144 }
145 VariantKind::Newtype { inner } => {
146 let field = variant.data.fields.first();
147 format!(
148 "{{ name: '{}', fields: {} }}",
149 variant.name,
150 generate_schema_with_field(inner, field)
151 )
152 }
153 VariantKind::Tuple { fields } => {
154 let field_schemas: Vec<_> = fields
155 .iter()
156 .map(|f| generate_schema_with_field(f.shape(), Some(f)))
157 .collect();
158 format!(
159 "{{ name: '{}', fields: [{}] }}",
160 variant.name,
161 field_schemas.join(", ")
162 )
163 }
164 VariantKind::Struct { fields } => {
165 let field_schemas: Vec<_> = fields
166 .iter()
167 .map(|f| {
168 format!(
169 "'{}': {}",
170 f.name,
171 generate_schema_with_field(f.shape(), Some(f))
172 )
173 })
174 .collect();
175 format!(
176 "{{ name: '{}', fields: {{ {} }} }}",
177 variant.name,
178 field_schemas.join(", ")
179 )
180 }
181 }
182}
183
184fn generate_scalar_schema(scalar: ScalarType) -> String {
186 match scalar {
187 ScalarType::Bool => "{ kind: 'bool' }".into(),
188 ScalarType::U8 => "{ kind: 'u8' }".into(),
189 ScalarType::U16 => "{ kind: 'u16' }".into(),
190 ScalarType::U32 => "{ kind: 'u32' }".into(),
191 ScalarType::U64 | ScalarType::USize => "{ kind: 'u64' }".into(),
192 ScalarType::I8 => "{ kind: 'i8' }".into(),
193 ScalarType::I16 => "{ kind: 'i16' }".into(),
194 ScalarType::I32 => "{ kind: 'i32' }".into(),
195 ScalarType::I64 | ScalarType::ISize => "{ kind: 'i64' }".into(),
196 ScalarType::U128 | ScalarType::I128 => {
197 panic!(
198 "u128/i128 types are not supported in TypeScript codegen - use smaller integer types or encode as bytes"
199 )
200 }
201 ScalarType::F32 => "{ kind: 'f32' }".into(),
202 ScalarType::F64 => "{ kind: 'f64' }".into(),
203 ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
204 "{ kind: 'string' }".into()
205 }
206 ScalarType::Unit => "{ kind: 'struct', fields: {} }".into(),
207 _ => "{ kind: 'bytes' }".into(),
208 }
209}
210
211fn generate_roam_error_schema(err_schema: &str) -> String {
219 format!(
220 "{{ kind: 'enum', variants: [\
221 {{ name: 'User', fields: {err_schema} }}, \
222 {{ name: 'UnknownMethod', fields: null }}, \
223 {{ name: 'InvalidPayload', fields: null }}, \
224 {{ name: 'Cancelled', fields: null }}\
225 ] }}"
226 )
227}
228
229fn generate_result_schema(ok_schema: &str, err_schema: &str) -> String {
234 let roam_error = generate_roam_error_schema(err_schema);
235 format!(
236 "{{ kind: 'enum', variants: [{{ name: 'Ok', fields: {ok_schema} }}, {{ name: 'Err', fields: {roam_error} }}] }}"
237 )
238}
239
240pub fn generate_descriptor(service: &ServiceDescriptor) -> String {
245 use crate::render::hex_u64;
246
247 let mut out = String::new();
248 let service_name_lower = service.service_name.to_lower_camel_case();
249
250 out.push_str("// Service descriptor for runtime schema-driven dispatch\n");
251 out.push_str(&format!(
252 "export const {service_name_lower}_descriptor: ServiceDescriptor = {{\n"
253 ));
254 out.push_str(&format!(" service_name: '{}',\n", service.service_name));
255 out.push_str(" methods: [\n");
256
257 for method in service.methods {
258 let method_name = method.method_name.to_lower_camel_case();
259 let id = crate::method_id(method);
260
261 let arg_schemas: Vec<_> = method
263 .args
264 .iter()
265 .map(|a| generate_schema(a.shape))
266 .collect();
267 let args_schema = format!(
268 "{{ kind: 'tuple', elements: [{}] }}",
269 arg_schemas.join(", ")
270 );
271
272 let result_schema = match classify_shape(method.return_shape) {
274 ShapeKind::Result { ok, err } => {
275 let ok_schema = generate_schema(ok);
276 let err_schema = generate_schema(err);
277 generate_result_schema(&ok_schema, &err_schema)
278 }
279 _ => {
280 let ok_schema = generate_schema(method.return_shape);
282 generate_result_schema(&ok_schema, "null")
283 }
284 };
285
286 out.push_str(" {\n");
287 out.push_str(&format!(" name: '{method_name}',\n"));
288 out.push_str(&format!(" id: {}n,\n", hex_u64(id)));
289 out.push_str(&format!(" args: {args_schema},\n"));
290 out.push_str(&format!(" result: {result_schema},\n"));
291 out.push_str(" },\n");
292 }
293
294 out.push_str(" ],\n");
295 out.push_str("};\n\n");
296 out
297}