use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Config {
pub version: String,
pub json_engine: JsonEngineConfig,
pub security: SecurityConfig,
}
#[derive(Debug, Clone)]
pub struct JsonEngineConfig {
pub schema_version: String,
pub strict_validation: bool,
pub output: OutputConfig,
pub aggregation: AggregationConfig,
pub training: TrainingConfig,
}
#[derive(Debug, Clone)]
pub struct OutputConfig {
pub base_path: String,
pub channels: Channels,
}
#[derive(Debug, Clone)]
pub struct Channels {
pub events: ChannelConfig,
pub quality_metrics: ChannelConfig,
pub training_samples: ChannelConfig,
pub aggregated_stats: AggregatedStatsConfig,
}
#[derive(Debug, Clone)]
pub struct ChannelConfig {
pub enabled: bool,
pub filename: String,
pub buffer_size: usize,
pub auto_flush_seconds: u64,
}
#[derive(Debug, Clone)]
pub struct AggregatedStatsConfig {
pub enabled: bool,
pub filename: String,
pub window_minutes: u64,
}
#[derive(Debug, Clone)]
pub struct AggregationConfig {
pub enabled: bool,
pub window_minutes: u64,
pub compute_percentiles: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct TrainingConfig {
pub auto_generate_samples: bool,
pub gold_threshold: f64,
pub silver_threshold: f64,
pub bronze_threshold: f64,
}
#[derive(Debug, Clone)]
pub struct SecurityConfig {
pub rate_limiting: RateLimitConfig,
}
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
pub enabled: bool,
pub requests_per_minute: u64,
pub burst: u64,
}
impl Default for Config {
fn default() -> Self {
Self {
version: "0.0.1".to_string(),
json_engine: JsonEngineConfig {
schema_version: "v1".to_string(),
strict_validation: false,
output: OutputConfig {
base_path: "./output".to_string(),
channels: Channels {
events: ChannelConfig {
enabled: true,
filename: "events.jsonl".to_string(),
buffer_size: 100,
auto_flush_seconds: 60,
},
quality_metrics: ChannelConfig {
enabled: true,
filename: "quality.jsonl".to_string(),
buffer_size: 100,
auto_flush_seconds: 60,
},
training_samples: ChannelConfig {
enabled: true,
filename: "training.jsonl".to_string(),
buffer_size: 100,
auto_flush_seconds: 60,
},
aggregated_stats: AggregatedStatsConfig {
enabled: false,
filename: "aggregated.jsonl".to_string(),
window_minutes: 60,
},
},
},
aggregation: AggregationConfig {
enabled: false,
window_minutes: 60,
compute_percentiles: vec![50, 95, 99],
},
training: TrainingConfig {
auto_generate_samples: false,
gold_threshold: 0.9,
silver_threshold: 0.7,
bronze_threshold: 0.5,
},
},
security: SecurityConfig {
rate_limiting: RateLimitConfig {
enabled: false,
requests_per_minute: 0,
burst: 0,
},
},
}
}
}
pub fn atomic_append(
path: &Path,
data: &[u8],
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::fs::{rename, OpenOptions};
use std::io::Write;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut existing: Vec<u8> = match std::fs::read(path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(e) => return Err(Box::new(e)),
};
existing.extend_from_slice(data);
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos();
let tmp_path = path.with_extension(format!("tmp.{}", ts));
{
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp_path)?;
f.write_all(&existing)?;
f.sync_all()?;
}
rename(&tmp_path, path)?;
Ok(())
}
impl Config {
pub fn load(config_path: &Path) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let used_config_path = config_path.to_path_buf();
std::mem::drop(used_config_path);
Ok(Config::default())
}
pub fn resolve_output_dir(&self, config_dir: &Path) -> PathBuf {
let base = PathBuf::from(&self.json_engine.output.base_path);
if base.is_absolute() {
base
} else {
config_dir
.join(&base)
.canonicalize()
.unwrap_or_else(|_| config_dir.join(&base))
}
}
pub fn validate_runtime(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if self.version.trim().is_empty() {
return Err("config.version must not be empty".into());
}
if self.json_engine.schema_version.trim().is_empty() {
return Err("json_engine.schema_version must not be empty".into());
}
if self.json_engine.output.base_path.trim().is_empty() {
return Err("json_engine.output.base_path must not be empty".into());
}
Ok(())
}
}