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