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 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
116pub 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
129pub 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
138pub struct CliOverrides {
140 pub config: Option<PathBuf>,
141 pub no_config: bool,
142 pub dir: Option<PathBuf>,
144 pub include: Vec<String>,
145 pub exclude: Vec<String>,
146 pub output: Option<PathBuf>,
148 pub client_output: Option<PathBuf>,
149 pub types_import: Option<String>,
150 pub extension: Option<String>,
151 pub preserve_docs: bool,
153 pub fields: Option<FieldNaming>,
154 pub debounce_ms: Option<u64>,
156 pub clear_screen: bool,
157}
158
159pub 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 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}