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