1use crate::model::{Manifest, Procedure, ProcedureKind};
9
10pub const GENERATED_HEADER: &str = "\
12// This file is auto-generated by vercel-rpc-cli. Do not edit manually.
13// Re-run `rpc generate` or use `rpc watch` to regenerate.
14";
15
16pub fn is_void_input(proc: &Procedure) -> bool {
18 proc.input.as_ref().is_none_or(|ty| ty.name == "()")
19}
20
21pub struct FrameworkConfig<'a> {
23 pub framework_import: Option<&'a str>,
26
27 pub query_fn_name: &'a str,
29
30 pub input_as_getter: bool,
33
34 pub query_interfaces: &'a [&'a str],
36
37 pub mutation_interfaces: &'a [&'a str],
39
40 pub query_impl: &'a str,
42
43 pub mutation_impl: &'a str,
45}
46
47pub fn generate_framework_file(
51 manifest: &Manifest,
52 client_import_path: &str,
53 types_import_path: &str,
54 _preserve_docs: bool,
55 config: &FrameworkConfig<'_>,
56) -> String {
57 let queries: Vec<_> = manifest
58 .procedures
59 .iter()
60 .filter(|p| p.kind == ProcedureKind::Query)
61 .collect();
62 let mutations: Vec<_> = manifest
63 .procedures
64 .iter()
65 .filter(|p| p.kind == ProcedureKind::Mutation)
66 .collect();
67
68 if queries.is_empty() && mutations.is_empty() {
69 return String::new();
70 }
71
72 let has_queries = !queries.is_empty();
73 let has_mutations = !mutations.is_empty();
74
75 let mut out = String::with_capacity(4096);
76
77 out.push_str(GENERATED_HEADER);
79 out.push('\n');
80
81 if let Some(import) = config.framework_import {
83 emit!(out, "{import}\n");
84 }
85
86 emit!(
88 out,
89 "import {{ type RpcClient, RpcError, type CallOptions }} from \"{client_import_path}\";\n"
90 );
91
92 let type_names: Vec<&str> = manifest
94 .structs
95 .iter()
96 .map(|s| s.name.as_str())
97 .chain(manifest.enums.iter().map(|e| e.name.as_str()))
98 .collect();
99
100 emit_types_import(&mut out, &type_names, types_import_path);
101 emit_re_exports(&mut out, &type_names);
102
103 emit_type_helpers(&mut out, has_queries, has_mutations);
105 out.push('\n');
106
107 emit_key_unions_and_args(&mut out, &queries, &mutations, has_queries, has_mutations);
109 out.push('\n');
110
111 if has_queries {
113 for iface in config.query_interfaces {
114 emit!(out, "{iface}\n");
115 }
116 }
117 if has_mutations {
118 for iface in config.mutation_interfaces {
119 emit!(out, "{iface}\n");
120 }
121 }
122
123 if has_queries {
125 let void_names: Vec<_> = queries
126 .iter()
127 .filter(|p| is_void_input(p))
128 .map(|p| format!("\"{}\"", p.name))
129 .collect();
130 emit!(
131 out,
132 "const VOID_QUERY_KEYS: Set<QueryKey> = new Set([{}]);\n",
133 void_names.join(", ")
134 );
135 emit_query_overloads(
136 &queries,
137 config.query_fn_name,
138 config.input_as_getter,
139 &mut out,
140 );
141 emit!(out, "{}\n", config.query_impl);
142 }
143
144 if has_mutations {
146 emit!(out, "{}\n", config.mutation_impl);
147 }
148
149 out
150}
151
152fn emit_types_import(out: &mut String, type_names: &[&str], types_import_path: &str) {
154 if type_names.is_empty() {
155 emit!(
156 out,
157 "import type {{ Procedures }} from \"{types_import_path}\";\n"
158 );
159 } else {
160 let types_csv = type_names.join(", ");
161 emit!(
162 out,
163 "import type {{ Procedures, {types_csv} }} from \"{types_import_path}\";\n"
164 );
165 }
166}
167
168fn emit_re_exports(out: &mut String, type_names: &[&str]) {
170 emit!(out, "export {{ RpcError }};");
171 if type_names.is_empty() {
172 emit!(
173 out,
174 "export type {{ RpcClient, CallOptions, Procedures }};\n"
175 );
176 } else {
177 let types_csv = type_names.join(", ");
178 emit!(
179 out,
180 "export type {{ RpcClient, CallOptions, Procedures, {types_csv} }};\n"
181 );
182 }
183}
184
185fn emit_type_helpers(out: &mut String, has_queries: bool, has_mutations: bool) {
187 if has_queries {
188 emit!(out, "type QueryKey = keyof Procedures[\"queries\"];");
189 emit!(
190 out,
191 "type QueryInput<K extends QueryKey> = Procedures[\"queries\"][K][\"input\"];"
192 );
193 emit!(
194 out,
195 "type QueryOutput<K extends QueryKey> = Procedures[\"queries\"][K][\"output\"];"
196 );
197 }
198 if has_mutations {
199 emit!(out, "type MutationKey = keyof Procedures[\"mutations\"];");
200 emit!(
201 out,
202 "type MutationInput<K extends MutationKey> = Procedures[\"mutations\"][K][\"input\"];"
203 );
204 emit!(
205 out,
206 "type MutationOutput<K extends MutationKey> = Procedures[\"mutations\"][K][\"output\"];"
207 );
208 }
209}
210
211fn emit_key_unions_and_args(
213 out: &mut String,
214 queries: &[&Procedure],
215 mutations: &[&Procedure],
216 has_queries: bool,
217 has_mutations: bool,
218) {
219 if has_queries {
220 let void_queries: Vec<_> = queries.iter().filter(|p| is_void_input(p)).collect();
221 let non_void_queries: Vec<_> = queries.iter().filter(|p| !is_void_input(p)).collect();
222
223 if !void_queries.is_empty() {
224 let names: Vec<_> = void_queries
225 .iter()
226 .map(|p| format!("\"{}\"", p.name))
227 .collect();
228 emit!(out, "type VoidQueryKey = {};", names.join(" | "));
229 }
230 if !non_void_queries.is_empty() {
231 let names: Vec<_> = non_void_queries
232 .iter()
233 .map(|p| format!("\"{}\"", p.name))
234 .collect();
235 emit!(out, "type NonVoidQueryKey = {};", names.join(" | "));
236 }
237 }
238
239 if has_mutations {
240 let void_mutations: Vec<_> = mutations.iter().filter(|p| is_void_input(p)).collect();
241 let non_void_mutations: Vec<_> = mutations.iter().filter(|p| !is_void_input(p)).collect();
242
243 if !void_mutations.is_empty() {
244 let names: Vec<_> = void_mutations
245 .iter()
246 .map(|p| format!("\"{}\"", p.name))
247 .collect();
248 emit!(out, "type VoidMutationKey = {};", names.join(" | "));
249 }
250 if !non_void_mutations.is_empty() {
251 let names: Vec<_> = non_void_mutations
252 .iter()
253 .map(|p| format!("\"{}\"", p.name))
254 .collect();
255 emit!(out, "type NonVoidMutationKey = {};", names.join(" | "));
256 }
257
258 let all_void = non_void_mutations.is_empty();
259 let all_non_void = void_mutations.is_empty();
260 if all_void {
261 emit!(out, "type MutationArgs<K extends MutationKey> = [];");
262 } else if all_non_void {
263 emit!(
264 out,
265 "type MutationArgs<K extends MutationKey> = [input: MutationInput<K>];"
266 );
267 } else {
268 emit!(
269 out,
270 "type MutationArgs<K extends MutationKey> = K extends VoidMutationKey ? [] : [input: MutationInput<K>];"
271 );
272 }
273 }
274}
275
276fn emit_query_overloads(
281 queries: &[&Procedure],
282 fn_name: &str,
283 input_as_getter: bool,
284 out: &mut String,
285) {
286 let (void_queries, non_void_queries): (Vec<&&Procedure>, Vec<&&Procedure>) =
287 queries.iter().partition(|p| is_void_input(p));
288
289 for proc in &void_queries {
290 emit!(
291 out,
292 "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, options?: QueryOptions<K> | (() => QueryOptions<K>)): QueryResult<K>;",
293 proc.name,
294 );
295 }
296
297 let input_type = if input_as_getter {
298 "() => QueryInput<K>"
299 } else {
300 "QueryInput<K>"
301 };
302
303 for proc in &non_void_queries {
304 emit!(
305 out,
306 "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, input: {input_type}, options?: QueryOptions<K> | (() => QueryOptions<K>)): QueryResult<K>;",
307 proc.name,
308 );
309 }
310}