1use serde::{Deserialize, Serialize};
4use std::env;
5use std::fs;
6use std::path::PathBuf;
7use std::time::Duration;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum ConfigError {
13 #[error("Invalid configuration: {0}")]
15 InvalidValue(String),
16
17 #[error("Configuration file not found: {0}")]
19 FileNotFound(String),
20
21 #[error("Parse error: {0}")]
23 ParseError(String),
24
25 #[error("Environment variable error: {0}")]
27 EnvError(String),
28
29 #[error("IO error: {0}")]
31 IoError(#[from] std::io::Error),
32
33 #[error("Serialization error: {0}")]
35 SerializationError(#[from] serde_json::Error),
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, Default)]
40pub struct Config {
41 pub node: NodeConfig,
43
44 pub network: NetworkConfig,
46
47 pub consensus: ConsensusConfig,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct NodeConfig {
54 pub node_id: String,
56
57 pub data_dir: PathBuf,
59
60 pub log_level: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct NetworkConfig {
67 pub port: u16,
69
70 pub max_peers: usize,
72
73 pub connect_timeout: Duration,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ConsensusConfig {
80 pub finality_threshold: f64,
82
83 pub round_timeout: Duration,
85
86 pub max_rounds: usize,
88}
89
90impl Default for NodeConfig {
91 fn default() -> Self {
92 Self {
93 node_id: "node-0".to_string(),
94 data_dir: PathBuf::from("./data"),
95 log_level: "info".to_string(),
96 }
97 }
98}
99
100impl Default for NetworkConfig {
101 fn default() -> Self {
102 Self {
103 port: 8080,
104 max_peers: 50,
105 connect_timeout: Duration::from_secs(30),
106 }
107 }
108}
109
110impl Default for ConsensusConfig {
111 fn default() -> Self {
112 Self {
113 finality_threshold: 0.67,
114 round_timeout: Duration::from_secs(10),
115 max_rounds: 100,
116 }
117 }
118}
119
120impl Config {
121 pub fn load_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigError> {
123 let content = fs::read_to_string(&path)
124 .map_err(|_| ConfigError::FileNotFound(path.as_ref().display().to_string()))?;
125
126 let mut config: Config =
127 serde_json::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
128
129 config.apply_env_overrides()?;
131
132 config.validate()?;
134
135 Ok(config)
136 }
137
138 pub fn load_from_toml<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigError> {
140 let content = fs::read_to_string(&path)
141 .map_err(|_| ConfigError::FileNotFound(path.as_ref().display().to_string()))?;
142
143 let mut config: Config =
144 toml::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
145
146 config.apply_env_overrides()?;
148
149 config.validate()?;
151
152 Ok(config)
153 }
154
155 pub fn apply_env_overrides(&mut self) -> Result<(), ConfigError> {
157 if let Ok(node_id) = env::var("QUDAG_NODE_ID") {
159 self.node.node_id = node_id;
160 }
161
162 if let Ok(data_dir) = env::var("QUDAG_DATA_DIR") {
163 self.node.data_dir = PathBuf::from(data_dir);
164 }
165
166 if let Ok(log_level) = env::var("QUDAG_LOG_LEVEL") {
167 self.node.log_level = log_level;
168 }
169
170 if let Ok(port) = env::var("QUDAG_PORT") {
172 self.network.port = port
173 .parse()
174 .map_err(|e| ConfigError::EnvError(format!("Invalid port: {}", e)))?;
175 }
176
177 if let Ok(max_peers) = env::var("QUDAG_MAX_PEERS") {
178 self.network.max_peers = max_peers
179 .parse()
180 .map_err(|e| ConfigError::EnvError(format!("Invalid max_peers: {}", e)))?;
181 }
182
183 if let Ok(timeout) = env::var("QUDAG_CONNECT_TIMEOUT") {
184 let timeout_secs: u64 = timeout
185 .parse()
186 .map_err(|e| ConfigError::EnvError(format!("Invalid connect_timeout: {}", e)))?;
187 self.network.connect_timeout = Duration::from_secs(timeout_secs);
188 }
189
190 if let Ok(threshold) = env::var("QUDAG_FINALITY_THRESHOLD") {
192 self.consensus.finality_threshold = threshold
193 .parse()
194 .map_err(|e| ConfigError::EnvError(format!("Invalid finality_threshold: {}", e)))?;
195 }
196
197 if let Ok(timeout) = env::var("QUDAG_ROUND_TIMEOUT") {
198 let timeout_secs: u64 = timeout
199 .parse()
200 .map_err(|e| ConfigError::EnvError(format!("Invalid round_timeout: {}", e)))?;
201 self.consensus.round_timeout = Duration::from_secs(timeout_secs);
202 }
203
204 if let Ok(max_rounds) = env::var("QUDAG_MAX_ROUNDS") {
205 self.consensus.max_rounds = max_rounds
206 .parse()
207 .map_err(|e| ConfigError::EnvError(format!("Invalid max_rounds: {}", e)))?;
208 }
209
210 Ok(())
211 }
212
213 pub fn validate(&self) -> Result<(), ConfigError> {
215 if self.node.node_id.is_empty() {
217 return Err(ConfigError::InvalidValue(
218 "node_id cannot be empty".to_string(),
219 ));
220 }
221
222 if self.node.log_level.is_empty() {
223 return Err(ConfigError::InvalidValue(
224 "log_level cannot be empty".to_string(),
225 ));
226 }
227
228 match self.node.log_level.as_str() {
230 "trace" | "debug" | "info" | "warn" | "error" => {}
231 _ => {
232 return Err(ConfigError::InvalidValue(format!(
233 "Invalid log_level: {}",
234 self.node.log_level
235 )))
236 }
237 }
238
239 if self.network.port == 0 {
241 return Err(ConfigError::InvalidValue("port cannot be 0".to_string()));
242 }
243
244 if self.network.max_peers == 0 {
245 return Err(ConfigError::InvalidValue(
246 "max_peers must be > 0".to_string(),
247 ));
248 }
249
250 if self.network.max_peers > 10000 {
251 return Err(ConfigError::InvalidValue(
252 "max_peers must be <= 10000".to_string(),
253 ));
254 }
255
256 if self.network.connect_timeout.is_zero() {
257 return Err(ConfigError::InvalidValue(
258 "connect_timeout must be > 0".to_string(),
259 ));
260 }
261
262 if self.network.connect_timeout > Duration::from_secs(300) {
263 return Err(ConfigError::InvalidValue(
264 "connect_timeout must be <= 300s".to_string(),
265 ));
266 }
267
268 if self.consensus.finality_threshold <= 0.0 || self.consensus.finality_threshold > 1.0 {
270 return Err(ConfigError::InvalidValue(
271 "finality_threshold must be between 0.0 and 1.0".to_string(),
272 ));
273 }
274
275 if self.consensus.round_timeout.is_zero() {
276 return Err(ConfigError::InvalidValue(
277 "round_timeout must be > 0".to_string(),
278 ));
279 }
280
281 if self.consensus.max_rounds == 0 {
282 return Err(ConfigError::InvalidValue(
283 "max_rounds must be > 0".to_string(),
284 ));
285 }
286
287 if self.consensus.max_rounds > 1000 {
288 return Err(ConfigError::InvalidValue(
289 "max_rounds must be <= 1000".to_string(),
290 ));
291 }
292
293 Ok(())
294 }
295
296 pub fn save_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), ConfigError> {
298 let content = serde_json::to_string_pretty(self)?;
299 fs::write(path, content)?;
300 Ok(())
301 }
302
303 pub fn save_to_toml<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), ConfigError> {
305 let content = toml::to_string_pretty(self).map_err(|e| {
306 ConfigError::SerializationError(serde_json::Error::io(std::io::Error::new(
307 std::io::ErrorKind::InvalidData,
308 e,
309 )))
310 })?;
311 fs::write(path, content)?;
312 Ok(())
313 }
314}