roam_codegen/targets/typescript/
schema.rs1use facet_core::{ScalarType, Shape};
15use heck::ToLowerCamelCase;
16use roam_schema::{
17 EnumInfo, ServiceDetail, ShapeKind, StructInfo, VariantKind, classify_shape, classify_variant,
18 is_bytes, is_rx, is_tx,
19};
20
21pub fn generate_schema(shape: &'static Shape) -> String {
24 if is_bytes(shape) {
26 return "{ kind: 'bytes' }".into();
27 }
28
29 match classify_shape(shape) {
30 ShapeKind::Scalar(scalar) => generate_scalar_schema(scalar),
31 ShapeKind::Tx { inner } => {
32 format!("{{ kind: 'tx', element: {} }}", generate_schema(inner))
33 }
34 ShapeKind::Rx { inner } => {
35 format!("{{ kind: 'rx', element: {} }}", generate_schema(inner))
36 }
37 ShapeKind::List { element } => {
38 format!("{{ kind: 'vec', element: {} }}", generate_schema(element))
39 }
40 ShapeKind::Option { inner } => {
41 format!("{{ kind: 'option', inner: {} }}", generate_schema(inner))
42 }
43 ShapeKind::Array { element, .. } | ShapeKind::Slice { element } => {
44 format!("{{ kind: 'vec', element: {} }}", generate_schema(element))
45 }
46 ShapeKind::Map { key, value } => {
47 format!(
48 "{{ kind: 'map', key: {}, value: {} }}",
49 generate_schema(key),
50 generate_schema(value)
51 )
52 }
53 ShapeKind::Set { element } => {
54 format!("{{ kind: 'vec', element: {} }}", generate_schema(element))
55 }
56 ShapeKind::Tuple { elements } => {
57 let element_schemas: Vec<_> =
59 elements.iter().map(|p| generate_schema(p.shape)).collect();
60 format!(
61 "{{ kind: 'tuple', elements: [{}] }}",
62 element_schemas.join(", ")
63 )
64 }
65 ShapeKind::Struct(StructInfo { fields, .. }) => {
66 let field_schemas: Vec<_> = fields
67 .iter()
68 .map(|f| format!("'{}': {}", f.name, generate_schema(f.shape())))
69 .collect();
70 format!(
71 "{{ kind: 'struct', fields: {{ {} }} }}",
72 field_schemas.join(", ")
73 )
74 }
75 ShapeKind::Enum(EnumInfo { variants, .. }) => {
76 let variant_schemas: Vec<_> = variants.iter().map(generate_enum_variant).collect();
78 format!(
79 "{{ kind: 'enum', variants: [{}] }}",
80 variant_schemas.join(", ")
81 )
82 }
83 ShapeKind::Pointer { pointee } => generate_schema(pointee),
84 ShapeKind::Result { ok, err } => {
85 format!(
87 "{{ kind: 'enum', variants: [{{ name: 'Ok', fields: {} }}, {{ name: 'Err', fields: {} }}] }}",
88 generate_schema(ok),
89 generate_schema(err)
90 )
91 }
92 ShapeKind::TupleStruct { fields } => {
93 let inner: Vec<String> = fields.iter().map(|f| generate_schema(f.shape())).collect();
94 format!("{{ kind: 'tuple', elements: [{}] }}", inner.join(", "))
95 }
96 ShapeKind::Opaque => "{ kind: 'bytes' }".into(),
97 }
98}
99
100fn generate_enum_variant(variant: &facet_core::Variant) -> String {
102 match classify_variant(variant) {
103 VariantKind::Unit => {
104 format!("{{ name: '{}', fields: null }}", variant.name)
105 }
106 VariantKind::Newtype { inner } => {
107 format!(
109 "{{ name: '{}', fields: {} }}",
110 variant.name,
111 generate_schema(inner)
112 )
113 }
114 VariantKind::Tuple { fields } => {
115 let field_schemas: Vec<_> = fields.iter().map(|f| generate_schema(f.shape())).collect();
117 format!(
118 "{{ name: '{}', fields: [{}] }}",
119 variant.name,
120 field_schemas.join(", ")
121 )
122 }
123 VariantKind::Struct { fields } => {
124 let field_schemas: Vec<_> = fields
126 .iter()
127 .map(|f| format!("'{}': {}", f.name, generate_schema(f.shape())))
128 .collect();
129 format!(
130 "{{ name: '{}', fields: {{ {} }} }}",
131 variant.name,
132 field_schemas.join(", ")
133 )
134 }
135 }
136}
137
138fn generate_scalar_schema(scalar: ScalarType) -> String {
140 match scalar {
141 ScalarType::Bool => "{ kind: 'bool' }".into(),
142 ScalarType::U8 => "{ kind: 'u8' }".into(),
143 ScalarType::U16 => "{ kind: 'u16' }".into(),
144 ScalarType::U32 => "{ kind: 'u32' }".into(),
145 ScalarType::U64 | ScalarType::USize | ScalarType::U128 => "{ kind: 'u64' }".into(),
146 ScalarType::I8 => "{ kind: 'i8' }".into(),
147 ScalarType::I16 => "{ kind: 'i16' }".into(),
148 ScalarType::I32 => "{ kind: 'i32' }".into(),
149 ScalarType::I64 | ScalarType::ISize | ScalarType::I128 => "{ kind: 'i64' }".into(),
150 ScalarType::F32 => "{ kind: 'f32' }".into(),
151 ScalarType::F64 => "{ kind: 'f64' }".into(),
152 ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
153 "{ kind: 'string' }".into()
154 }
155 ScalarType::Unit => "{ kind: 'struct', fields: {} }".into(),
156 _ => "{ kind: 'bytes' }".into(),
157 }
158}
159
160pub fn generate_method_schemas(service: &ServiceDetail) -> String {
170 let mut out = String::new();
171 let service_name_lower = service.name.to_lower_camel_case();
172
173 out.push_str("// Method schemas for runtime encoding/decoding and channel binding\n");
174 out.push_str(&format!(
175 "export const {service_name_lower}_schemas: Record<string, MethodSchema> = {{\n"
176 ));
177
178 for method in &service.methods {
179 let method_name = method.method_name.to_lower_camel_case();
180 let arg_schemas: Vec<_> = method.args.iter().map(|a| generate_schema(a.ty)).collect();
181
182 let (return_schema, error_schema) = match classify_shape(method.return_type) {
184 ShapeKind::Result { ok, err } => {
185 (generate_schema(ok), generate_schema(err))
187 }
188 _ => {
189 (generate_schema(method.return_type), "null".to_string())
191 }
192 };
193
194 out.push_str(&format!(
195 " {method_name}: {{ args: [{}], returns: {}, error: {} }},\n",
196 arg_schemas.join(", "),
197 return_schema,
198 error_schema
199 ));
200 }
201
202 out.push_str("};\n\n");
203 out
204}
205
206pub fn generate_binding_serializers(service: &ServiceDetail) -> String {
209 let mut out = String::new();
210 let service_name_lower = service.name.to_lower_camel_case();
211
212 out.push_str("// Serializers for runtime channel binding\n");
213 out.push_str(&format!(
214 "export const {service_name_lower}_serializers: BindingSerializers = {{\n"
215 ));
216
217 out.push_str(" getTxSerializer(schema: Schema): (value: unknown) => Uint8Array {\n");
219 out.push_str(" switch (schema.kind) {\n");
220 out.push_str(" case 'bool': return (v) => encodeBool(v as boolean);\n");
221 out.push_str(" case 'u8': return (v) => encodeU8(v as number);\n");
222 out.push_str(" case 'i8': return (v) => encodeI8(v as number);\n");
223 out.push_str(" case 'u16': return (v) => encodeU16(v as number);\n");
224 out.push_str(" case 'i16': return (v) => encodeI16(v as number);\n");
225 out.push_str(" case 'u32': return (v) => encodeU32(v as number);\n");
226 out.push_str(" case 'i32': return (v) => encodeI32(v as number);\n");
227 out.push_str(" case 'u64': return (v) => encodeU64(v as bigint);\n");
228 out.push_str(" case 'i64': return (v) => encodeI64(v as bigint);\n");
229 out.push_str(" case 'f32': return (v) => encodeF32(v as number);\n");
230 out.push_str(" case 'f64': return (v) => encodeF64(v as number);\n");
231 out.push_str(" case 'string': return (v) => encodeString(v as string);\n");
232 out.push_str(" case 'bytes': return (v) => encodeBytes(v as Uint8Array);\n");
233 out.push_str(
234 " default: throw new Error(`Unsupported schema kind for Tx: ${schema.kind}`);\n",
235 );
236 out.push_str(" }\n");
237 out.push_str(" },\n");
238
239 out.push_str(" getRxDeserializer(schema: Schema): (bytes: Uint8Array) => unknown {\n");
241 out.push_str(" switch (schema.kind) {\n");
242 out.push_str(" case 'bool': return (b) => decodeBool(b, 0).value;\n");
243 out.push_str(" case 'u8': return (b) => decodeU8(b, 0).value;\n");
244 out.push_str(" case 'i8': return (b) => decodeI8(b, 0).value;\n");
245 out.push_str(" case 'u16': return (b) => decodeU16(b, 0).value;\n");
246 out.push_str(" case 'i16': return (b) => decodeI16(b, 0).value;\n");
247 out.push_str(" case 'u32': return (b) => decodeU32(b, 0).value;\n");
248 out.push_str(" case 'i32': return (b) => decodeI32(b, 0).value;\n");
249 out.push_str(" case 'u64': return (b) => decodeU64(b, 0).value;\n");
250 out.push_str(" case 'i64': return (b) => decodeI64(b, 0).value;\n");
251 out.push_str(" case 'f32': return (b) => decodeF32(b, 0).value;\n");
252 out.push_str(" case 'f64': return (b) => decodeF64(b, 0).value;\n");
253 out.push_str(" case 'string': return (b) => decodeString(b, 0).value;\n");
254 out.push_str(" case 'bytes': return (b) => decodeBytes(b, 0).value;\n");
255 out.push_str(
256 " default: throw new Error(`Unsupported schema kind for Rx: ${schema.kind}`);\n",
257 );
258 out.push_str(" }\n");
259 out.push_str(" },\n");
260
261 out.push_str("};\n\n");
262 out
263}
264
265pub fn generate_schemas(service: &ServiceDetail) -> String {
267 let mut out = String::new();
268
269 out.push_str(&generate_method_schemas(service));
271
272 let has_streaming = service.methods.iter().any(|m| {
274 m.args.iter().any(|a| is_tx(a.ty) || is_rx(a.ty))
275 || is_tx(m.return_type)
276 || is_rx(m.return_type)
277 });
278
279 if has_streaming {
281 out.push_str(&generate_binding_serializers(service));
282 }
283
284 out
285}