Skip to main content

metaxy_cli/codegen/
common.rs

1//! Shared logic for framework-specific codegen files (React, Svelte, Vue, SolidJS).
2//!
3//! Each framework wrapper follows the same structure: imports, type helpers,
4//! void/non-void key unions, interface definitions, query/mutation overloads,
5//! and implementation blocks. This module extracts all the common patterns
6//! so that individual framework modules only supply their unique constants.
7
8use crate::model::{Manifest, Procedure, ProcedureKind};
9
10/// Header comment included at the top of every generated file.
11pub 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
16/// Returns `true` if the procedure takes no input (void).
17pub fn is_void_input(proc: &Procedure) -> bool {
18    proc.input.as_ref().is_none_or(|ty| ty.name == "()")
19}
20
21/// Configuration for generating a framework-specific reactive wrapper file.
22pub struct FrameworkConfig<'a> {
23    /// Framework-specific import line (e.g. `import { useState, ... } from "react";`).
24    /// `None` for Svelte which has no framework import.
25    pub framework_import: Option<&'a str>,
26
27    /// Name of the query function (e.g. `"useQuery"` or `"createQuery"`).
28    pub query_fn_name: &'a str,
29
30    /// Name of the stream function (e.g. `"useStream"` or `"createStream"`).
31    pub stream_fn_name: &'a str,
32
33    /// Whether non-void query input is a getter `() => QueryInput<K>` (Svelte/Vue/Solid)
34    /// or a direct value `QueryInput<K>` (React).
35    pub input_as_getter: bool,
36
37    /// TypeScript interface constants for queries (options, result).
38    pub query_interfaces: &'a [&'a str],
39
40    /// TypeScript interface constants for mutations (options, result).
41    pub mutation_interfaces: &'a [&'a str],
42
43    /// TypeScript interface constants for streams (options, result).
44    pub stream_interfaces: &'a [&'a str],
45
46    /// Query implementation block (TypeScript source).
47    pub query_impl: &'a str,
48
49    /// Mutation implementation block (TypeScript source).
50    pub mutation_impl: &'a str,
51
52    /// Stream implementation block (TypeScript source).
53    pub stream_impl: &'a str,
54}
55
56/// Generates a complete framework-specific reactive wrapper file.
57///
58/// Returns an empty string when the manifest contains no procedures.
59pub 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    // Header
93    out.push_str(GENERATED_HEADER);
94    out.push('\n');
95
96    // Framework import (if any)
97    if let Some(import) = config.framework_import {
98        emit!(out, "{import}\n");
99    }
100
101    // Client import
102    emit!(
103        out,
104        "import {{ type RpcClient, RpcError, type CallOptions }} from \"{client_import_path}\";\n"
105    );
106
107    // Types import + re-exports
108    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    // Type helpers
119    emit_type_helpers(&mut out, has_queries, has_mutations, has_streams);
120    out.push('\n');
121
122    // Void/non-void key unions + MutationArgs
123    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    // Interfaces
135    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    // Query overloads + implementation
152    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    // Mutation implementation
173    if has_mutations {
174        emit!(out, "{}\n", config.mutation_impl);
175    }
176
177    // Stream overloads + implementation
178    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
201/// Emits the `import type { Procedures, ... }` line.
202fn 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
217/// Emits re-exports: `export { RpcError }` and `export type { ... }`.
218fn 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
234/// Emits QueryKey/MutationKey/StreamKey type aliases and their Input/Output helpers.
235fn 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
271/// Emits VoidQueryKey/NonVoidQueryKey unions and the MutationArgs conditional type.
272fn 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
358/// Emits overload signatures for the query function.
359///
360/// `fn_name` is the function name (e.g. `"useQuery"` or `"createQuery"`).
361/// When `input_as_getter` is true, non-void input is `() => QueryInput<K>`.
362fn 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
394/// Emits overload signatures for the stream function.
395fn 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}