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.
139pub struct CliOverrides {
140    pub config: Option<PathBuf>,
141    pub no_config: bool,
142    // input
143    pub dir: Option<PathBuf>,
144    pub include: Vec<String>,
145    pub exclude: Vec<String>,
146    // output
147    pub output: Option<PathBuf>,
148    pub client_output: Option<PathBuf>,
149    pub types_import: Option<String>,
150    pub extension: Option<String>,
151    // codegen
152    pub preserve_docs: bool,
153    pub fields: Option<FieldNaming>,
154    // watch
155    pub debounce_ms: Option<u64>,
156    pub clear_screen: bool,
157}
158
159/// Resolve config: discover/load the file, then apply CLI overrides.
160pub fn resolve(cli: &CliOverrides) -> Result<RpcConfig> {
161    let mut config = if cli.no_config {
162        RpcConfig::default()
163    } else if let Some(path) = &cli.config {
164        load(path)?
165    } else {
166        let cwd = std::env::current_dir().context("Failed to get current directory")?;
167        match discover(&cwd) {
168            Some(path) => load(&path)?,
169            None => RpcConfig::default(),
170        }
171    };
172
173    // Apply CLI overrides
174    if let Some(dir) = &cli.dir {
175        config.input.dir = dir.clone();
176    }
177    if !cli.include.is_empty() {
178        config.input.include = cli.include.clone();
179    }
180    if !cli.exclude.is_empty() {
181        config.input.exclude = cli.exclude.clone();
182    }
183    if let Some(output) = &cli.output {
184        config.output.types = output.clone();
185    }
186    if let Some(client_output) = &cli.client_output {
187        config.output.client = client_output.clone();
188    }
189    if let Some(types_import) = &cli.types_import {
190        config.output.imports.types_path = types_import.clone();
191    }
192    if let Some(extension) = &cli.extension {
193        config.output.imports.extension = extension.clone();
194    }
195    if cli.preserve_docs {
196        config.codegen.preserve_docs = true;
197    }
198    if let Some(fields) = cli.fields {
199        config.codegen.naming.fields = fields;
200    }
201    if let Some(debounce_ms) = cli.debounce_ms {
202        config.watch.debounce_ms = debounce_ms;
203    }
204    if cli.clear_screen {
205        config.watch.clear_screen = true;
206    }
207
208    Ok(config)
209}