Skip to main content

oparry_core/
config.rs

1//! Configuration management
2
3use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8/// Output format
9#[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    /// Parse from string
19    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/// General configuration
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct GeneralConfig {
38    /// Enable strict mode (warnings as errors)
39    #[serde(default)]
40    pub strict: bool,
41
42    /// Stop on first error
43    #[serde(default)]
44    pub fail_fast: bool,
45
46    /// Maximum issues to report
47    #[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/// Tailwind configuration
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TailwindConfig {
68    /// Enable Tailwind validator
69    #[serde(default = "default_true")]
70    pub enabled: bool,
71
72    /// Path to Tailwind config
73    #[serde(default = "default_tailwind_config")]
74    pub config_path: PathBuf,
75
76    /// Safe list patterns
77    #[serde(default)]
78    pub safe_list: Vec<String>,
79
80    /// Block list patterns
81    #[serde(default)]
82    pub block_list: Vec<String>,
83
84    /// Maximum arbitrary values
85    #[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/// Import configuration
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ImportConfig {
112    /// Enforce path aliases
113    #[serde(default = "default_true")]
114    pub enforce_alias: bool,
115
116    /// Alias mappings
117    #[serde(default)]
118    pub alias_map: HashMap<String, String>,
119
120    /// Require file extensions
121    #[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/// Component configuration
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ComponentConfig {
138    /// Enforce shadcn/ui usage
139    #[serde(default = "default_true")]
140    pub enforce_shadcn: bool,
141
142    /// shadcn/ui components path
143    #[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/// Rust configuration
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RustConfig {
167    /// Enable Rust validator
168    #[serde(default = "default_true")]
169    pub enabled: bool,
170
171    /// Deny unsafe code
172    #[serde(default)]
173    pub deny_unsafe: Option<String>,
174
175    /// Warn on unwrap()
176    #[serde(default = "default_true")]
177    pub warn_unwrap: bool,
178
179    /// Enforce Result handling
180    #[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/// Next.js configuration
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct NextJsConfig {
198    /// Enable Next.js validator
199    #[serde(default = "default_true")]
200    pub enabled: bool,
201
202    /// Enforce App Router conventions
203    #[serde(default = "default_true")]
204    pub enforce_app_router: bool,
205
206    /// Validate page exports
207    #[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/// NestJS configuration
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct NestJsConfig {
224    /// Enable NestJS validator
225    #[serde(default = "default_true")]
226    pub enabled: bool,
227
228    /// Enforce decorator usage
229    #[serde(default = "default_true")]
230    pub enforce_decorators: bool,
231
232    /// Validate module imports
233    #[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/// Output configuration
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct OutputConfig {
250    /// Output format
251    #[serde(default)]
252    pub format: OutputFormat,
253
254    /// Show file paths
255    #[serde(default = "default_true")]
256    pub show_paths: bool,
257
258    /// Color output
259    #[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/// Main configuration
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct Config {
276    /// General settings
277    #[serde(default)]
278    pub general: GeneralConfig,
279
280    /// Output settings
281    #[serde(default)]
282    pub output: OutputConfig,
283
284    /// Tailwind settings
285    #[serde(default)]
286    pub tailwind: TailwindConfig,
287
288    /// Import settings
289    #[serde(default)]
290    pub imports: ImportConfig,
291
292    /// Component settings
293    #[serde(default)]
294    pub components: ComponentConfig,
295
296    /// Rust settings
297    #[serde(default)]
298    pub rust: RustConfig,
299
300    /// Next.js settings
301    #[serde(default)]
302    pub nextjs: NextJsConfig,
303
304    /// NestJS settings
305    #[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    /// Load config from file
326    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    /// Load from default locations
337    pub fn load() -> Result<Self> {
338        // Check .parryrc.toml first
339        if let Ok(config) = Self::from_file(Path::new(".parryrc.toml")) {
340            return Ok(config);
341        }
342
343        // Check parry.toml
344        if let Ok(config) = Self::from_file(Path::new("parry.toml")) {
345            return Ok(config);
346        }
347
348        // Return default
349        Ok(Config::default())
350    }
351
352    /// Save config to file
353    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    /// Merge with another config (other takes precedence)
364    pub fn merge(&mut self, other: Config) {
365        // Merge general settings
366        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        // Merge tailwind settings
374        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        // Merge imports
379        self.imports.alias_map.extend(other.imports.alias_map);
380
381        // Merge components
382        self.components.enforce_shadcn = self.components.enforce_shadcn || other.components.enforce_shadcn;
383
384        // Output takes precedence
385        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}