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 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
124pub 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
137pub 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#[derive(Default)]
148pub struct CliOverrides {
149 pub config: Option<PathBuf>,
150 pub no_config: bool,
151 pub dir: Option<PathBuf>,
153 pub include: Vec<String>,
154 pub exclude: Vec<String>,
155 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 pub preserve_docs: bool,
166 pub fields: Option<FieldNaming>,
167 pub debounce_ms: Option<u64>,
169 pub clear_screen: bool,
170}
171
172pub 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 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}