ricecoder_images/
config.rs1use crate::error::ImageResult;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ImageConfig {
10 pub formats: FormatsConfig,
12 pub display: DisplayConfig,
14 pub cache: CacheConfig,
16 pub analysis: AnalysisConfig,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FormatsConfig {
23 pub supported: Vec<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct DisplayConfig {
30 pub max_width: u32,
32 pub max_height: u32,
34 pub placeholder_char: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CacheConfig {
41 pub enabled: bool,
43 pub ttl_seconds: u64,
45 pub max_size_mb: u64,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AnalysisConfig {
52 pub timeout_seconds: u64,
54 pub max_image_size_mb: u64,
56 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, 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 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 pub fn load_with_hierarchy() -> ImageResult<Self> {
148 let mut config = Self::default();
149
150 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 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 fn merge(mut base: Self, override_config: Self) -> Self {
172 if !override_config.formats.supported.is_empty() {
174 base.formats = override_config.formats;
175 }
176
177 if override_config.display.max_width != 0 {
179 base.display = override_config.display;
180 }
181
182 if override_config.cache.ttl_seconds != 0 {
184 base.cache = override_config.cache;
185 }
186
187 if override_config.analysis.timeout_seconds != 0 {
189 base.analysis = override_config.analysis;
190 }
191
192 base
193 }
194
195 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 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}