1use crate::model::{Manifest, Procedure, ProcedureKind};
9
10pub const GENERATED_HEADER: &str = "\
12// This file is auto-generated by metaxy-cli. Do not edit manually.
13// Re-run `metaxy generate` or use `metaxy 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 stream_fn_name: &'a str,
32
33 pub input_as_getter: bool,
36
37 pub query_interfaces: &'a [&'a str],
39
40 pub mutation_interfaces: &'a [&'a str],
42
43 pub stream_interfaces: &'a [&'a str],
45
46 pub query_impl: &'a str,
48
49 pub mutation_impl: &'a str,
51
52 pub stream_impl: &'a str,
54}
55
56pub fn generate_framework_file(
60 manifest: &Manifest,
61 client_import_path: &str,
62 types_import_path: &str,
63 _preserve_docs: bool,
64 config: &FrameworkConfig<'_>,
65) -> String {
66 let queries: Vec<_> = manifest
67 .procedures
68 .iter()
69 .filter(|p| p.kind == ProcedureKind::Query)
70 .collect();
71 let mutations: Vec<_> = manifest
72 .procedures
73 .iter()
74 .filter(|p| p.kind == ProcedureKind::Mutation)
75 .collect();
76 let streams: Vec<_> = manifest
77 .procedures
78 .iter()
79 .filter(|p| p.kind == ProcedureKind::Stream)
80 .collect();
81
82 if queries.is_empty() && mutations.is_empty() && streams.is_empty() {
83 return String::new();
84 }
85
86 let has_queries = !queries.is_empty();
87 let has_mutations = !mutations.is_empty();
88 let has_streams = !streams.is_empty();
89
90 let mut out = String::with_capacity(4096);
91
92 out.push_str(GENERATED_HEADER);
94 out.push('\n');
95
96 if let Some(import) = config.framework_import {
98 emit!(out, "{import}\n");
99 }
100
101 emit!(
103 out,
104 "import {{ type RpcClient, RpcError, type CallOptions }} from \"{client_import_path}\";\n"
105 );
106
107 let type_names: Vec<&str> = manifest
109 .structs
110 .iter()
111 .map(|s| s.name.as_str())
112 .chain(manifest.enums.iter().map(|e| e.name.as_str()))
113 .collect();
114
115 emit_types_import(&mut out, &type_names, types_import_path);
116 emit_re_exports(&mut out, &type_names);
117
118 emit_type_helpers(&mut out, has_queries, has_mutations, has_streams);
120 out.push('\n');
121
122 emit_key_unions_and_args(
124 &mut out,
125 &queries,
126 &mutations,
127 &streams,
128 has_queries,
129 has_mutations,
130 has_streams,
131 );
132 out.push('\n');
133
134 if has_queries {
136 for iface in config.query_interfaces {
137 emit!(out, "{iface}\n");
138 }
139 }
140 if has_mutations {
141 for iface in config.mutation_interfaces {
142 emit!(out, "{iface}\n");
143 }
144 }
145 if has_streams {
146 for iface in config.stream_interfaces {
147 emit!(out, "{iface}\n");
148 }
149 }
150
151 if has_queries {
153 let void_names: Vec<_> = queries
154 .iter()
155 .filter(|p| is_void_input(p))
156 .map(|p| format!("\"{}\"", p.name))
157 .collect();
158 emit!(
159 out,
160 "const VOID_QUERY_KEYS: Set<QueryKey> = new Set([{}]);\n",
161 void_names.join(", ")
162 );
163 emit_query_overloads(
164 &queries,
165 config.query_fn_name,
166 config.input_as_getter,
167 &mut out,
168 );
169 emit!(out, "{}\n", config.query_impl);
170 }
171
172 if has_mutations {
174 emit!(out, "{}\n", config.mutation_impl);
175 }
176
177 if has_streams {
179 let void_names: Vec<_> = streams
180 .iter()
181 .filter(|p| is_void_input(p))
182 .map(|p| format!("\"{}\"", p.name))
183 .collect();
184 emit!(
185 out,
186 "const VOID_STREAM_KEYS: Set<StreamKey> = new Set([{}]);\n",
187 void_names.join(", ")
188 );
189 emit_stream_overloads(
190 &streams,
191 config.stream_fn_name,
192 config.input_as_getter,
193 &mut out,
194 );
195 emit!(out, "{}\n", config.stream_impl);
196 }
197
198 out
199}
200
201fn emit_types_import(out: &mut String, type_names: &[&str], types_import_path: &str) {
203 if type_names.is_empty() {
204 emit!(
205 out,
206 "import type {{ Procedures }} from \"{types_import_path}\";\n"
207 );
208 } else {
209 let types_csv = type_names.join(", ");
210 emit!(
211 out,
212 "import type {{ Procedures, {types_csv} }} from \"{types_import_path}\";\n"
213 );
214 }
215}
216
217fn emit_re_exports(out: &mut String, type_names: &[&str]) {
219 emit!(out, "export {{ RpcError }};");
220 if type_names.is_empty() {
221 emit!(
222 out,
223 "export type {{ RpcClient, CallOptions, Procedures }};\n"
224 );
225 } else {
226 let types_csv = type_names.join(", ");
227 emit!(
228 out,
229 "export type {{ RpcClient, CallOptions, Procedures, {types_csv} }};\n"
230 );
231 }
232}
233
234fn emit_type_helpers(out: &mut String, has_queries: bool, has_mutations: bool, has_streams: bool) {
236 if has_queries {
237 emit!(out, "type QueryKey = keyof Procedures[\"queries\"];");
238 emit!(
239 out,
240 "type QueryInput<K extends QueryKey> = Procedures[\"queries\"][K][\"input\"];"
241 );
242 emit!(
243 out,
244 "type QueryOutput<K extends QueryKey> = Procedures[\"queries\"][K][\"output\"];"
245 );
246 }
247 if has_mutations {
248 emit!(out, "type MutationKey = keyof Procedures[\"mutations\"];");
249 emit!(
250 out,
251 "type MutationInput<K extends MutationKey> = Procedures[\"mutations\"][K][\"input\"];"
252 );
253 emit!(
254 out,
255 "type MutationOutput<K extends MutationKey> = Procedures[\"mutations\"][K][\"output\"];"
256 );
257 }
258 if has_streams {
259 emit!(out, "type StreamKey = keyof Procedures[\"streams\"];");
260 emit!(
261 out,
262 "type StreamInput<K extends StreamKey> = Procedures[\"streams\"][K][\"input\"];"
263 );
264 emit!(
265 out,
266 "type StreamOutput<K extends StreamKey> = Procedures[\"streams\"][K][\"output\"];"
267 );
268 }
269}
270
271fn emit_key_unions_and_args(
273 out: &mut String,
274 queries: &[&Procedure],
275 mutations: &[&Procedure],
276 streams: &[&Procedure],
277 has_queries: bool,
278 has_mutations: bool,
279 has_streams: bool,
280) {
281 if has_queries {
282 let void_queries: Vec<_> = queries.iter().filter(|p| is_void_input(p)).collect();
283 let non_void_queries: Vec<_> = queries.iter().filter(|p| !is_void_input(p)).collect();
284
285 if !void_queries.is_empty() {
286 let names: Vec<_> = void_queries
287 .iter()
288 .map(|p| format!("\"{}\"", p.name))
289 .collect();
290 emit!(out, "type VoidQueryKey = {};", names.join(" | "));
291 }
292 if !non_void_queries.is_empty() {
293 let names: Vec<_> = non_void_queries
294 .iter()
295 .map(|p| format!("\"{}\"", p.name))
296 .collect();
297 emit!(out, "type NonVoidQueryKey = {};", names.join(" | "));
298 }
299 }
300
301 if has_mutations {
302 let void_mutations: Vec<_> = mutations.iter().filter(|p| is_void_input(p)).collect();
303 let non_void_mutations: Vec<_> = mutations.iter().filter(|p| !is_void_input(p)).collect();
304
305 if !void_mutations.is_empty() {
306 let names: Vec<_> = void_mutations
307 .iter()
308 .map(|p| format!("\"{}\"", p.name))
309 .collect();
310 emit!(out, "type VoidMutationKey = {};", names.join(" | "));
311 }
312 if !non_void_mutations.is_empty() {
313 let names: Vec<_> = non_void_mutations
314 .iter()
315 .map(|p| format!("\"{}\"", p.name))
316 .collect();
317 emit!(out, "type NonVoidMutationKey = {};", names.join(" | "));
318 }
319
320 let all_void = non_void_mutations.is_empty();
321 let all_non_void = void_mutations.is_empty();
322 if all_void {
323 emit!(out, "type MutationArgs<K extends MutationKey> = [];");
324 } else if all_non_void {
325 emit!(
326 out,
327 "type MutationArgs<K extends MutationKey> = [input: MutationInput<K>];"
328 );
329 } else {
330 emit!(
331 out,
332 "type MutationArgs<K extends MutationKey> = K extends VoidMutationKey ? [] : [input: MutationInput<K>];"
333 );
334 }
335 }
336
337 if has_streams {
338 let void_streams: Vec<_> = streams.iter().filter(|p| is_void_input(p)).collect();
339 let non_void_streams: Vec<_> = streams.iter().filter(|p| !is_void_input(p)).collect();
340
341 if !void_streams.is_empty() {
342 let names: Vec<_> = void_streams
343 .iter()
344 .map(|p| format!("\"{}\"", p.name))
345 .collect();
346 emit!(out, "type VoidStreamKey = {};", names.join(" | "));
347 }
348 if !non_void_streams.is_empty() {
349 let names: Vec<_> = non_void_streams
350 .iter()
351 .map(|p| format!("\"{}\"", p.name))
352 .collect();
353 emit!(out, "type NonVoidStreamKey = {};", names.join(" | "));
354 }
355 }
356}
357
358fn emit_query_overloads(
363 queries: &[&Procedure],
364 fn_name: &str,
365 input_as_getter: bool,
366 out: &mut String,
367) {
368 let (void_queries, non_void_queries): (Vec<&&Procedure>, Vec<&&Procedure>) =
369 queries.iter().partition(|p| is_void_input(p));
370
371 for proc in &void_queries {
372 emit!(
373 out,
374 "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, options?: QueryOptions<K> | (() => QueryOptions<K>)): QueryResult<K>;",
375 proc.name,
376 );
377 }
378
379 let input_type = if input_as_getter {
380 "() => QueryInput<K>"
381 } else {
382 "QueryInput<K>"
383 };
384
385 for proc in &non_void_queries {
386 emit!(
387 out,
388 "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, input: {input_type}, options?: QueryOptions<K> | (() => QueryOptions<K>)): QueryResult<K>;",
389 proc.name,
390 );
391 }
392}
393
394fn emit_stream_overloads(
396 streams: &[&Procedure],
397 fn_name: &str,
398 input_as_getter: bool,
399 out: &mut String,
400) {
401 let (void_streams, non_void_streams): (Vec<&&Procedure>, Vec<&&Procedure>) =
402 streams.iter().partition(|p| is_void_input(p));
403
404 for proc in &void_streams {
405 emit!(
406 out,
407 "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, options?: StreamOptions<K>): StreamResult<K>;",
408 proc.name,
409 );
410 }
411
412 let input_type = if input_as_getter {
413 "() => StreamInput<K>"
414 } else {
415 "StreamInput<K>"
416 };
417
418 for proc in &non_void_streams {
419 emit!(
420 out,
421 "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, input: {input_type}, options?: StreamOptions<K>): StreamResult<K>;",
422 proc.name,
423 );
424 }
425}