1use serde::{Deserialize, Serialize};
2use serde_json::Value as JsonValue;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct ExcludeConfig {
8 #[serde(default)]
9 pub types: Vec<String>,
10 #[serde(default)]
11 pub functions: Vec<String>,
12 #[serde(default)]
14 pub methods: Vec<String>,
15}
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct IncludeConfig {
19 #[serde(default)]
20 pub types: Vec<String>,
21 #[serde(default)]
22 pub functions: Vec<String>,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct OutputConfig {
27 pub python: Option<PathBuf>,
28 pub node: Option<PathBuf>,
29 pub ruby: Option<PathBuf>,
30 pub php: Option<PathBuf>,
31 pub elixir: Option<PathBuf>,
32 pub wasm: Option<PathBuf>,
33 pub ffi: Option<PathBuf>,
34 pub go: Option<PathBuf>,
35 pub java: Option<PathBuf>,
36 pub csharp: Option<PathBuf>,
37 pub r: Option<PathBuf>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ScaffoldConfig {
42 pub description: Option<String>,
43 pub license: Option<String>,
44 pub repository: Option<String>,
45 pub homepage: Option<String>,
46 #[serde(default)]
47 pub authors: Vec<String>,
48 #[serde(default)]
49 pub keywords: Vec<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ReadmeConfig {
54 pub template_dir: Option<PathBuf>,
55 pub snippets_dir: Option<PathBuf>,
56 pub config: Option<PathBuf>,
58 pub output_pattern: Option<String>,
59 pub discord_url: Option<String>,
61 pub banner_url: Option<String>,
63 #[serde(default)]
67 pub languages: HashMap<String, JsonValue>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(untagged)]
75pub enum StringOrVec {
76 Single(String),
77 Multiple(Vec<String>),
78}
79
80impl StringOrVec {
81 pub fn commands(&self) -> Vec<&str> {
83 match self {
84 StringOrVec::Single(s) => vec![s.as_str()],
85 StringOrVec::Multiple(v) => v.iter().map(String::as_str).collect(),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LintConfig {
92 pub format: Option<StringOrVec>,
93 pub check: Option<StringOrVec>,
94 pub typecheck: Option<StringOrVec>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct UpdateConfig {
99 pub update: Option<StringOrVec>,
101 pub upgrade: Option<StringOrVec>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct TestConfig {
107 pub command: Option<StringOrVec>,
109 pub e2e: Option<StringOrVec>,
111 pub coverage: Option<StringOrVec>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct SetupConfig {
117 pub install: Option<StringOrVec>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct CleanConfig {
123 pub clean: Option<StringOrVec>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct BuildCommandConfig {
129 pub build: Option<StringOrVec>,
131 pub build_release: Option<StringOrVec>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct TextReplacement {
138 pub path: String,
140 pub search: String,
142 pub replace: String,
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn string_or_vec_single_from_toml() {
152 let toml_str = r#"format = "ruff format""#;
153 #[derive(Deserialize)]
154 struct T {
155 format: StringOrVec,
156 }
157 let t: T = toml::from_str(toml_str).unwrap();
158 assert_eq!(t.format.commands(), vec!["ruff format"]);
159 }
160
161 #[test]
162 fn string_or_vec_multiple_from_toml() {
163 let toml_str = r#"format = ["cmd1", "cmd2", "cmd3"]"#;
164 #[derive(Deserialize)]
165 struct T {
166 format: StringOrVec,
167 }
168 let t: T = toml::from_str(toml_str).unwrap();
169 assert_eq!(t.format.commands(), vec!["cmd1", "cmd2", "cmd3"]);
170 }
171
172 #[test]
173 fn lint_config_backward_compat_string() {
174 let toml_str = r#"
175format = "ruff format ."
176check = "ruff check ."
177typecheck = "mypy ."
178"#;
179 let cfg: LintConfig = toml::from_str(toml_str).unwrap();
180 assert_eq!(cfg.format.unwrap().commands(), vec!["ruff format ."]);
181 assert_eq!(cfg.check.unwrap().commands(), vec!["ruff check ."]);
182 assert_eq!(cfg.typecheck.unwrap().commands(), vec!["mypy ."]);
183 }
184
185 #[test]
186 fn lint_config_array_commands() {
187 let toml_str = r#"
188format = ["cmd1", "cmd2"]
189check = "single-check"
190"#;
191 let cfg: LintConfig = toml::from_str(toml_str).unwrap();
192 assert_eq!(cfg.format.unwrap().commands(), vec!["cmd1", "cmd2"]);
193 assert_eq!(cfg.check.unwrap().commands(), vec!["single-check"]);
194 assert!(cfg.typecheck.is_none());
195 }
196
197 #[test]
198 fn lint_config_all_optional() {
199 let toml_str = "";
200 let cfg: LintConfig = toml::from_str(toml_str).unwrap();
201 assert!(cfg.format.is_none());
202 assert!(cfg.check.is_none());
203 assert!(cfg.typecheck.is_none());
204 }
205
206 #[test]
207 fn update_config_from_toml() {
208 let toml_str = r#"
209update = "cargo update"
210upgrade = ["cargo upgrade --incompatible", "cargo update"]
211"#;
212 let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
213 assert_eq!(cfg.update.unwrap().commands(), vec!["cargo update"]);
214 assert_eq!(
215 cfg.upgrade.unwrap().commands(),
216 vec!["cargo upgrade --incompatible", "cargo update"]
217 );
218 }
219
220 #[test]
221 fn update_config_all_optional() {
222 let toml_str = "";
223 let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
224 assert!(cfg.update.is_none());
225 assert!(cfg.upgrade.is_none());
226 }
227
228 #[test]
229 fn string_or_vec_empty_array_from_toml() {
230 let toml_str = "format = []";
231 #[derive(Deserialize)]
232 struct T {
233 format: StringOrVec,
234 }
235 let t: T = toml::from_str(toml_str).unwrap();
236 assert!(matches!(t.format, StringOrVec::Multiple(_)));
237 assert!(t.format.commands().is_empty());
238 }
239
240 #[test]
241 fn string_or_vec_single_element_array_from_toml() {
242 let toml_str = r#"format = ["cmd"]"#;
243 #[derive(Deserialize)]
244 struct T {
245 format: StringOrVec,
246 }
247 let t: T = toml::from_str(toml_str).unwrap();
248 assert_eq!(t.format.commands(), vec!["cmd"]);
249 }
250
251 #[test]
252 fn setup_config_single_string() {
253 let toml_str = r#"install = "uv sync""#;
254 let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
255 assert_eq!(cfg.install.unwrap().commands(), vec!["uv sync"]);
256 }
257
258 #[test]
259 fn setup_config_array_commands() {
260 let toml_str = r#"install = ["step1", "step2"]"#;
261 let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
262 assert_eq!(cfg.install.unwrap().commands(), vec!["step1", "step2"]);
263 }
264
265 #[test]
266 fn setup_config_all_optional() {
267 let toml_str = "";
268 let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
269 assert!(cfg.install.is_none());
270 }
271
272 #[test]
273 fn clean_config_single_string() {
274 let toml_str = r#"clean = "rm -rf dist""#;
275 let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
276 assert_eq!(cfg.clean.unwrap().commands(), vec!["rm -rf dist"]);
277 }
278
279 #[test]
280 fn clean_config_array_commands() {
281 let toml_str = r#"clean = ["step1", "step2"]"#;
282 let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
283 assert_eq!(cfg.clean.unwrap().commands(), vec!["step1", "step2"]);
284 }
285
286 #[test]
287 fn clean_config_all_optional() {
288 let toml_str = "";
289 let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
290 assert!(cfg.clean.is_none());
291 }
292
293 #[test]
294 fn build_command_config_single_strings() {
295 let toml_str = r#"
296build = "cargo build"
297build_release = "cargo build --release"
298"#;
299 let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
300 assert_eq!(cfg.build.unwrap().commands(), vec!["cargo build"]);
301 assert_eq!(cfg.build_release.unwrap().commands(), vec!["cargo build --release"]);
302 }
303
304 #[test]
305 fn build_command_config_array_commands() {
306 let toml_str = r#"
307build = ["step1", "step2"]
308build_release = ["step1 --release", "step2 --release"]
309"#;
310 let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
311 assert_eq!(cfg.build.unwrap().commands(), vec!["step1", "step2"]);
312 assert_eq!(
313 cfg.build_release.unwrap().commands(),
314 vec!["step1 --release", "step2 --release"]
315 );
316 }
317
318 #[test]
319 fn build_command_config_all_optional() {
320 let toml_str = "";
321 let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
322 assert!(cfg.build.is_none());
323 assert!(cfg.build_release.is_none());
324 }
325
326 #[test]
327 fn test_config_backward_compat_string() {
328 let toml_str = r#"command = "pytest""#;
329 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
330 assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
331 assert!(cfg.e2e.is_none());
332 assert!(cfg.coverage.is_none());
333 }
334
335 #[test]
336 fn test_config_array_command() {
337 let toml_str = r#"command = ["cmd1", "cmd2"]"#;
338 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
339 assert_eq!(cfg.command.unwrap().commands(), vec!["cmd1", "cmd2"]);
340 }
341
342 #[test]
343 fn test_config_with_coverage() {
344 let toml_str = r#"
345command = "pytest"
346coverage = "pytest --cov=. --cov-report=term-missing"
347"#;
348 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
349 assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
350 assert_eq!(
351 cfg.coverage.unwrap().commands(),
352 vec!["pytest --cov=. --cov-report=term-missing"]
353 );
354 assert!(cfg.e2e.is_none());
355 }
356
357 #[test]
358 fn test_config_all_optional() {
359 let toml_str = "";
360 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
361 assert!(cfg.command.is_none());
362 assert!(cfg.e2e.is_none());
363 assert!(cfg.coverage.is_none());
364 }
365
366 #[test]
367 fn full_alef_toml_with_lint_and_update() {
368 let toml_str = r#"
369languages = ["python", "node"]
370
371[crate]
372name = "test"
373sources = ["src/lib.rs"]
374
375[lint.python]
376format = "ruff format ."
377check = "ruff check --fix ."
378
379[lint.node]
380format = ["npx oxfmt", "npx oxlint --fix"]
381
382[update.python]
383update = "uv sync --upgrade"
384upgrade = "uv sync --all-packages --all-extras --upgrade"
385
386[update.node]
387update = "pnpm up -r"
388upgrade = ["corepack up", "pnpm up --latest -r -w"]
389"#;
390 let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
391 let lint_map = cfg.lint.as_ref().unwrap();
392 assert!(lint_map.contains_key("python"));
393 assert!(lint_map.contains_key("node"));
394
395 let py_lint = lint_map.get("python").unwrap();
396 assert_eq!(py_lint.format.as_ref().unwrap().commands(), vec!["ruff format ."]);
397
398 let node_lint = lint_map.get("node").unwrap();
399 assert_eq!(
400 node_lint.format.as_ref().unwrap().commands(),
401 vec!["npx oxfmt", "npx oxlint --fix"]
402 );
403
404 let update_map = cfg.update.as_ref().unwrap();
405 assert!(update_map.contains_key("python"));
406 assert!(update_map.contains_key("node"));
407
408 let node_update = update_map.get("node").unwrap();
409 assert_eq!(node_update.update.as_ref().unwrap().commands(), vec!["pnpm up -r"]);
410 assert_eq!(
411 node_update.upgrade.as_ref().unwrap().commands(),
412 vec!["corepack up", "pnpm up --latest -r -w"]
413 );
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize, Default)]
419pub struct SyncConfig {
420 #[serde(default)]
422 pub extra_paths: Vec<String>,
423 #[serde(default)]
425 pub text_replacements: Vec<TextReplacement>,
426}