1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6use super::formats::ExportFormat;
7use super::reports::ReportType;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ExportConfig {
12 pub default_format: ExportFormat,
14
15 pub output_dir: PathBuf,
17
18 pub max_export_size: usize,
20
21 pub enable_compression: bool,
23
24 pub email: Option<EmailConfig>,
26
27 pub storage: StorageConfig,
29
30 pub scheduled_reports: Vec<ScheduledReportConfig>,
32
33 pub templates_dir: PathBuf,
35}
36
37impl Default for ExportConfig {
38 fn default() -> Self {
39 Self {
40 default_format: ExportFormat::Csv,
41 output_dir: PathBuf::from("./exports"),
42 max_export_size: 100 * 1024 * 1024, enable_compression: true,
44 email: None,
45 storage: StorageConfig::default(),
46 scheduled_reports: Vec::new(),
47 templates_dir: PathBuf::from("./templates"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct EmailConfig {
55 pub smtp_host: String,
57
58 pub smtp_port: u16,
60
61 pub smtp_username: String,
63
64 pub smtp_password: String,
66
67 pub use_tls: bool,
69
70 pub use_starttls: bool,
72
73 pub from_email: String,
75
76 pub from_name: String,
78
79 pub default_recipients: Vec<String>,
81
82 pub template_name: String,
84
85 pub timeout_secs: u64,
87}
88
89impl Default for EmailConfig {
90 fn default() -> Self {
91 Self {
92 smtp_host: "localhost".to_string(),
93 smtp_port: 587,
94 smtp_username: String::new(),
95 smtp_password: String::new(),
96 use_tls: false,
97 use_starttls: true,
98 from_email: "reports@llm-cost-ops.local".to_string(),
99 from_name: "LLM Cost Ops".to_string(),
100 default_recipients: Vec::new(),
101 template_name: "default".to_string(),
102 timeout_secs: 30,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct StorageConfig {
110 pub backend: StorageBackend,
112
113 pub local: Option<LocalStorageConfig>,
115
116 pub s3: Option<S3StorageConfig>,
118
119 pub retention_days: u32,
121
122 pub auto_cleanup: bool,
124}
125
126impl Default for StorageConfig {
127 fn default() -> Self {
128 Self {
129 backend: StorageBackend::Local,
130 local: Some(LocalStorageConfig::default()),
131 s3: None,
132 retention_days: 90,
133 auto_cleanup: true,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum StorageBackend {
142 Local,
143 S3,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct LocalStorageConfig {
149 pub base_dir: PathBuf,
151
152 pub use_date_subdirs: bool,
154
155 pub file_pattern: String,
157}
158
159impl Default for LocalStorageConfig {
160 fn default() -> Self {
161 Self {
162 base_dir: PathBuf::from("./reports"),
163 use_date_subdirs: true,
164 file_pattern: "{report_type}_{date}_{id}.{ext}".to_string(),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct S3StorageConfig {
172 pub bucket: String,
174
175 pub region: String,
177
178 pub access_key: String,
180
181 pub secret_key: String,
183
184 pub endpoint: Option<String>,
186
187 pub prefix: String,
189
190 pub storage_class: String,
192}
193
194impl Default for S3StorageConfig {
195 fn default() -> Self {
196 Self {
197 bucket: String::new(),
198 region: "us-east-1".to_string(),
199 access_key: String::new(),
200 secret_key: String::new(),
201 endpoint: None,
202 prefix: "reports/".to_string(),
203 storage_class: "STANDARD".to_string(),
204 }
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ScheduledReportConfig {
211 pub id: String,
213
214 pub report_type: ReportType,
216
217 pub schedule: String,
219
220 pub format: ExportFormat,
222
223 pub delivery: Vec<DeliveryTarget>,
225
226 pub filters: ReportFiltersConfig,
228
229 pub enabled: bool,
231
232 pub timezone: String,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(tag = "type", rename_all = "lowercase")]
239pub enum DeliveryTarget {
240 Email {
241 recipients: Vec<String>,
242 subject: String,
243 body_template: Option<String>,
244 },
245 Storage {
246 path: Option<String>,
247 },
248 Webhook {
249 url: String,
250 headers: std::collections::HashMap<String, String>,
251 },
252}
253
254#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct ReportFiltersConfig {
257 pub provider: Option<String>,
258 pub model: Option<String>,
259 pub user_id: Option<String>,
260 pub resource_type: Option<String>,
261 pub organization_id: Option<String>,
262 pub tags: std::collections::HashMap<String, String>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ReportTemplate {
268 pub name: String,
270
271 pub file_path: PathBuf,
273
274 pub format: TemplateFormat,
276
277 pub default_vars: std::collections::HashMap<String, String>,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "lowercase")]
284pub enum TemplateFormat {
285 Html,
286 Text,
287 Markdown,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct CompressionConfig {
293 pub algorithm: CompressionAlgorithm,
295
296 pub level: u32,
298
299 pub min_size: usize,
301}
302
303impl Default for CompressionConfig {
304 fn default() -> Self {
305 Self {
306 algorithm: CompressionAlgorithm::Gzip,
307 level: 6,
308 min_size: 1024, }
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "lowercase")]
316pub enum CompressionAlgorithm {
317 Gzip,
318 Brotli,
319 Zstd,
320}
321
322impl ExportConfig {
323 pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, Box<dyn std::error::Error>> {
325 let contents = std::fs::read_to_string(path)?;
326 let config = toml::from_str(&contents)?;
327 Ok(config)
328 }
329
330 pub fn to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), Box<dyn std::error::Error>> {
332 let contents = toml::to_string_pretty(self)?;
333 std::fs::write(path, contents)?;
334 Ok(())
335 }
336
337 pub fn email_config(&self) -> Option<&EmailConfig> {
339 self.email.as_ref()
340 }
341
342 pub fn storage_config(&self) -> &StorageConfig {
344 &self.storage
345 }
346
347 pub fn scheduled_reports(&self) -> &[ScheduledReportConfig] {
349 &self.scheduled_reports
350 }
351
352 pub fn add_scheduled_report(&mut self, config: ScheduledReportConfig) {
354 self.scheduled_reports.push(config);
355 }
356
357 pub fn remove_scheduled_report(&mut self, id: &str) -> Option<ScheduledReportConfig> {
359 if let Some(pos) = self.scheduled_reports.iter().position(|r| r.id == id) {
360 Some(self.scheduled_reports.remove(pos))
361 } else {
362 None
363 }
364 }
365
366 pub fn get_scheduled_report(&self, id: &str) -> Option<&ScheduledReportConfig> {
368 self.scheduled_reports.iter().find(|r| r.id == id)
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn test_default_config() {
378 let config = ExportConfig::default();
379 assert_eq!(config.default_format, ExportFormat::Csv);
380 assert_eq!(config.max_export_size, 100 * 1024 * 1024);
381 assert!(config.enable_compression);
382 }
383
384 #[test]
385 fn test_email_config_default() {
386 let config = EmailConfig::default();
387 assert_eq!(config.smtp_port, 587);
388 assert!(config.use_starttls);
389 assert!(!config.use_tls);
390 }
391
392 #[test]
393 fn test_storage_config_default() {
394 let config = StorageConfig::default();
395 assert_eq!(config.backend, StorageBackend::Local);
396 assert_eq!(config.retention_days, 90);
397 assert!(config.auto_cleanup);
398 }
399
400 #[test]
401 fn test_add_remove_scheduled_report() {
402 let mut config = ExportConfig::default();
403
404 let scheduled = ScheduledReportConfig {
405 id: "test-report".to_string(),
406 report_type: ReportType::Cost,
407 schedule: "0 0 * * *".to_string(),
408 format: ExportFormat::Csv,
409 delivery: vec![],
410 filters: ReportFiltersConfig::default(),
411 enabled: true,
412 timezone: "UTC".to_string(),
413 };
414
415 config.add_scheduled_report(scheduled.clone());
416 assert_eq!(config.scheduled_reports().len(), 1);
417
418 let removed = config.remove_scheduled_report("test-report");
419 assert!(removed.is_some());
420 assert_eq!(config.scheduled_reports().len(), 0);
421 }
422
423 #[test]
424 fn test_compression_config() {
425 let config = CompressionConfig::default();
426 assert_eq!(config.algorithm, CompressionAlgorithm::Gzip);
427 assert_eq!(config.level, 6);
428 assert_eq!(config.min_size, 1024);
429 }
430}