vox_codegen/targets/typescript/
client.rs1use heck::{ToLowerCamelCase, ToUpperCamelCase};
9use vox_types::{ServiceDescriptor, ShapeKind, classify_shape, is_rx, is_tx};
10
11use super::types::{ts_type_client_arg, ts_type_client_return};
12
13fn format_doc_comment(doc: &str, indent: &str) -> String {
15 let lines: Vec<&str> = doc.lines().collect();
16
17 if lines.is_empty() {
18 return String::new();
19 }
20
21 if lines.len() == 1 {
22 format!("{}/** {} */\n", indent, lines[0].trim())
23 } else {
24 let mut out = format!("{}/**\n", indent);
25 for line in lines {
26 let trimmed = line.trim();
27 if trimmed.is_empty() {
28 out.push_str(&format!("{} *\n", indent));
29 } else {
30 out.push_str(&format!("{} * {}\n", indent, trimmed));
31 }
32 }
33 out.push_str(&format!("{} */\n", indent));
34 out
35 }
36}
37
38pub fn generate_caller_interface(service: &ServiceDescriptor) -> String {
42 let mut out = String::new();
43 let service_name = service.service_name.to_upper_camel_case();
44
45 out.push_str(&format!("// Caller interface for {service_name}\n"));
46 out.push_str(&format!("export interface {service_name}Caller {{\n"));
47
48 for method in service.methods {
49 let method_name = method.method_name.to_lower_camel_case();
50 let args = method
51 .args
52 .iter()
53 .map(|a| {
54 format!(
55 "{}: {}",
56 a.name.to_lower_camel_case(),
57 ts_type_client_arg(a.shape)
58 )
59 })
60 .collect::<Vec<_>>()
61 .join(", ");
62 let ret_ty = ts_type_client_return(method.return_shape);
63
64 if let Some(doc) = &method.doc {
65 out.push_str(&format_doc_comment(doc, " "));
66 }
67 out.push_str(&format!(" {method_name}({args}): Promise<{ret_ty}>;\n"));
68 }
69
70 out.push_str("}\n\n");
71 out
72}
73
74pub fn generate_client_impl(service: &ServiceDescriptor) -> String {
83 let mut out = String::new();
84 let service_name = service.service_name.to_upper_camel_case();
85 let service_name_lower = service.service_name.to_lower_camel_case();
86
87 out.push_str(&format!("// Client implementation for {service_name}\n"));
88 out.push_str(&format!(
89 "export class {service_name}Client implements {service_name}Caller {{\n"
90 ));
91 out.push_str(" static get descriptor(): ServiceDescriptor {\n");
92 out.push_str(&format!(" return {service_name_lower}_descriptor;\n"));
93 out.push_str(" }\n\n");
94 out.push_str(" private caller: Caller;\n\n");
95 out.push_str(" constructor(caller: Caller) {\n");
96 out.push_str(" this.caller = caller;\n");
97 out.push_str(" }\n\n");
98
99 for method in service.methods {
100 let method_name = method.method_name.to_lower_camel_case();
101 let method_descriptor_name = format!("{service_name_lower}_{method_name}_method");
102
103 let has_streaming_args = method.args.iter().any(|a| is_tx(a.shape) || is_rx(a.shape));
104
105 let args = method
106 .args
107 .iter()
108 .map(|a| {
109 format!(
110 "{}: {}",
111 a.name.to_lower_camel_case(),
112 ts_type_client_arg(a.shape)
113 )
114 })
115 .collect::<Vec<_>>()
116 .join(", ");
117
118 let ret_ty = ts_type_client_return(method.return_shape);
119
120 let args_record = if method.args.is_empty() {
121 "{}".to_string()
122 } else {
123 let fields: Vec<_> = method
124 .args
125 .iter()
126 .map(|a| a.name.to_lower_camel_case())
127 .collect();
128 format!("{{ {} }}", fields.join(", "))
129 };
130
131 if let Some(doc) = &method.doc {
132 out.push_str(&format_doc_comment(doc, " "));
133 }
134 out.push_str(&format!(
135 " async {method_name}({args}): Promise<{ret_ty}> {{\n"
136 ));
137
138 let _ = has_streaming_args;
139 let method_id_hex = crate::render::hex_u64(crate::method_id(method));
141 let call_fields = format!(
144 " method: \"{service_name}.{method_name}\",\n args: {args_record},\n descriptor: {method_descriptor_name},\n methodSchemas: {service_name_lower}Methods[\"{method_id_hex}\"],\n registry: {service_name_lower}Registry,\n",
145 );
146
147 let is_fallible = matches!(
148 classify_shape(method.return_shape),
149 ShapeKind::Result { .. }
150 );
151
152 if is_fallible {
153 out.push_str(" try {\n");
154 out.push_str(" const __voxResult = await this.caller.call({\n");
155 out.push_str(&call_fields);
156 out.push_str(" });\n");
157 out.push_str(&format!(
158 " return {{ ok: true, value: __voxResult }} as {ret_ty};\n"
159 ));
160 out.push_str(" } catch (e: any) {\n");
161 out.push_str(" if (e instanceof RpcError && e.isUserError()) {\n");
162 out.push_str(&format!(
163 " return {{ ok: false, error: e.userError }} as {ret_ty};\n"
164 ));
165 out.push_str(" }\n");
166 out.push_str(" throw e;\n");
167 out.push_str(" }\n");
168 } else {
169 out.push_str(" const __voxResult = await this.caller.call({\n");
170 out.push_str(&call_fields);
171 out.push_str(" });\n");
172 out.push_str(&format!(" return __voxResult as {ret_ty};\n"));
173 }
174
175 out.push_str(" }\n\n");
176 }
177
178 out.push_str("}\n\n");
179 out
180}
181
182pub fn generate_connect_function(service: &ServiceDescriptor) -> String {
184 let service_name = service.service_name.to_upper_camel_case();
185
186 let mut out = String::new();
187 out.push_str(&format!(
188 "/**\n * Connect to a {service_name} server over WebSocket.\n"
189 ));
190 out.push_str(" * @param url - WebSocket URL (e.g., \"ws://localhost:9000\")\n");
191 out.push_str(&format!(
192 " * @returns A connected {service_name}Client instance\n"
193 ));
194 out.push_str(" */\n");
195 out.push_str(&format!(
196 "export async function connect{service_name}(\n url: string,\n options: ConnectLaneOptions = {{}},\n): Promise<{service_name}Client> {{\n"
197 ));
198 out.push_str(&format!(
199 " return connectLane(wsConnector(url), {service_name}Client, {{\n ...options,\n laneMetadata: options.laneMetadata ?? voxServiceMetadata(\"{}\"),\n }});\n",
200 service.service_name
201 ));
202 out.push_str("}\n\n");
203 out
204}
205
206pub fn generate_client_with_connect_helper(
207 service: &ServiceDescriptor,
208 connect_helper: bool,
209) -> String {
210 let mut out = String::new();
211 out.push_str(&generate_caller_interface(service));
212 out.push_str(&generate_client_impl(service));
213 if connect_helper {
214 out.push_str(&generate_connect_function(service));
215 }
216 out
217}
218
219pub fn generate_client(service: &ServiceDescriptor) -> String {
221 generate_client_with_connect_helper(service, true)
222}