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
138#[derive(Default)]
140pub struct CliOverrides {
141 pub config: Option<PathBuf>,
142 pub no_config: bool,
143 pub dir: Option<PathBuf>,
145 pub include: Vec<String>,
146 pub exclude: Vec<String>,
147 pub output: Option<PathBuf>,
149 pub client_output: Option<PathBuf>,
150 pub types_import: Option<String>,
151 pub extension: Option<String>,
152 pub preserve_docs: bool,
154 pub fields: Option<FieldNaming>,
155 pub debounce_ms: Option<u64>,
157 pub clear_screen: bool,
158}
159
160pub 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 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}