use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::formats::ExportFormat;
use super::reports::ReportType;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportConfig {
pub default_format: ExportFormat,
pub output_dir: PathBuf,
pub max_export_size: usize,
pub enable_compression: bool,
pub email: Option<EmailConfig>,
pub storage: StorageConfig,
pub scheduled_reports: Vec<ScheduledReportConfig>,
pub templates_dir: PathBuf,
}
impl Default for ExportConfig {
fn default() -> Self {
Self {
default_format: ExportFormat::Csv,
output_dir: PathBuf::from("./exports"),
max_export_size: 100 * 1024 * 1024, enable_compression: true,
email: None,
storage: StorageConfig::default(),
scheduled_reports: Vec::new(),
templates_dir: PathBuf::from("./templates"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailConfig {
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_username: String,
pub smtp_password: String,
pub use_tls: bool,
pub use_starttls: bool,
pub from_email: String,
pub from_name: String,
pub default_recipients: Vec<String>,
pub template_name: String,
pub timeout_secs: u64,
}
impl Default for EmailConfig {
fn default() -> Self {
Self {
smtp_host: "localhost".to_string(),
smtp_port: 587,
smtp_username: String::new(),
smtp_password: String::new(),
use_tls: false,
use_starttls: true,
from_email: "reports@llm-cost-ops.local".to_string(),
from_name: "LLM Cost Ops".to_string(),
default_recipients: Vec::new(),
template_name: "default".to_string(),
timeout_secs: 30,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub backend: StorageBackend,
pub local: Option<LocalStorageConfig>,
pub s3: Option<S3StorageConfig>,
pub retention_days: u32,
pub auto_cleanup: bool,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
backend: StorageBackend::Local,
local: Some(LocalStorageConfig::default()),
s3: None,
retention_days: 90,
auto_cleanup: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StorageBackend {
Local,
S3,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalStorageConfig {
pub base_dir: PathBuf,
pub use_date_subdirs: bool,
pub file_pattern: String,
}
impl Default for LocalStorageConfig {
fn default() -> Self {
Self {
base_dir: PathBuf::from("./reports"),
use_date_subdirs: true,
file_pattern: "{report_type}_{date}_{id}.{ext}".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct S3StorageConfig {
pub bucket: String,
pub region: String,
pub access_key: String,
pub secret_key: String,
pub endpoint: Option<String>,
pub prefix: String,
pub storage_class: String,
}
impl Default for S3StorageConfig {
fn default() -> Self {
Self {
bucket: String::new(),
region: "us-east-1".to_string(),
access_key: String::new(),
secret_key: String::new(),
endpoint: None,
prefix: "reports/".to_string(),
storage_class: "STANDARD".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduledReportConfig {
pub id: String,
pub report_type: ReportType,
pub schedule: String,
pub format: ExportFormat,
pub delivery: Vec<DeliveryTarget>,
pub filters: ReportFiltersConfig,
pub enabled: bool,
pub timezone: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum DeliveryTarget {
Email {
recipients: Vec<String>,
subject: String,
body_template: Option<String>,
},
Storage {
path: Option<String>,
},
Webhook {
url: String,
headers: std::collections::HashMap<String, String>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReportFiltersConfig {
pub provider: Option<String>,
pub model: Option<String>,
pub user_id: Option<String>,
pub resource_type: Option<String>,
pub organization_id: Option<String>,
pub tags: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportTemplate {
pub name: String,
pub file_path: PathBuf,
pub format: TemplateFormat,
pub default_vars: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TemplateFormat {
Html,
Text,
Markdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionConfig {
pub algorithm: CompressionAlgorithm,
pub level: u32,
pub min_size: usize,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
algorithm: CompressionAlgorithm::Gzip,
level: 6,
min_size: 1024, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompressionAlgorithm {
Gzip,
Brotli,
Zstd,
}
impl ExportConfig {
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string(path)?;
let config = toml::from_str(&contents)?;
Ok(config)
}
pub fn to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), Box<dyn std::error::Error>> {
let contents = toml::to_string_pretty(self)?;
std::fs::write(path, contents)?;
Ok(())
}
pub fn email_config(&self) -> Option<&EmailConfig> {
self.email.as_ref()
}
pub fn storage_config(&self) -> &StorageConfig {
&self.storage
}
pub fn scheduled_reports(&self) -> &[ScheduledReportConfig] {
&self.scheduled_reports
}
pub fn add_scheduled_report(&mut self, config: ScheduledReportConfig) {
self.scheduled_reports.push(config);
}
pub fn remove_scheduled_report(&mut self, id: &str) -> Option<ScheduledReportConfig> {
if let Some(pos) = self.scheduled_reports.iter().position(|r| r.id == id) {
Some(self.scheduled_reports.remove(pos))
} else {
None
}
}
pub fn get_scheduled_report(&self, id: &str) -> Option<&ScheduledReportConfig> {
self.scheduled_reports.iter().find(|r| r.id == id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ExportConfig::default();
assert_eq!(config.default_format, ExportFormat::Csv);
assert_eq!(config.max_export_size, 100 * 1024 * 1024);
assert!(config.enable_compression);
}
#[test]
fn test_email_config_default() {
let config = EmailConfig::default();
assert_eq!(config.smtp_port, 587);
assert!(config.use_starttls);
assert!(!config.use_tls);
}
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert_eq!(config.backend, StorageBackend::Local);
assert_eq!(config.retention_days, 90);
assert!(config.auto_cleanup);
}
#[test]
fn test_add_remove_scheduled_report() {
let mut config = ExportConfig::default();
let scheduled = ScheduledReportConfig {
id: "test-report".to_string(),
report_type: ReportType::Cost,
schedule: "0 0 * * *".to_string(),
format: ExportFormat::Csv,
delivery: vec![],
filters: ReportFiltersConfig::default(),
enabled: true,
timezone: "UTC".to_string(),
};
config.add_scheduled_report(scheduled.clone());
assert_eq!(config.scheduled_reports().len(), 1);
let removed = config.remove_scheduled_report("test-report");
assert!(removed.is_some());
assert_eq!(config.scheduled_reports().len(), 0);
}
#[test]
fn test_compression_config() {
let config = CompressionConfig::default();
assert_eq!(config.algorithm, CompressionAlgorithm::Gzip);
assert_eq!(config.level, 6);
assert_eq!(config.min_size, 1024);
}
}