datasynth_server/
config_loader.rs1use datasynth_config::schema::GeneratorConfig;
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::{info, warn};
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum ConfigSource {
16 File { path: PathBuf },
18 Url { url: String },
20 Inline { content: String },
22 #[default]
24 Default,
25}
26
27pub async fn load_config(source: &ConfigSource) -> Result<GeneratorConfig, ConfigLoadError> {
29 match source {
30 ConfigSource::File { path } => {
31 info!("Loading config from file: {}", path.display());
32 let content = tokio::fs::read_to_string(path).await.map_err(|e| {
33 ConfigLoadError::Io(format!("Failed to read {}: {}", path.display(), e))
34 })?;
35 let config: GeneratorConfig = serde_yaml::from_str(&content)
36 .map_err(|e| ConfigLoadError::Parse(format!("Failed to parse YAML: {}", e)))?;
37 Ok(config)
38 }
39 ConfigSource::Url { url } => {
40 warn!(
41 "URL config loading not yet supported (requires reqwest dependency). URL: {}",
42 url
43 );
44 Err(ConfigLoadError::Io(format!(
45 "URL config loading not yet supported. Use file or inline config instead. URL: {}",
46 url
47 )))
48 }
49 ConfigSource::Inline { content } => {
50 info!("Loading inline config ({} bytes)", content.len());
51 let config: GeneratorConfig = serde_yaml::from_str(content)
52 .map_err(|e| ConfigLoadError::Parse(format!("Failed to parse YAML: {}", e)))?;
53 Ok(config)
54 }
55 ConfigSource::Default => {
56 info!("Using default generator config");
57 Ok(crate::grpc::service::default_generator_config())
58 }
59 }
60}
61
62pub async fn reload_config(
64 source: &ConfigSource,
65 config_lock: &Arc<RwLock<GeneratorConfig>>,
66) -> Result<(), ConfigLoadError> {
67 let new_config = load_config(source).await?;
68 let mut config = config_lock.write().await;
69 *config = new_config;
70 info!("Configuration reloaded successfully");
71 Ok(())
72}
73
74#[derive(Debug, Clone)]
76pub enum ConfigLoadError {
77 Io(String),
79 Parse(String),
81}
82
83impl std::fmt::Display for ConfigLoadError {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::Io(msg) => write!(f, "Config I/O error: {}", msg),
87 Self::Parse(msg) => write!(f, "Config parse error: {}", msg),
88 }
89 }
90}
91
92impl std::error::Error for ConfigLoadError {}
93
94#[cfg(test)]
95#[allow(clippy::unwrap_used)]
96mod tests {
97 use super::*;
98
99 #[tokio::test]
100 async fn test_load_default_config() {
101 let config = load_config(&ConfigSource::Default).await.unwrap();
102 assert!(!config.companies.is_empty());
103 }
104
105 #[tokio::test]
106 async fn test_load_inline_config() {
107 let yaml = r#"
108global:
109 industry: manufacturing
110 start_date: "2024-01-01"
111 period_months: 1
112 seed: 42
113 parallel: false
114 group_currency: USD
115 worker_threads: 1
116 memory_limit_mb: 512
117companies:
118 - code: TEST
119 name: Test Company
120 currency: USD
121 country: US
122 annual_transaction_volume: ten_k
123 volume_weight: 1.0
124 fiscal_year_variant: K4
125chart_of_accounts:
126 complexity: small
127output:
128 output_directory: ./output
129"#;
130 let source = ConfigSource::Inline {
131 content: yaml.to_string(),
132 };
133 let config = load_config(&source).await.unwrap();
134 assert_eq!(config.companies[0].code, "TEST");
135 }
136
137 #[tokio::test]
138 async fn test_load_missing_file() {
139 let source = ConfigSource::File {
140 path: PathBuf::from("/nonexistent/config.yaml"),
141 };
142 assert!(load_config(&source).await.is_err());
143 }
144
145 #[tokio::test]
146 async fn test_load_invalid_yaml() {
147 let source = ConfigSource::Inline {
148 content: "{{invalid yaml:".to_string(),
149 };
150 assert!(load_config(&source).await.is_err());
151 }
152
153 #[tokio::test]
154 async fn test_reload_config() {
155 let initial = crate::grpc::service::default_generator_config();
156 let config_lock = Arc::new(RwLock::new(initial));
157
158 reload_config(&ConfigSource::Default, &config_lock)
159 .await
160 .unwrap();
161
162 let config = config_lock.read().await;
163 assert!(!config.companies.is_empty());
164 }
165}