use std::collections::HashMap;
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use sha2::{Sha256, Digest};
use bincode;
const MAGIC: &[u8; 4] = b"PNUT";
const VERSION: u32 = 1;
#[derive(Debug, Clone)]
pub struct ConfigFile {
pub path: PathBuf,
pub file_type: ConfigType,
pub mtime: SystemTime,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigType {
Binary,
Tsk,
Text,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Value {
Null,
Bool(bool),
Integer(i64),
Float(f64),
String(String),
Array(Vec<Value>),
Object(HashMap<String, Value>),
}
pub struct PeanutConfig {
cache: HashMap<PathBuf, HashMap<String, Value>>,
auto_compile: bool,
watch: bool,
}
impl PeanutConfig {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
auto_compile: true,
watch: true,
}
}
pub fn with_options(auto_compile: bool, watch: bool) -> Self {
Self {
cache: HashMap::new(),
auto_compile,
watch,
}
}
pub fn find_config_hierarchy(&self, start_dir: &Path) -> io::Result<Vec<ConfigFile>> {
let mut configs = Vec::new();
let start_dir = start_dir.canonicalize()?;
let mut current_dir = start_dir.clone();
loop {
let binary_path = current_dir.join("peanu.pnt");
let tsk_path = current_dir.join("peanu.tsk");
let text_path = current_dir.join("peanu.peanuts");
if let Ok(metadata) = binary_path.metadata() {
configs.push(ConfigFile {
path: binary_path,
file_type: ConfigType::Binary,
mtime: metadata.modified()?,
});
} else if let Ok(metadata) = tsk_path.metadata() {
configs.push(ConfigFile {
path: tsk_path,
file_type: ConfigType::Tsk,
mtime: metadata.modified()?,
});
} else if let Ok(metadata) = text_path.metadata() {
configs.push(ConfigFile {
path: text_path,
file_type: ConfigType::Text,
mtime: metadata.modified()?,
});
}
if let Some(parent) = current_dir.parent() {
current_dir = parent.to_path_buf();
} else {
break;
}
}
let global_config = PathBuf::from("peanut.tsk");
if let Ok(metadata) = global_config.metadata() {
configs.insert(0, ConfigFile {
path: global_config,
file_type: ConfigType::Tsk,
mtime: metadata.modified()?,
});
}
configs.reverse();
Ok(configs)
}
pub fn parse_text_config(&self, content: &str) -> Result<HashMap<String, Value>, String> {
let mut config = HashMap::new();
let mut current_section = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
let section_name = line[1..line.len()-1].to_string();
current_section = Some(section_name.clone());
config.insert(section_name, Value::Object(HashMap::new()));
continue;
}
if let Some(colon_idx) = line.find(':') {
let key = line[..colon_idx].trim().to_string();
let value = line[colon_idx+1..].trim();
let parsed_value = self.parse_value(value);
if let Some(ref section) = current_section {
if let Some(Value::Object(ref mut map)) = config.get_mut(section) {
map.insert(key, parsed_value);
}
} else {
config.insert(key, parsed_value);
}
}
}
Ok(config)
}
fn parse_value(&self, value: &str) -> Value {
if (value.starts_with('"') && value.ends_with('"')) ||
(value.starts_with('\'') && value.ends_with('\'')) {
return Value::String(value[1..value.len()-1].to_string());
}
if value == "true" {
return Value::Bool(true);
}
if value == "false" {
return Value::Bool(false);
}
if value.to_lowercase() == "null" {
return Value::Null;
}
if let Ok(int_val) = value.parse::<i64>() {
return Value::Integer(int_val);
}
if let Ok(float_val) = value.parse::<f64>() {
return Value::Float(float_val);
}
if value.contains(',') {
let parts: Vec<Value> = value
.split(',')
.map(|s| self.parse_value(s.trim()))
.collect();
return Value::Array(parts);
}
Value::String(value.to_string())
}
pub fn compile_to_binary(&self, config: &HashMap<String, Value>, output_path: &Path) -> io::Result<()> {
let mut file = fs::File::create(output_path)?;
file.write_all(MAGIC)?;
file.write_all(&VERSION.to_le_bytes())?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
file.write_all(×tamp.to_le_bytes())?;
let config_data = bincode::serialize(config)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let mut hasher = Sha256::new();
hasher.update(&config_data);
let checksum = hasher.finalize();
file.write_all(&checksum[..8])?;
file.write_all(&config_data)?;
let shell_path = output_path.with_extension("shell");
self.compile_to_shell(config, &shell_path)?;
Ok(())
}
fn compile_to_shell(&self, config: &HashMap<String, Value>, output_path: &Path) -> io::Result<()> {
#[derive(Serialize)]
struct ShellFormat {
version: u32,
timestamp: u64,
data: HashMap<String, Value>,
}
let shell_data = ShellFormat {
version: VERSION,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
data: config.clone(),
};
let json = serde_json::to_string_pretty(&shell_data)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
fs::write(output_path, json)?;
Ok(())
}
pub fn load_binary(&self, file_path: &Path) -> io::Result<HashMap<String, Value>> {
let mut file = fs::File::open(file_path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
if data.len() < 24 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Binary file too short"));
}
if &data[0..4] != MAGIC {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid peanut binary file"));
}
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
if version > VERSION {
return Err(io::Error::new(io::ErrorKind::InvalidData,
format!("Unsupported binary version: {}", version)));
}
let stored_checksum = &data[16..24];
let config_data = &data[24..];
let mut hasher = Sha256::new();
hasher.update(config_data);
let calculated_checksum = hasher.finalize();
if stored_checksum != &calculated_checksum[..8] {
return Err(io::Error::new(io::ErrorKind::InvalidData,
"Binary file corrupted (checksum mismatch)"));
}
let config: HashMap<String, Value> = bincode::deserialize(config_data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok(config)
}
fn deep_merge(&self, mut target: HashMap<String, Value>, source: HashMap<String, Value>) -> HashMap<String, Value> {
for (key, value) in source {
match (target.get_mut(&key), value) {
(Some(Value::Object(target_map)), Value::Object(source_map)) => {
let merged = self.deep_merge(target_map.clone(), source_map);
target.insert(key, Value::Object(merged));
}
_ => {
target.insert(key, value);
}
}
}
target
}
pub fn load(&mut self, directory: &Path) -> io::Result<HashMap<String, Value>> {
let abs_dir = directory.canonicalize()?;
if let Some(cached) = self.cache.get(&abs_dir) {
return Ok(cached.clone());
}
let hierarchy = self.find_config_hierarchy(&abs_dir)?;
let mut merged_config = HashMap::new();
for config_file in &hierarchy {
let config = match config_file.file_type {
ConfigType::Binary => self.load_binary(&config_file.path)?,
ConfigType::Tsk | ConfigType::Text => {
let content = fs::read_to_string(&config_file.path)?;
self.parse_text_config(&content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
}
};
merged_config = self.deep_merge(merged_config, config);
}
self.cache.insert(abs_dir.clone(), merged_config.clone());
if self.auto_compile {
self.auto_compile_configs(&hierarchy)?;
}
Ok(merged_config)
}
fn auto_compile_configs(&self, hierarchy: &[ConfigFile]) -> io::Result<()> {
for config_file in hierarchy {
if config_file.file_type == ConfigType::Text || config_file.file_type == ConfigType::Tsk {
let binary_path = config_file.path.with_extension("pntb");
let need_compile = if let Ok(binary_metadata) = binary_path.metadata() {
config_file.mtime > binary_metadata.modified()?
} else {
true
};
if need_compile {
let content = fs::read_to_string(&config_file.path)?;
let config = self.parse_text_config(&content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
self.compile_to_binary(&config, &binary_path)?;
println!("Compiled {} to binary format", config_file.path.display());
}
}
}
Ok(())
}
pub fn get(&mut self, key_path: &str, default: Value, directory: &Path) -> io::Result<Value> {
let config = self.load(directory)?;
let keys: Vec<&str> = key_path.split('.').collect();
let mut current = &Value::Object(config);
for key in keys {
match current {
Value::Object(map) => {
if let Some(value) = map.get(key) {
current = value;
} else {
return Ok(default);
}
}
_ => return Ok(default),
}
}
Ok(current.clone())
}
}
impl Default for PeanutConfig {
fn default() -> Self {
Self::new()
}
}
pub fn benchmark() {
use std::time::Instant;
let config = PeanutConfig::new();
let test_content = r#"
[server]
host: "localhost"
port: 8080
workers: 4
debug: true
[database]
driver: "postgresql"
host: "db.example.com"
port: 5432
pool_size: 10
[cache]
enabled: true
ttl: 3600
backend: "redis"
"#;
println!("🥜 Peanut Configuration Performance Test\n");
let start = Instant::now();
for _ in 0..1000 {
let _ = config.parse_text_config(test_content);
}
let text_time = start.elapsed();
println!("Text parsing (1000 iterations): {:?}", text_time);
let parsed = config.parse_text_config(test_content).unwrap();
let binary_data = bincode::serialize(&parsed).unwrap();
let start = Instant::now();
for _ in 0..1000 {
let _: HashMap<String, Value> = bincode::deserialize(&binary_data).unwrap();
}
let binary_time = start.elapsed();
println!("Binary loading (1000 iterations): {:?}", binary_time);
let improvement = ((text_time.as_nanos() as f64 - binary_time.as_nanos() as f64)
/ text_time.as_nanos() as f64) * 100.0;
println!("\n✨ Binary format is {:.0}% faster than text parsing!", improvement);
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_parse_text_config() {
let config = PeanutConfig::new();
let content = r#"
[server]
host: "localhost"
port: 8080
enabled: true
[database]
connections: 10
"#;
let result = config.parse_text_config(content).unwrap();
if let Some(Value::Object(server)) = result.get("server") {
assert_eq!(server.get("host"), Some(&Value::String("localhost".to_string())));
assert_eq!(server.get("port"), Some(&Value::Integer(8080)));
assert_eq!(server.get("enabled"), Some(&Value::Bool(true)));
} else {
panic!("Server section not found");
}
}
#[test]
fn test_binary_roundtrip() {
let config = PeanutConfig::new();
let temp_dir = TempDir::new().unwrap();
let binary_path = temp_dir.path().join("test.pnt");
let mut test_config = HashMap::new();
test_config.insert("key".to_string(), Value::String("value".to_string()));
test_config.insert("number".to_string(), Value::Integer(42));
config.compile_to_binary(&test_config, &binary_path).unwrap();
let loaded = config.load_binary(&binary_path).unwrap();
assert_eq!(loaded, test_config);
}
}