use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Invalid configuration: {0}")]
InvalidValue(String),
#[error("Configuration file not found: {0}")]
FileNotFound(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Environment variable error: {0}")]
EnvError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub node: NodeConfig,
pub network: NetworkConfig,
pub consensus: ConsensusConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
pub node_id: String,
pub data_dir: PathBuf,
pub log_level: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
pub port: u16,
pub max_peers: usize,
pub connect_timeout: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusConfig {
pub finality_threshold: f64,
pub round_timeout: Duration,
pub max_rounds: usize,
}
impl Default for NodeConfig {
fn default() -> Self {
Self {
node_id: "node-0".to_string(),
data_dir: PathBuf::from("./data"),
log_level: "info".to_string(),
}
}
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
port: 8080,
max_peers: 50,
connect_timeout: Duration::from_secs(30),
}
}
}
impl Default for ConsensusConfig {
fn default() -> Self {
Self {
finality_threshold: 0.67,
round_timeout: Duration::from_secs(10),
max_rounds: 100,
}
}
}
impl Config {
pub fn load_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigError> {
let content = fs::read_to_string(&path)
.map_err(|_| ConfigError::FileNotFound(path.as_ref().display().to_string()))?;
let mut config: Config =
serde_json::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
config.apply_env_overrides()?;
config.validate()?;
Ok(config)
}
pub fn load_from_toml<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigError> {
let content = fs::read_to_string(&path)
.map_err(|_| ConfigError::FileNotFound(path.as_ref().display().to_string()))?;
let mut config: Config =
toml::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
config.apply_env_overrides()?;
config.validate()?;
Ok(config)
}
pub fn apply_env_overrides(&mut self) -> Result<(), ConfigError> {
if let Ok(node_id) = env::var("QUDAG_NODE_ID") {
self.node.node_id = node_id;
}
if let Ok(data_dir) = env::var("QUDAG_DATA_DIR") {
self.node.data_dir = PathBuf::from(data_dir);
}
if let Ok(log_level) = env::var("QUDAG_LOG_LEVEL") {
self.node.log_level = log_level;
}
if let Ok(port) = env::var("QUDAG_PORT") {
self.network.port = port
.parse()
.map_err(|e| ConfigError::EnvError(format!("Invalid port: {}", e)))?;
}
if let Ok(max_peers) = env::var("QUDAG_MAX_PEERS") {
self.network.max_peers = max_peers
.parse()
.map_err(|e| ConfigError::EnvError(format!("Invalid max_peers: {}", e)))?;
}
if let Ok(timeout) = env::var("QUDAG_CONNECT_TIMEOUT") {
let timeout_secs: u64 = timeout
.parse()
.map_err(|e| ConfigError::EnvError(format!("Invalid connect_timeout: {}", e)))?;
self.network.connect_timeout = Duration::from_secs(timeout_secs);
}
if let Ok(threshold) = env::var("QUDAG_FINALITY_THRESHOLD") {
self.consensus.finality_threshold = threshold
.parse()
.map_err(|e| ConfigError::EnvError(format!("Invalid finality_threshold: {}", e)))?;
}
if let Ok(timeout) = env::var("QUDAG_ROUND_TIMEOUT") {
let timeout_secs: u64 = timeout
.parse()
.map_err(|e| ConfigError::EnvError(format!("Invalid round_timeout: {}", e)))?;
self.consensus.round_timeout = Duration::from_secs(timeout_secs);
}
if let Ok(max_rounds) = env::var("QUDAG_MAX_ROUNDS") {
self.consensus.max_rounds = max_rounds
.parse()
.map_err(|e| ConfigError::EnvError(format!("Invalid max_rounds: {}", e)))?;
}
Ok(())
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.node.node_id.is_empty() {
return Err(ConfigError::InvalidValue(
"node_id cannot be empty".to_string(),
));
}
if self.node.log_level.is_empty() {
return Err(ConfigError::InvalidValue(
"log_level cannot be empty".to_string(),
));
}
match self.node.log_level.as_str() {
"trace" | "debug" | "info" | "warn" | "error" => {}
_ => {
return Err(ConfigError::InvalidValue(format!(
"Invalid log_level: {}",
self.node.log_level
)))
}
}
if self.network.port == 0 {
return Err(ConfigError::InvalidValue("port cannot be 0".to_string()));
}
if self.network.max_peers == 0 {
return Err(ConfigError::InvalidValue(
"max_peers must be > 0".to_string(),
));
}
if self.network.max_peers > 10000 {
return Err(ConfigError::InvalidValue(
"max_peers must be <= 10000".to_string(),
));
}
if self.network.connect_timeout.is_zero() {
return Err(ConfigError::InvalidValue(
"connect_timeout must be > 0".to_string(),
));
}
if self.network.connect_timeout > Duration::from_secs(300) {
return Err(ConfigError::InvalidValue(
"connect_timeout must be <= 300s".to_string(),
));
}
if self.consensus.finality_threshold <= 0.0 || self.consensus.finality_threshold > 1.0 {
return Err(ConfigError::InvalidValue(
"finality_threshold must be between 0.0 and 1.0".to_string(),
));
}
if self.consensus.round_timeout.is_zero() {
return Err(ConfigError::InvalidValue(
"round_timeout must be > 0".to_string(),
));
}
if self.consensus.max_rounds == 0 {
return Err(ConfigError::InvalidValue(
"max_rounds must be > 0".to_string(),
));
}
if self.consensus.max_rounds > 1000 {
return Err(ConfigError::InvalidValue(
"max_rounds must be <= 1000".to_string(),
));
}
Ok(())
}
pub fn save_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), ConfigError> {
let content = serde_json::to_string_pretty(self)?;
fs::write(path, content)?;
Ok(())
}
pub fn save_to_toml<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), ConfigError> {
let content = toml::to_string_pretty(self).map_err(|e| {
ConfigError::SerializationError(serde_json::Error::io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e,
)))
})?;
fs::write(path, content)?;
Ok(())
}
}