sara_core/config/
settings.rs

1//! Configuration settings structures.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6use crate::error::ConfigError;
7
8/// Main configuration structure.
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct Config {
11    /// Repository configuration.
12    #[serde(default)]
13    pub repositories: RepositoryConfig,
14
15    /// Validation settings.
16    #[serde(default)]
17    pub validation: ValidationConfig,
18
19    /// Output settings.
20    #[serde(default)]
21    pub output: OutputConfig,
22
23    /// Custom templates configuration.
24    #[serde(default)]
25    pub templates: TemplatesConfig,
26}
27
28impl Config {
29    /// Creates a new config with default values.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Adds a repository path.
35    pub fn add_repository(&mut self, path: impl Into<PathBuf>) {
36        self.repositories.paths.push(path.into());
37    }
38
39    /// Expands all glob patterns in template paths.
40    pub fn expand_template_paths(&self) -> Result<Vec<PathBuf>, ConfigError> {
41        expand_glob_patterns(&self.templates.paths)
42    }
43}
44
45/// Repository configuration.
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct RepositoryConfig {
48    /// List of repository paths to scan.
49    #[serde(default)]
50    pub paths: Vec<PathBuf>,
51}
52
53/// Validation settings.
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55pub struct ValidationConfig {
56    /// Treat orphan items as errors (true) or warnings (false).
57    #[serde(default)]
58    pub strict_orphans: bool,
59
60    /// List of allowed custom fields in frontmatter.
61    #[serde(default)]
62    pub allowed_custom_fields: Vec<String>,
63}
64
65/// Output settings.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct OutputConfig {
68    /// Enable colored output.
69    #[serde(default = "default_true")]
70    pub colors: bool,
71
72    /// Enable emoji output.
73    #[serde(default = "default_true")]
74    pub emojis: bool,
75}
76
77impl Default for OutputConfig {
78    fn default() -> Self {
79        Self {
80            colors: true,
81            emojis: true,
82        }
83    }
84}
85
86fn default_true() -> bool {
87    true
88}
89
90/// Custom templates configuration.
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct TemplatesConfig {
93    /// Paths to custom template Markdown files (supports glob patterns, e.g., "*.md").
94    /// Each template must contain exactly one 'type' field in its YAML frontmatter
95    /// to identify the item type it defines.
96    /// Custom templates override built-in templates for the corresponding item type.
97    #[serde(default)]
98    pub paths: Vec<String>,
99}
100
101/// Expands glob patterns in a list of path strings.
102pub fn expand_glob_patterns(patterns: &[String]) -> Result<Vec<PathBuf>, ConfigError> {
103    let mut result = Vec::new();
104
105    for pattern in patterns {
106        match glob::glob(pattern) {
107            Ok(paths) => {
108                for entry in paths {
109                    match entry {
110                        Ok(path) => result.push(path),
111                        Err(e) => {
112                            return Err(ConfigError::InvalidGlobPattern {
113                                pattern: pattern.clone(),
114                                reason: e.to_string(),
115                            });
116                        }
117                    }
118                }
119            }
120            Err(e) => {
121                return Err(ConfigError::InvalidGlobPattern {
122                    pattern: pattern.clone(),
123                    reason: e.to_string(),
124                });
125            }
126        }
127    }
128
129    Ok(result)
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_default_config() {
138        let config = Config::default();
139        assert!(config.repositories.paths.is_empty());
140        assert!(!config.validation.strict_orphans);
141        assert!(config.output.colors);
142        assert!(config.output.emojis);
143    }
144
145    #[test]
146    fn test_add_repository() {
147        let mut config = Config::default();
148        config.add_repository("/path/to/repo");
149        assert_eq!(config.repositories.paths.len(), 1);
150    }
151
152    #[test]
153    fn test_config_serialization() {
154        let config = Config::default();
155        let toml_str = toml::to_string(&config).unwrap();
156        assert!(toml_str.contains("[repositories]"));
157    }
158}