1use std::fmt::Write;
2
3use super::typescript::{emit_jsdoc, rust_type_to_ts};
4use crate::model::{Manifest, Procedure, ProcedureKind};
5
6const GENERATED_HEADER: &str = "\
8// This file is auto-generated by vercel-rpc-cli. Do not edit manually.
9// Re-run `rpc generate` or use `rpc watch` to regenerate.
10";
11
12const ERROR_CLASS: &str = r#"export class RpcError extends Error {
14 readonly status: number;
15 readonly data: unknown;
16
17 constructor(status: number, message: string, data?: unknown) {
18 super(message);
19 this.name = "RpcError";
20 this.status = status;
21 this.data = data;
22 }
23}"#;
24
25const FETCH_HELPER: &str = r#"async function rpcFetch(
27 baseUrl: string,
28 method: "GET" | "POST",
29 procedure: string,
30 input?: unknown,
31): Promise<unknown> {
32 let url = `${baseUrl}/${procedure}`;
33 const init: RequestInit = { method, headers: {} };
34
35 if (method === "GET" && input !== undefined) {
36 url += `?input=${encodeURIComponent(JSON.stringify(input))}`;
37 } else if (method === "POST" && input !== undefined) {
38 init.body = JSON.stringify(input);
39 (init.headers as Record<string, string>)["Content-Type"] = "application/json";
40 }
41
42 const res = await fetch(url, init);
43
44 if (!res.ok) {
45 let data: unknown;
46 try {
47 data = await res.json();
48 } catch {
49 data = await res.text().catch(() => null);
50 }
51 throw new RpcError(
52 res.status,
53 `RPC error on "${procedure}": ${res.status} ${res.statusText}`,
54 data,
55 );
56 }
57
58 const json = await res.json();
59 return json?.result?.data ?? json;
60}"#;
61
62pub fn generate_client_file(
71 manifest: &Manifest,
72 types_import_path: &str,
73 preserve_docs: bool,
74) -> String {
75 let mut out = String::with_capacity(2048);
76
77 out.push_str(GENERATED_HEADER);
79 out.push('\n');
80
81 let type_names: Vec<&str> = manifest
83 .structs
84 .iter()
85 .map(|s| s.name.as_str())
86 .chain(manifest.enums.iter().map(|e| e.name.as_str()))
87 .collect();
88
89 if type_names.is_empty() {
91 let _ = writeln!(
92 out,
93 "import type {{ Procedures }} from \"{types_import_path}\";\n"
94 );
95 let _ = writeln!(out, "export type {{ Procedures }};\n");
96 } else {
97 let types_csv = type_names.join(", ");
98 let _ = writeln!(
99 out,
100 "import type {{ Procedures, {types_csv} }} from \"{types_import_path}\";\n"
101 );
102 let _ = writeln!(out, "export type {{ Procedures, {types_csv} }};\n");
103 }
104
105 let _ = writeln!(out, "{ERROR_CLASS}\n");
107
108 let _ = writeln!(out, "{FETCH_HELPER}\n");
110
111 generate_type_helpers(&mut out);
113 out.push('\n');
114
115 generate_client_factory(manifest, preserve_docs, &mut out);
117
118 out
119}
120
121fn generate_type_helpers(out: &mut String) {
123 let _ = writeln!(out, "type QueryKey = keyof Procedures[\"queries\"];");
124 let _ = writeln!(out, "type MutationKey = keyof Procedures[\"mutations\"];");
125 let _ = writeln!(
126 out,
127 "type QueryInput<K extends QueryKey> = Procedures[\"queries\"][K][\"input\"];"
128 );
129 let _ = writeln!(
130 out,
131 "type QueryOutput<K extends QueryKey> = Procedures[\"queries\"][K][\"output\"];"
132 );
133 let _ = writeln!(
134 out,
135 "type MutationInput<K extends MutationKey> = Procedures[\"mutations\"][K][\"input\"];"
136 );
137 let _ = writeln!(
138 out,
139 "type MutationOutput<K extends MutationKey> = Procedures[\"mutations\"][K][\"output\"];"
140 );
141}
142
143fn generate_client_factory(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
145 let has_queries = manifest
146 .procedures
147 .iter()
148 .any(|p| p.kind == ProcedureKind::Query);
149 let has_mutations = manifest
150 .procedures
151 .iter()
152 .any(|p| p.kind == ProcedureKind::Mutation);
153
154 let _ = writeln!(out, "export interface RpcClient {{");
156
157 if has_queries {
158 generate_query_overloads(manifest, preserve_docs, out);
159 }
160
161 if has_mutations {
162 if has_queries {
163 out.push('\n');
164 }
165 generate_mutation_overloads(manifest, preserve_docs, out);
166 }
167
168 let _ = writeln!(out, "}}");
169 out.push('\n');
170
171 let _ = writeln!(
173 out,
174 "export function createRpcClient(baseUrl: string): RpcClient {{"
175 );
176 let _ = writeln!(out, " return {{");
177
178 if has_queries {
179 let _ = writeln!(
180 out,
181 " query(key: QueryKey, ...args: unknown[]): Promise<unknown> {{"
182 );
183 let _ = writeln!(
184 out,
185 " return rpcFetch(baseUrl, \"GET\", key, args[0]);"
186 );
187 let _ = writeln!(out, " }},");
188 }
189
190 if has_mutations {
191 let _ = writeln!(
192 out,
193 " mutate(key: MutationKey, ...args: unknown[]): Promise<unknown> {{"
194 );
195 let _ = writeln!(
196 out,
197 " return rpcFetch(baseUrl, \"POST\", key, args[0]);"
198 );
199 let _ = writeln!(out, " }},");
200 }
201
202 let _ = writeln!(out, " }} as RpcClient;");
203 let _ = writeln!(out, "}}");
204}
205
206fn generate_query_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
208 let (void_queries, non_void_queries): (Vec<_>, Vec<_>) = manifest
209 .procedures
210 .iter()
211 .filter(|p| p.kind == ProcedureKind::Query)
212 .partition(|p| is_void_input(p));
213
214 for proc in &void_queries {
216 if preserve_docs && let Some(doc) = &proc.docs {
217 emit_jsdoc(doc, " ", out);
218 }
219 let output_ts = proc
220 .output
221 .as_ref()
222 .map(rust_type_to_ts)
223 .unwrap_or_else(|| "void".to_string());
224 let _ = writeln!(
225 out,
226 " query(key: \"{}\"): Promise<{}>;",
227 proc.name, output_ts,
228 );
229 }
230
231 for proc in &non_void_queries {
233 if preserve_docs && let Some(doc) = &proc.docs {
234 emit_jsdoc(doc, " ", out);
235 }
236 let input_ts = proc
237 .input
238 .as_ref()
239 .map(rust_type_to_ts)
240 .unwrap_or_else(|| "void".to_string());
241 let output_ts = proc
242 .output
243 .as_ref()
244 .map(rust_type_to_ts)
245 .unwrap_or_else(|| "void".to_string());
246 let _ = writeln!(
247 out,
248 " query(key: \"{}\", input: {}): Promise<{}>;",
249 proc.name, input_ts, output_ts,
250 );
251 }
252}
253
254fn generate_mutation_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
256 let (void_mutations, non_void_mutations): (Vec<_>, Vec<_>) = manifest
257 .procedures
258 .iter()
259 .filter(|p| p.kind == ProcedureKind::Mutation)
260 .partition(|p| is_void_input(p));
261
262 for proc in &void_mutations {
264 if preserve_docs && let Some(doc) = &proc.docs {
265 emit_jsdoc(doc, " ", out);
266 }
267 let output_ts = proc
268 .output
269 .as_ref()
270 .map(rust_type_to_ts)
271 .unwrap_or_else(|| "void".to_string());
272 let _ = writeln!(
273 out,
274 " mutate(key: \"{}\"): Promise<{}>;",
275 proc.name, output_ts,
276 );
277 }
278
279 for proc in &non_void_mutations {
281 if preserve_docs && let Some(doc) = &proc.docs {
282 emit_jsdoc(doc, " ", out);
283 }
284 let input_ts = proc
285 .input
286 .as_ref()
287 .map(rust_type_to_ts)
288 .unwrap_or_else(|| "void".to_string());
289 let output_ts = proc
290 .output
291 .as_ref()
292 .map(rust_type_to_ts)
293 .unwrap_or_else(|| "void".to_string());
294 let _ = writeln!(
295 out,
296 " mutate(key: \"{}\", input: {}): Promise<{}>;",
297 proc.name, input_ts, output_ts,
298 );
299 }
300}
301
302fn is_void_input(proc: &Procedure) -> bool {
304 proc.input.as_ref().is_none_or(|ty| ty.name == "()")
305}