ricecoder_images/
config.rs

1//! Configuration for image support.
2
3use crate::error::ImageResult;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7/// Image support configuration.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ImageConfig {
10    /// Supported image formats
11    pub formats: FormatsConfig,
12    /// Display settings
13    pub display: DisplayConfig,
14    /// Cache settings
15    pub cache: CacheConfig,
16    /// Analysis settings
17    pub analysis: AnalysisConfig,
18}
19
20/// Supported image formats configuration.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FormatsConfig {
23    /// List of supported formats (e.g., "png", "jpg", "gif", "webp")
24    pub supported: Vec<String>,
25}
26
27/// Display settings configuration.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct DisplayConfig {
30    /// Maximum width for display (characters)
31    pub max_width: u32,
32    /// Maximum height for display (characters)
33    pub max_height: u32,
34    /// ASCII placeholder character
35    pub placeholder_char: String,
36}
37
38/// Cache settings configuration.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CacheConfig {
41    /// Whether caching is enabled
42    pub enabled: bool,
43    /// Cache TTL in seconds (24 hours = 86400)
44    pub ttl_seconds: u64,
45    /// Maximum cache size in MB (100 MB)
46    pub max_size_mb: u64,
47}
48
49/// Analysis settings configuration.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AnalysisConfig {
52    /// Analysis timeout in seconds
53    pub timeout_seconds: u64,
54    /// Maximum image size for analysis in MB (10 MB)
55    pub max_image_size_mb: u64,
56    /// Whether to automatically optimize large images
57    pub optimize_large_images: bool,
58}
59
60#[allow(clippy::derivable_impls)]
61impl Default for ImageConfig {
62    fn default() -> Self {
63        Self {
64            formats: FormatsConfig::default(),
65            display: DisplayConfig::default(),
66            cache: CacheConfig::default(),
67            analysis: AnalysisConfig::default(),
68        }
69    }
70}
71
72impl Default for FormatsConfig {
73    fn default() -> Self {
74        Self {
75            supported: vec![
76                "png".to_string(),
77                "jpg".to_string(),
78                "jpeg".to_string(),
79                "gif".to_string(),
80                "webp".to_string(),
81            ],
82        }
83    }
84}
85
86impl Default for DisplayConfig {
87    fn default() -> Self {
88        Self {
89            max_width: 80,
90            max_height: 30,
91            placeholder_char: "█".to_string(),
92        }
93    }
94}
95
96impl Default for CacheConfig {
97    fn default() -> Self {
98        Self {
99            enabled: true,
100            ttl_seconds: 86400, // 24 hours
101            max_size_mb: 100,
102        }
103    }
104}
105
106impl Default for AnalysisConfig {
107    fn default() -> Self {
108        Self {
109            timeout_seconds: 10,
110            max_image_size_mb: 10,
111            optimize_large_images: true,
112        }
113    }
114}
115
116impl ImageConfig {
117    /// Load configuration from a YAML file.
118    ///
119    /// # Arguments
120    ///
121    /// * `path` - Path to the configuration file
122    ///
123    /// # Returns
124    ///
125    /// Configuration loaded from file, or default if file doesn't exist
126    pub fn from_file(path: &PathBuf) -> ImageResult<Self> {
127        if !path.exists() {
128            return Ok(Self::default());
129        }
130
131        let content = std::fs::read_to_string(path)?;
132        let config = serde_yaml::from_str(&content)?;
133        Ok(config)
134    }
135
136    /// Load configuration with hierarchy support.
137    ///
138    /// Configuration hierarchy (highest to lowest priority):
139    /// 1. Runtime overrides (not implemented here)
140    /// 2. Project-level config (projects/ricecoder/config/images.yaml)
141    /// 3. User-level config (~/.ricecoder/config/images.yaml)
142    /// 4. Built-in defaults
143    ///
144    /// # Returns
145    ///
146    /// Merged configuration from all available sources
147    pub fn load_with_hierarchy() -> ImageResult<Self> {
148        let mut config = Self::default();
149
150        // Try user-level config
151        if let Ok(user_home) = std::env::var("HOME") {
152            let user_config_path = PathBuf::from(user_home)
153                .join(".ricecoder")
154                .join("config")
155                .join("images.yaml");
156            if let Ok(user_config) = Self::from_file(&user_config_path) {
157                config = Self::merge(config, user_config);
158            }
159        }
160
161        // Try project-level config
162        let project_config_path = PathBuf::from("config/images.yaml");
163        if let Ok(project_config) = Self::from_file(&project_config_path) {
164            config = Self::merge(config, project_config);
165        }
166
167        Ok(config)
168    }
169
170    /// Merge two configurations, with `override_config` taking precedence.
171    fn merge(mut base: Self, override_config: Self) -> Self {
172        // Merge formats
173        if !override_config.formats.supported.is_empty() {
174            base.formats = override_config.formats;
175        }
176
177        // Merge display settings
178        if override_config.display.max_width != 0 {
179            base.display = override_config.display;
180        }
181
182        // Merge cache settings
183        if override_config.cache.ttl_seconds != 0 {
184            base.cache = override_config.cache;
185        }
186
187        // Merge analysis settings
188        if override_config.analysis.timeout_seconds != 0 {
189            base.analysis = override_config.analysis;
190        }
191
192        base
193    }
194
195    /// Check if a format is supported.
196    pub fn is_format_supported(&self, format: &str) -> bool {
197        self.formats
198            .supported
199            .iter()
200            .any(|f| f.eq_ignore_ascii_case(format))
201    }
202
203    /// Get the list of supported formats as a comma-separated string.
204    pub fn supported_formats_string(&self) -> String {
205        self.formats.supported.join(", ")
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_default_config() {
215        let config = ImageConfig::default();
216        assert!(config.cache.enabled);
217        assert_eq!(config.cache.ttl_seconds, 86400);
218        assert_eq!(config.cache.max_size_mb, 100);
219        assert_eq!(config.display.max_width, 80);
220        assert_eq!(config.display.max_height, 30);
221        assert_eq!(config.analysis.timeout_seconds, 10);
222        assert_eq!(config.analysis.max_image_size_mb, 10);
223    }
224
225    #[test]
226    fn test_is_format_supported() {
227        let config = ImageConfig::default();
228        assert!(config.is_format_supported("png"));
229        assert!(config.is_format_supported("PNG"));
230        assert!(config.is_format_supported("jpg"));
231        assert!(config.is_format_supported("jpeg"));
232        assert!(config.is_format_supported("gif"));
233        assert!(config.is_format_supported("webp"));
234        assert!(!config.is_format_supported("bmp"));
235    }
236
237    #[test]
238    fn test_supported_formats_string() {
239        let config = ImageConfig::default();
240        let formats = config.supported_formats_string();
241        assert!(formats.contains("png"));
242        assert!(formats.contains("jpg"));
243        assert!(formats.contains("gif"));
244        assert!(formats.contains("webp"));
245    }
246
247    #[test]
248    fn test_config_serialization() {
249        let config = ImageConfig::default();
250        let yaml = serde_yaml::to_string(&config).expect("Failed to serialize");
251        let deserialized: ImageConfig =
252            serde_yaml::from_str(&yaml).expect("Failed to deserialize");
253        assert_eq!(config.cache.ttl_seconds, deserialized.cache.ttl_seconds);
254        assert_eq!(config.display.max_width, deserialized.display.max_width);
255    }
256}