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