1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use serde::Deserialize;
6
7pub const CONFIG_FILE_NAME: &str = "rpc.config.toml";
8
9#[derive(Debug, Default, Deserialize)]
10#[serde(default)]
11pub struct RpcConfig {
12 pub input: InputConfig,
13 pub output: OutputConfig,
14 pub codegen: CodegenConfig,
15 pub watch: WatchConfig,
16}
17
18#[derive(Debug, Deserialize)]
19#[serde(default)]
20pub struct InputConfig {
21 pub dir: PathBuf,
22 pub include: Vec<String>,
23 pub exclude: Vec<String>,
24}
25
26#[derive(Debug, Deserialize)]
27#[serde(default)]
28pub struct OutputConfig {
29 pub types: PathBuf,
30 pub client: PathBuf,
31 pub svelte: Option<PathBuf>,
32 pub react: Option<PathBuf>,
33 pub vue: Option<PathBuf>,
34 pub solid: Option<PathBuf>,
35 pub imports: ImportsConfig,
36}
37
38#[derive(Debug, Deserialize)]
39#[serde(default)]
40pub struct ImportsConfig {
41 pub types_path: String,
42 pub extension: String,
43}
44
45#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, clap::ValueEnum)]
46pub enum FieldNaming {
47 #[default]
48 #[serde(rename = "preserve")]
49 #[value(name = "preserve")]
50 Preserve,
51 #[serde(rename = "camelCase")]
52 #[value(name = "camelCase")]
53 CamelCase,
54}
55
56#[derive(Debug, Default, Deserialize)]
57#[serde(default)]
58pub struct NamingConfig {
59 pub fields: FieldNaming,
60}
61
62#[derive(Debug, Default, Deserialize)]
63#[serde(default)]
64pub struct CodegenConfig {
65 pub preserve_docs: bool,
66 pub branded_newtypes: bool,
67 pub naming: NamingConfig,
68 pub type_overrides: HashMap<String, String>,
69 pub bigint_types: Vec<String>,
70}
71
72#[derive(Debug, Deserialize)]
73#[serde(default)]
74pub struct WatchConfig {
75 pub debounce_ms: u64,
76 pub clear_screen: bool,
77}
78
79impl Default for InputConfig {
80 fn default() -> Self {
81 Self {
82 dir: PathBuf::from("api"),
83 include: vec!["**/*.rs".into()],
84 exclude: vec![],
85 }
86 }
87}
88
89impl Default for OutputConfig {
90 fn default() -> Self {
91 Self {
92 types: PathBuf::from("src/lib/rpc-types.ts"),
93 client: PathBuf::from("src/lib/rpc-client.ts"),
94 svelte: None,
95 react: None,
96 vue: None,
97 solid: None,
98 imports: ImportsConfig::default(),
99 }
100 }
101}
102
103impl Default for ImportsConfig {
104 fn default() -> Self {
105 Self {
106 types_path: "./rpc-types".to_string(),
107 extension: String::new(),
108 }
109 }
110}
111
112impl ImportsConfig {
113 pub fn types_specifier(&self) -> String {
115 format!("{}{}", self.types_path, self.extension)
116 }
117}
118
119impl Default for WatchConfig {
120 fn default() -> Self {
121 Self {
122 debounce_ms: 200,
123 clear_screen: false,
124 }
125 }
126}
127
128pub fn discover(start: &Path) -> Option<PathBuf> {
131 let mut dir = start;
132 loop {
133 let candidate = dir.join(CONFIG_FILE_NAME);
134 if candidate.is_file() {
135 return Some(candidate);
136 }
137 dir = dir.parent()?;
138 }
139}
140
141pub fn load(path: &Path) -> Result<RpcConfig> {
143 let content = std::fs::read_to_string(path)
144 .with_context(|| format!("Failed to read config file {}", path.display()))?;
145 let config: RpcConfig =
146 toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
147 Ok(config)
148}
149
150#[derive(Default)]
152pub struct CliOverrides {
153 pub config: Option<PathBuf>,
154 pub no_config: bool,
155 pub dir: Option<PathBuf>,
157 pub include: Vec<String>,
158 pub exclude: Vec<String>,
159 pub output: Option<PathBuf>,
161 pub client_output: Option<PathBuf>,
162 pub svelte_output: Option<PathBuf>,
163 pub react_output: Option<PathBuf>,
164 pub vue_output: Option<PathBuf>,
165 pub solid_output: Option<PathBuf>,
166 pub types_import: Option<String>,
167 pub extension: Option<String>,
168 pub preserve_docs: bool,
170 pub branded_newtypes: Option<bool>,
171 pub fields: Option<FieldNaming>,
172 pub type_overrides: Vec<(String, String)>,
173 pub bigint_types: Vec<String>,
174 pub debounce_ms: Option<u64>,
176 pub clear_screen: bool,
177}
178
179pub fn resolve(cli: CliOverrides) -> Result<RpcConfig> {
181 let mut config = if cli.no_config {
182 RpcConfig::default()
183 } else if let Some(path) = &cli.config {
184 load(path)?
185 } else {
186 let cwd = std::env::current_dir().context("Failed to get current directory")?;
187 match discover(&cwd) {
188 Some(path) => load(&path)?,
189 None => RpcConfig::default(),
190 }
191 };
192
193 if let Some(dir) = cli.dir {
195 config.input.dir = dir;
196 }
197 if !cli.include.is_empty() {
198 config.input.include = cli.include;
199 }
200 if !cli.exclude.is_empty() {
201 config.input.exclude = cli.exclude;
202 }
203 if let Some(output) = cli.output {
204 config.output.types = output;
205 }
206 if let Some(client_output) = cli.client_output {
207 config.output.client = client_output;
208 }
209 if let Some(svelte_output) = cli.svelte_output {
210 config.output.svelte = Some(svelte_output);
211 }
212 if let Some(react_output) = cli.react_output {
213 config.output.react = Some(react_output);
214 }
215 if let Some(vue_output) = cli.vue_output {
216 config.output.vue = Some(vue_output);
217 }
218 if let Some(solid_output) = cli.solid_output {
219 config.output.solid = Some(solid_output);
220 }
221 if let Some(types_import) = cli.types_import {
222 config.output.imports.types_path = types_import;
223 }
224 if let Some(extension) = cli.extension {
225 config.output.imports.extension = extension;
226 }
227 if cli.preserve_docs {
228 config.codegen.preserve_docs = true;
229 }
230 if let Some(branded) = cli.branded_newtypes {
231 config.codegen.branded_newtypes = branded;
232 }
233 if let Some(fields) = cli.fields {
234 config.codegen.naming.fields = fields;
235 }
236 for (key, value) in cli.type_overrides {
237 config.codegen.type_overrides.insert(key, value);
238 }
239 if !cli.bigint_types.is_empty() {
240 config.codegen.bigint_types = cli.bigint_types;
241 }
242 if let Some(debounce_ms) = cli.debounce_ms {
243 config.watch.debounce_ms = debounce_ms;
244 }
245 if cli.clear_screen {
246 config.watch.clear_screen = true;
247 }
248
249 Ok(config)
250}