Skip to main content

vox_codegen/targets/typescript/
server.rs

1//! TypeScript server/handler generation.
2//!
3//! Generates the handler interface and a Dispatcher class that routes calls
4//! to handler methods. All encode/decode is handled by the runtime via the
5//! service descriptor — no serialization code in generated output.
6
7use heck::{ToLowerCamelCase, ToUpperCamelCase};
8use vox_types::{ServiceDescriptor, ShapeKind, classify_shape};
9
10use super::types::{ts_type_server_arg, ts_type_server_return};
11
12/// Generate handler interface (for implementing the service).
13///
14/// r[impl rpc.channel.binding] - Handler binds channels in args.
15pub fn generate_handler_interface(service: &ServiceDescriptor) -> String {
16    let mut out = String::new();
17    let service_name = service.service_name.to_upper_camel_case();
18
19    out.push_str(&format!("// Handler interface for {service_name}\n"));
20    out.push_str(&format!("export interface {service_name}Handler {{\n"));
21
22    for method in service.methods {
23        let method_name = method.method_name.to_lower_camel_case();
24        let args = method
25            .args
26            .iter()
27            .map(|a| {
28                format!(
29                    "{}: {}",
30                    a.name.to_lower_camel_case(),
31                    ts_type_server_arg(a.shape)
32                )
33            })
34            .collect::<Vec<_>>()
35            .join(", ");
36        let ret_ty = ts_type_server_return(method.return_shape);
37
38        out.push_str(&format!(
39            "  {method_name}({args}): Promise<{ret_ty}> | {ret_ty};\n"
40        ));
41    }
42
43    out.push_str("}\n\n");
44    out
45}
46
47/// Generate the Dispatcher class.
48///
49/// Implements `Dispatcher` from vox-core:
50/// - `getDescriptor()` returns the service descriptor
51/// - `dispatch(context, method, args, call)` routes by method ID and calls handler methods
52///
53/// The runtime handles all arg decoding (using method.args tuple schema) and
54/// response encoding (using method.result schema via call.reply/replyErr).
55/// Generated dispatch code only does type casts and handler invocation.
56pub fn generate_dispatcher_class(service: &ServiceDescriptor) -> String {
57    use crate::render::hex_u64;
58
59    let mut out = String::new();
60    let service_name = service.service_name.to_upper_camel_case();
61    let service_name_lower = service.service_name.to_lower_camel_case();
62
63    out.push_str(&format!("// Dispatcher for {service_name}\n"));
64    out.push_str(&format!(
65        "export class {service_name}Dispatcher implements Dispatcher {{\n"
66    ));
67    out.push_str(&format!(
68        "  private readonly handler: {service_name}Handler;\n\n"
69    ));
70    out.push_str(&format!(
71        "  constructor(handler: {service_name}Handler) {{\n"
72    ));
73    out.push_str("    this.handler = handler;\n");
74    out.push_str("  }\n\n");
75
76    // getDescriptor()
77    out.push_str("  getDescriptor(): ServiceDescriptor {\n");
78    out.push_str(&format!("    return {service_name_lower}_descriptor;\n"));
79    out.push_str("  }\n\n");
80
81    // dispatch()
82    out.push_str(
83        "  async dispatch(_context: RequestContext, method: MethodDescriptor, args: unknown[], call: VoxCall): Promise<void> {\n",
84    );
85
86    let mut first = true;
87    for method in service.methods {
88        let method_name = method.method_name.to_lower_camel_case();
89        let id = crate::method_id(method);
90        let is_fallible = matches!(
91            classify_shape(method.return_shape),
92            ShapeKind::Result { .. }
93        );
94
95        // Build typed arg list from args array
96        let arg_names: Vec<_> = method
97            .args
98            .iter()
99            .map(|a| a.name.to_lower_camel_case())
100            .collect();
101        let typed_args: Vec<_> = method
102            .args
103            .iter()
104            .enumerate()
105            .map(|(i, a)| format!("args[{i}] as {}", ts_type_server_arg(a.shape)))
106            .collect();
107
108        // Find Tx arg indices for closing after handler returns
109        let tx_arg_indices: Vec<usize> = method
110            .args
111            .iter()
112            .enumerate()
113            .filter(|(_, a)| matches!(classify_shape(a.shape), ShapeKind::Tx { .. }))
114            .map(|(i, _)| i)
115            .collect();
116
117        let keyword = if first { "if" } else { "} else if" };
118        first = false;
119
120        out.push_str(&format!(
121            "    {keyword} (method.id === {}n) {{\n",
122            hex_u64(id)
123        ));
124        out.push_str("      try {\n");
125        out.push_str(&format!(
126            "        const result = await this.handler.{method_name}({});\n",
127            typed_args.join(", ")
128        ));
129
130        // Close Tx args before replying (ensures Close messages precede Response)
131        for i in &tx_arg_indices {
132            let arg_name = &arg_names[*i];
133            out.push_str(&format!(
134                "        (args[{i}] as {{ close(): void }}).close(); // close {arg_name} before reply\n"
135            ));
136        }
137
138        if is_fallible {
139            out.push_str("        if (result.ok) call.reply(result.value); else call.replyErr(result.error);\n");
140        } else {
141            out.push_str("        call.reply(result);\n");
142        }
143
144        out.push_str("      } catch (error) {\n");
145        out.push_str(
146            "        call.replyInternalError(error instanceof Error ? error.message : String(error));\n",
147        );
148        out.push_str("      }\n");
149    }
150
151    if !first {
152        out.push_str("    }\n");
153    }
154
155    out.push_str("  }\n");
156    out.push_str("}\n\n");
157    out
158}
159
160/// Generate complete server code (handler interface + dispatcher class).
161pub fn generate_server(service: &ServiceDescriptor) -> String {
162    let mut out = String::new();
163    out.push_str(&generate_handler_interface(service));
164    out.push_str(&generate_dispatcher_class(service));
165    out
166}