Skip to main content

vercel_rpc_cli/
config.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4use serde::Deserialize;
5
6pub const CONFIG_FILE_NAME: &str = "rpc.config.toml";
7
8#[derive(Debug, Default, Deserialize)]
9#[serde(default)]
10pub struct RpcConfig {
11    pub input: InputConfig,
12    pub output: OutputConfig,
13    pub codegen: CodegenConfig,
14    pub watch: WatchConfig,
15}
16
17#[derive(Debug, Deserialize)]
18#[serde(default)]
19pub struct InputConfig {
20    pub dir: PathBuf,
21    pub include: Vec<String>,
22    pub exclude: Vec<String>,
23}
24
25#[derive(Debug, Deserialize)]
26#[serde(default)]
27pub struct OutputConfig {
28    pub types: PathBuf,
29    pub client: PathBuf,
30    pub svelte: Option<PathBuf>,
31    pub react: Option<PathBuf>,
32    pub vue: Option<PathBuf>,
33    pub solid: Option<PathBuf>,
34    pub imports: ImportsConfig,
35}
36
37#[derive(Debug, Deserialize)]
38#[serde(default)]
39pub struct ImportsConfig {
40    pub types_path: String,
41    pub extension: String,
42}
43
44#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, clap::ValueEnum)]
45pub enum FieldNaming {
46    #[default]
47    #[serde(rename = "preserve")]
48    #[value(name = "preserve")]
49    Preserve,
50    #[serde(rename = "camelCase")]
51    #[value(name = "camelCase")]
52    CamelCase,
53}
54
55#[derive(Debug, Default, Deserialize)]
56#[serde(default)]
57pub struct NamingConfig {
58    pub fields: FieldNaming,
59}
60
61#[derive(Debug, Default, Deserialize)]
62#[serde(default)]
63pub struct CodegenConfig {
64    pub preserve_docs: bool,
65    pub naming: NamingConfig,
66}
67
68#[derive(Debug, Deserialize)]
69#[serde(default)]
70pub struct WatchConfig {
71    pub debounce_ms: u64,
72    pub clear_screen: bool,
73}
74
75impl Default for InputConfig {
76    fn default() -> Self {
77        Self {
78            dir: PathBuf::from("api"),
79            include: vec!["**/*.rs".into()],
80            exclude: vec![],
81        }
82    }
83}
84
85impl Default for OutputConfig {
86    fn default() -> Self {
87        Self {
88            types: PathBuf::from("src/lib/rpc-types.ts"),
89            client: PathBuf::from("src/lib/rpc-client.ts"),
90            svelte: None,
91            react: None,
92            vue: None,
93            solid: None,
94            imports: ImportsConfig::default(),
95        }
96    }
97}
98
99impl Default for ImportsConfig {
100    fn default() -> Self {
101        Self {
102            types_path: "./rpc-types".to_string(),
103            extension: String::new(),
104        }
105    }
106}
107
108impl ImportsConfig {
109    /// Returns the full import specifier: `types_path` + `extension`.
110    pub fn types_specifier(&self) -> String {
111        format!("{}{}", self.types_path, self.extension)
112    }
113}
114
115impl Default for WatchConfig {
116    fn default() -> Self {
117        Self {
118            debounce_ms: 200,
119            clear_screen: false,
120        }
121    }
122}
123
124/// Walk up from `start` looking for `rpc.config.toml`.
125/// Returns `None` if not found.
126pub fn discover(start: &Path) -> Option<PathBuf> {
127    let mut dir = start;
128    loop {
129        let candidate = dir.join(CONFIG_FILE_NAME);
130        if candidate.is_file() {
131            return Some(candidate);
132        }
133        dir = dir.parent()?;
134    }
135}
136
137/// Read and parse a config file at the given path.
138pub fn load(path: &Path) -> Result<RpcConfig> {
139    let content = std::fs::read_to_string(path)
140        .with_context(|| format!("Failed to read config file {}", path.display()))?;
141    let config: RpcConfig =
142        toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
143    Ok(config)
144}
145
146/// CLI overrides that can be applied on top of a loaded config.
147#[derive(Default)]
148pub struct CliOverrides {
149    pub config: Option<PathBuf>,
150    pub no_config: bool,
151    // input
152    pub dir: Option<PathBuf>,
153    pub include: Vec<String>,
154    pub exclude: Vec<String>,
155    // output
156    pub output: Option<PathBuf>,
157    pub client_output: Option<PathBuf>,
158    pub svelte_output: Option<PathBuf>,
159    pub react_output: Option<PathBuf>,
160    pub vue_output: Option<PathBuf>,
161    pub solid_output: Option<PathBuf>,
162    pub types_import: Option<String>,
163    pub extension: Option<String>,
164    // codegen
165    pub preserve_docs: bool,
166    pub fields: Option<FieldNaming>,
167    // watch
168    pub debounce_ms: Option<u64>,
169    pub clear_screen: bool,
170}
171
172/// Resolve config: discover/load the file, then apply CLI overrides.
173pub fn resolve(cli: CliOverrides) -> Result<RpcConfig> {
174    let mut config = if cli.no_config {
175        RpcConfig::default()
176    } else if let Some(path) = &cli.config {
177        load(path)?
178    } else {
179        let cwd = std::env::current_dir().context("Failed to get current directory")?;
180        match discover(&cwd) {
181            Some(path) => load(&path)?,
182            None => RpcConfig::default(),
183        }
184    };
185
186    // Apply CLI overrides (move values instead of cloning)
187    if let Some(dir) = cli.dir {
188        config.input.dir = dir;
189    }
190    if !cli.include.is_empty() {
191        config.input.include = cli.include;
192    }
193    if !cli.exclude.is_empty() {
194        config.input.exclude = cli.exclude;
195    }
196    if let Some(output) = cli.output {
197        config.output.types = output;
198    }
199    if let Some(client_output) = cli.client_output {
200        config.output.client = client_output;
201    }
202    if let Some(svelte_output) = cli.svelte_output {
203        config.output.svelte = Some(svelte_output);
204    }
205    if let Some(react_output) = cli.react_output {
206        config.output.react = Some(react_output);
207    }
208    if let Some(vue_output) = cli.vue_output {
209        config.output.vue = Some(vue_output);
210    }
211    if let Some(solid_output) = cli.solid_output {
212        config.output.solid = Some(solid_output);
213    }
214    if let Some(types_import) = cli.types_import {
215        config.output.imports.types_path = types_import;
216    }
217    if let Some(extension) = cli.extension {
218        config.output.imports.extension = extension;
219    }
220    if cli.preserve_docs {
221        config.codegen.preserve_docs = true;
222    }
223    if let Some(fields) = cli.fields {
224        config.codegen.naming.fields = fields;
225    }
226    if let Some(debounce_ms) = cli.debounce_ms {
227        config.watch.debounce_ms = debounce_ms;
228    }
229    if cli.clear_screen {
230        config.watch.clear_screen = true;
231    }
232
233    Ok(config)
234}