roam_codegen/targets/typescript/
schema.rs

1//! TypeScript schema generation for runtime channel binding.
2//!
3//! Generates runtime schema information that allows the TypeScript runtime
4//! to discover and bind streaming channels (Tx/Rx) in method arguments.
5//!
6//! The generated schemas use the new EnumVariant[] format:
7//! ```typescript
8//! { kind: 'enum', variants: [
9//!   { name: 'Circle', fields: [{ kind: 'f64' }] },
10//!   { name: 'Point', fields: null },
11//! ] }
12//! ```
13
14use 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
21/// Generate a TypeScript Schema object literal for a type.
22/// Used by the runtime binder to find and bind Tx/Rx channels.
23pub fn generate_schema(shape: &'static Shape) -> String {
24    // Check for bytes first (Vec<u8>)
25    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            // Generate as TupleSchema
58            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            // Generate new EnumSchema format with EnumVariant[]
77            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            // Represent Result as enum with Ok/Err variants using new format
86            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
100/// Generate an EnumVariant object literal.
101fn 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            // Newtype variant: fields is a single Schema
108            format!(
109                "{{ name: '{}', fields: {} }}",
110                variant.name,
111                generate_schema(inner)
112            )
113        }
114        VariantKind::Tuple { fields } => {
115            // Tuple variant: fields is Schema[]
116            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            // Struct variant: fields is Record<string, Schema>
125            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
138/// Generate schema for scalar types.
139fn 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
160/// Generate method schemas for runtime channel binding and encoding/decoding.
161///
162/// For methods returning `Result<T, E>`:
163/// - `returns` is the schema for `T` (success type, used after decodeRpcResult succeeds)
164/// - `error` is the schema for `E` (user error type, used when decodeRpcResult throws USER error)
165///
166/// For infallible methods returning `T`:
167/// - `returns` is the schema for `T`
168/// - `error` is null
169pub 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        // Check if return type is Result<T, E>
183        let (return_schema, error_schema) = match classify_shape(method.return_type) {
184            ShapeKind::Result { ok, err } => {
185                // For Result<T, E>: returns is T, error is E
186                (generate_schema(ok), generate_schema(err))
187            }
188            _ => {
189                // Infallible method: returns is the full type, no error schema
190                (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
206/// Generate BindingSerializers for runtime channel binding.
207/// These provide encode/decode functions based on schema element types.
208pub 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    // getTxSerializer: given element schema, return a serializer function
218    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    // getRxDeserializer: given element schema, return a deserializer function
240    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
265/// Generate complete schema exports (method schemas + serializers).
266pub fn generate_schemas(service: &ServiceDetail) -> String {
267    let mut out = String::new();
268
269    // Generate method schemas
270    out.push_str(&generate_method_schemas(service));
271
272    // Check if any method uses streaming
273    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    // Generate serializers only if streaming is used
280    if has_streaming {
281        out.push_str(&generate_binding_serializers(service));
282    }
283
284    out
285}