1use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum OutputFormat {
12 Human,
13 Json,
14 Sarif,
15}
16
17impl OutputFormat {
18 pub fn from_str(s: &str) -> Result<Self> {
20 match s.to_lowercase().as_str() {
21 "human" => Ok(Self::Human),
22 "json" => Ok(Self::Json),
23 "sarif" => Ok(Self::Sarif),
24 _ => Err(Error::Config(format!("Invalid output format: {}", s))),
25 }
26 }
27}
28
29impl Default for OutputFormat {
30 fn default() -> Self {
31 Self::Human
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct GeneralConfig {
38 #[serde(default)]
40 pub strict: bool,
41
42 #[serde(default)]
44 pub fail_fast: bool,
45
46 #[serde(default = "default_max_issues")]
48 pub max_issues: usize,
49}
50
51impl Default for GeneralConfig {
52 fn default() -> Self {
53 Self {
54 strict: false,
55 fail_fast: false,
56 max_issues: default_max_issues(),
57 }
58 }
59}
60
61fn default_max_issues() -> usize {
62 100
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TailwindConfig {
68 #[serde(default = "default_true")]
70 pub enabled: bool,
71
72 #[serde(default = "default_tailwind_config")]
74 pub config_path: PathBuf,
75
76 #[serde(default)]
78 pub safe_list: Vec<String>,
79
80 #[serde(default)]
82 pub block_list: Vec<String>,
83
84 #[serde(default = "default_max_arbitrary")]
86 pub max_arbitrary_values: usize,
87}
88
89impl Default for TailwindConfig {
90 fn default() -> Self {
91 Self {
92 enabled: true,
93 config_path: default_tailwind_config(),
94 safe_list: Vec::new(),
95 block_list: Vec::new(),
96 max_arbitrary_values: default_max_arbitrary(),
97 }
98 }
99}
100
101fn default_tailwind_config() -> PathBuf {
102 PathBuf::from("tailwind.config.ts")
103}
104
105fn default_max_arbitrary() -> usize {
106 5
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ImportConfig {
112 #[serde(default = "default_true")]
114 pub enforce_alias: bool,
115
116 #[serde(default)]
118 pub alias_map: HashMap<String, String>,
119
120 #[serde(default)]
122 pub require_extensions: bool,
123}
124
125impl Default for ImportConfig {
126 fn default() -> Self {
127 Self {
128 enforce_alias: true,
129 alias_map: HashMap::new(),
130 require_extensions: false,
131 }
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ComponentConfig {
138 #[serde(default = "default_true")]
140 pub enforce_shadcn: bool,
141
142 #[serde(default = "default_shadcn_path")]
144 pub shadcn_path: String,
145}
146
147impl Default for ComponentConfig {
148 fn default() -> Self {
149 Self {
150 enforce_shadcn: true,
151 shadcn_path: default_shadcn_path(),
152 }
153 }
154}
155
156fn default_shadcn_path() -> String {
157 "@/components/ui".to_string()
158}
159
160fn default_true() -> bool {
161 true
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RustConfig {
167 #[serde(default = "default_true")]
169 pub enabled: bool,
170
171 #[serde(default)]
173 pub deny_unsafe: Option<String>,
174
175 #[serde(default = "default_true")]
177 pub warn_unwrap: bool,
178
179 #[serde(default = "default_true")]
181 pub enforce_result_handling: bool,
182}
183
184impl Default for RustConfig {
185 fn default() -> Self {
186 Self {
187 enabled: true,
188 deny_unsafe: None,
189 warn_unwrap: true,
190 enforce_result_handling: true,
191 }
192 }
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct NextJsConfig {
198 #[serde(default = "default_true")]
200 pub enabled: bool,
201
202 #[serde(default = "default_true")]
204 pub enforce_app_router: bool,
205
206 #[serde(default = "default_true")]
208 pub validate_page_exports: bool,
209}
210
211impl Default for NextJsConfig {
212 fn default() -> Self {
213 Self {
214 enabled: true,
215 enforce_app_router: true,
216 validate_page_exports: true,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct NestJsConfig {
224 #[serde(default = "default_true")]
226 pub enabled: bool,
227
228 #[serde(default = "default_true")]
230 pub enforce_decorators: bool,
231
232 #[serde(default = "default_true")]
234 pub validate_modules: bool,
235}
236
237impl Default for NestJsConfig {
238 fn default() -> Self {
239 Self {
240 enabled: true,
241 enforce_decorators: true,
242 validate_modules: true,
243 }
244 }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct OutputConfig {
250 #[serde(default)]
252 pub format: OutputFormat,
253
254 #[serde(default = "default_true")]
256 pub show_paths: bool,
257
258 #[serde(default)]
260 pub color: String,
261}
262
263impl Default for OutputConfig {
264 fn default() -> Self {
265 Self {
266 format: OutputFormat::Human,
267 show_paths: true,
268 color: "auto".to_string(),
269 }
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct Config {
276 #[serde(default)]
278 pub general: GeneralConfig,
279
280 #[serde(default)]
282 pub output: OutputConfig,
283
284 #[serde(default)]
286 pub tailwind: TailwindConfig,
287
288 #[serde(default)]
290 pub imports: ImportConfig,
291
292 #[serde(default)]
294 pub components: ComponentConfig,
295
296 #[serde(default)]
298 pub rust: RustConfig,
299
300 #[serde(default)]
302 pub nextjs: NextJsConfig,
303
304 #[serde(default)]
306 pub nestjs: NestJsConfig,
307}
308
309impl Default for Config {
310 fn default() -> Self {
311 Self {
312 general: GeneralConfig::default(),
313 output: OutputConfig::default(),
314 tailwind: TailwindConfig::default(),
315 imports: ImportConfig::default(),
316 components: ComponentConfig::default(),
317 rust: RustConfig::default(),
318 nextjs: NextJsConfig::default(),
319 nestjs: NestJsConfig::default(),
320 }
321 }
322}
323
324impl Config {
325 pub fn from_file(path: &Path) -> Result<Self> {
327 let content = std::fs::read_to_string(path).map_err(|e| Error::File {
328 path: path.to_path_buf(),
329 source: e,
330 })?;
331
332 let config: Config = toml::from_str(&content)?;
333 Ok(config)
334 }
335
336 pub fn load() -> Result<Self> {
338 if let Ok(config) = Self::from_file(Path::new(".parryrc.toml")) {
340 return Ok(config);
341 }
342
343 if let Ok(config) = Self::from_file(Path::new("parry.toml")) {
345 return Ok(config);
346 }
347
348 Ok(Config::default())
350 }
351
352 pub fn save(&self, path: &Path) -> Result<()> {
354 let content = toml::to_string_pretty(self)
355 .map_err(|e| Error::Config(e.to_string()))?;
356 std::fs::write(path, content).map_err(|e| Error::File {
357 path: path.to_path_buf(),
358 source: e,
359 })?;
360 Ok(())
361 }
362
363 pub fn merge(&mut self, other: Config) {
365 if other.general.strict {
367 self.general.strict = true;
368 }
369 if other.general.fail_fast {
370 self.general.fail_fast = true;
371 }
372
373 self.tailwind.enabled = self.tailwind.enabled || other.tailwind.enabled;
375 self.tailwind.safe_list.extend(other.tailwind.safe_list);
376 self.tailwind.block_list.extend(other.tailwind.block_list);
377
378 self.imports.alias_map.extend(other.imports.alias_map);
380
381 self.components.enforce_shadcn = self.components.enforce_shadcn || other.components.enforce_shadcn;
383
384 self.output = other.output;
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn test_config_default() {
395 let config = Config::default();
396 assert!(!config.general.strict);
397 assert!(config.tailwind.enabled);
398 assert_eq!(config.tailwind.config_path, PathBuf::from("tailwind.config.ts"));
399 }
400
401 #[test]
402 fn test_config_merge() {
403 let mut base = Config::default();
404 let override_config = Config {
405 general: GeneralConfig {
406 strict: true,
407 ..Default::default()
408 },
409 tailwind: TailwindConfig {
410 safe_list: vec!["p-*".to_string()],
411 ..Default::default()
412 },
413 ..Default::default()
414 };
415
416 base.merge(override_config);
417 assert!(base.general.strict);
418 assert!(base.tailwind.safe_list.contains(&"p-*".to_string()));
419 }
420
421 #[test]
422 fn test_output_format() {
423 assert_eq!(OutputFormat::from_str("json").unwrap(), OutputFormat::Json);
424 assert_eq!(OutputFormat::from_str("JSON").unwrap(), OutputFormat::Json);
425 assert!(OutputFormat::from_str("invalid").is_err());
426 }
427}