use anyhow::{Context, Result};
use crate::abi::ConfigFormat;
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedEntry {
pub key: String,
pub value: String,
pub value_type: ValueType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueType {
Int,
Float,
String,
Bool,
Array,
Table,
}
pub fn parse_config_file(path: &str, format: ConfigFormat) -> Result<Vec<ParsedEntry>> {
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read config: {}", path))?;
parse_config_string(&content, format)
}
pub fn parse_config_string(content: &str, format: ConfigFormat) -> Result<Vec<ParsedEntry>> {
match format {
ConfigFormat::Toml => parse_toml(content),
ConfigFormat::Json => parse_json(content),
ConfigFormat::Yaml => parse_yaml(content),
ConfigFormat::Ini => parse_ini(content),
}
}
fn parse_toml(content: &str) -> Result<Vec<ParsedEntry>> {
let table: toml::Table =
content.parse().with_context(|| "Failed to parse TOML content")?;
let mut entries = Vec::new();
flatten_toml_value(&toml::Value::Table(table), "", &mut entries);
Ok(entries)
}
fn flatten_toml_value(value: &toml::Value, prefix: &str, entries: &mut Vec<ParsedEntry>) {
match value {
toml::Value::Table(table) => {
for (key, val) in table {
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
flatten_toml_value(val, &full_key, entries);
}
}
toml::Value::Array(arr) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: format!("{:?}", arr),
value_type: ValueType::Array,
});
}
toml::Value::String(s) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: s.clone(),
value_type: ValueType::String,
});
}
toml::Value::Integer(i) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: i.to_string(),
value_type: ValueType::Int,
});
}
toml::Value::Float(f) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: f.to_string(),
value_type: ValueType::Float,
});
}
toml::Value::Boolean(b) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: b.to_string(),
value_type: ValueType::Bool,
});
}
toml::Value::Datetime(dt) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: dt.to_string(),
value_type: ValueType::String,
});
}
}
}
fn parse_json(content: &str) -> Result<Vec<ParsedEntry>> {
let json_value: JsonValue =
parse_json_value(content.trim()).with_context(|| "Failed to parse JSON content")?;
let mut entries = Vec::new();
flatten_json_value(&json_value, "", &mut entries);
Ok(entries)
}
#[derive(Debug, Clone)]
enum JsonValue {
Null,
Bool(bool),
Number(f64),
Str(String),
Array(Vec<JsonValue>),
Object(Vec<(String, JsonValue)>),
}
fn parse_json_value(input: &str) -> Result<JsonValue> {
let input = input.trim();
if input.is_empty() {
anyhow::bail!("Empty JSON input");
}
match input.as_bytes()[0] {
b'{' => parse_json_object(input),
b'[' => parse_json_array(input),
b'"' => parse_json_string(input).map(|(s, _)| JsonValue::Str(s)),
b't' if input.starts_with("true") => Ok(JsonValue::Bool(true)),
b'f' if input.starts_with("false") => Ok(JsonValue::Bool(false)),
b'n' if input.starts_with("null") => Ok(JsonValue::Null),
_ => {
let end = input
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+' && c != 'e' && c != 'E')
.unwrap_or(input.len());
let num_str = &input[..end];
let num: f64 = num_str
.parse()
.with_context(|| format!("Invalid JSON number: {}", num_str))?;
Ok(JsonValue::Number(num))
}
}
}
fn parse_json_object(input: &str) -> Result<JsonValue> {
let inner = find_matching_brace(input, b'{', b'}')?;
let inner = inner.trim();
if inner.is_empty() {
return Ok(JsonValue::Object(Vec::new()));
}
let mut entries = Vec::new();
let mut rest = inner;
while !rest.trim().is_empty() {
rest = rest.trim();
let (key, after_key) = parse_json_string(rest)?;
let after_key = after_key.trim();
if !after_key.starts_with(':') {
anyhow::bail!("Expected ':' after key in JSON object");
}
let value_str = after_key[1..].trim();
let (value, after_value) = parse_json_value_with_rest(value_str)?;
entries.push((key, value));
let after_value = after_value.trim();
if after_value.starts_with(',') {
rest = &after_value[1..];
} else {
break;
}
}
Ok(JsonValue::Object(entries))
}
fn parse_json_array(input: &str) -> Result<JsonValue> {
let inner = find_matching_brace(input, b'[', b']')?;
let inner = inner.trim();
if inner.is_empty() {
return Ok(JsonValue::Array(Vec::new()));
}
let mut items = Vec::new();
let mut rest = inner;
while !rest.trim().is_empty() {
rest = rest.trim();
let (value, after_value) = parse_json_value_with_rest(rest)?;
items.push(value);
let after_value = after_value.trim();
if after_value.starts_with(',') {
rest = &after_value[1..];
} else {
break;
}
}
Ok(JsonValue::Array(items))
}
fn parse_json_string(input: &str) -> Result<(String, &str)> {
let input = input.trim();
if !input.starts_with('"') {
anyhow::bail!("Expected '\"' at start of JSON string");
}
let bytes = input.as_bytes();
let mut i = 1;
let mut result = String::new();
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'"' => { result.push('"'); i += 2; }
b'\\' => { result.push('\\'); i += 2; }
b'n' => { result.push('\n'); i += 2; }
b't' => { result.push('\t'); i += 2; }
b'r' => { result.push('\r'); i += 2; }
b'/' => { result.push('/'); i += 2; }
_ => { result.push(bytes[i + 1] as char); i += 2; }
}
} else if bytes[i] == b'"' {
return Ok((result, &input[i + 1..]));
} else {
result.push(bytes[i] as char);
i += 1;
}
}
anyhow::bail!("Unterminated JSON string");
}
fn parse_json_value_with_rest(input: &str) -> Result<(JsonValue, &str)> {
let input = input.trim();
if input.is_empty() {
anyhow::bail!("Unexpected end of JSON input");
}
match input.as_bytes()[0] {
b'{' => {
let end = find_matching_brace_end(input, b'{', b'}')?;
let val = parse_json_object(&input[..end])?;
Ok((val, &input[end..]))
}
b'[' => {
let end = find_matching_brace_end(input, b'[', b']')?;
let val = parse_json_array(&input[..end])?;
Ok((val, &input[end..]))
}
b'"' => {
let (s, rest) = parse_json_string(input)?;
Ok((JsonValue::Str(s), rest))
}
b't' if input.starts_with("true") => Ok((JsonValue::Bool(true), &input[4..])),
b'f' if input.starts_with("false") => Ok((JsonValue::Bool(false), &input[5..])),
b'n' if input.starts_with("null") => Ok((JsonValue::Null, &input[4..])),
_ => {
let end = input
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+' && c != 'e' && c != 'E')
.unwrap_or(input.len());
let num_str = &input[..end];
let num: f64 = num_str
.parse()
.with_context(|| format!("Invalid JSON number: {}", num_str))?;
Ok((JsonValue::Number(num), &input[end..]))
}
}
}
fn find_matching_brace(input: &str, open: u8, close: u8) -> Result<&str> {
let end = find_matching_brace_end(input, open, close)?;
Ok(&input[1..end - 1])
}
fn find_matching_brace_end(input: &str, open: u8, close: u8) -> Result<usize> {
let bytes = input.as_bytes();
let mut depth = 0;
let mut in_string = false;
let mut i = 0;
while i < bytes.len() {
if in_string {
if bytes[i] == b'\\' {
i += 1; } else if bytes[i] == b'"' {
in_string = false;
}
} else {
if bytes[i] == b'"' {
in_string = true;
} else if bytes[i] == open {
depth += 1;
} else if bytes[i] == close {
depth -= 1;
if depth == 0 {
return Ok(i + 1);
}
}
}
i += 1;
}
anyhow::bail!("Unmatched brace in JSON input");
}
fn flatten_json_value(value: &JsonValue, prefix: &str, entries: &mut Vec<ParsedEntry>) {
match value {
JsonValue::Object(obj) => {
for (key, val) in obj {
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
flatten_json_value(val, &full_key, entries);
}
}
JsonValue::Array(arr) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: format!("{:?}", arr),
value_type: ValueType::Array,
});
}
JsonValue::Str(s) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: s.clone(),
value_type: ValueType::String,
});
}
JsonValue::Number(n) => {
if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: (*n as i64).to_string(),
value_type: ValueType::Int,
});
} else {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: n.to_string(),
value_type: ValueType::Float,
});
}
}
JsonValue::Bool(b) => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: b.to_string(),
value_type: ValueType::Bool,
});
}
JsonValue::Null => {
entries.push(ParsedEntry {
key: prefix.to_string(),
value: "null".to_string(),
value_type: ValueType::String,
});
}
}
}
fn parse_yaml(content: &str) -> Result<Vec<ParsedEntry>> {
let mut entries = Vec::new();
let mut stack: Vec<(usize, String)> = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed == "---" || trimmed == "..." {
continue;
}
let indent = line.len() - line.trim_start().len();
while let Some(&(level, _)) = stack.last() {
if level >= indent {
stack.pop();
} else {
break;
}
}
if let Some(colon_pos) = trimmed.find(':') {
let key = trimmed[..colon_pos].trim().to_string();
let value_part = trimmed[colon_pos + 1..].trim();
let prefix = stack
.last()
.map(|(_, p)| format!("{}.{}", p, key))
.unwrap_or_else(|| key.clone());
if value_part.is_empty() {
stack.push((indent, prefix));
} else {
let (value, value_type) = classify_yaml_value(value_part);
entries.push(ParsedEntry {
key: prefix,
value,
value_type,
});
}
}
}
Ok(entries)
}
fn classify_yaml_value(s: &str) -> (String, ValueType) {
match s.to_lowercase().as_str() {
"true" | "yes" | "on" => return ("true".to_string(), ValueType::Bool),
"false" | "no" | "off" => return ("false".to_string(), ValueType::Bool),
"null" | "~" => return ("null".to_string(), ValueType::String),
_ => {}
}
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
return (s[1..s.len() - 1].to_string(), ValueType::String);
}
if let Ok(_) = s.parse::<i64>() {
return (s.to_string(), ValueType::Int);
}
if let Ok(_) = s.parse::<f64>() {
return (s.to_string(), ValueType::Float);
}
(s.to_string(), ValueType::String)
}
fn parse_ini(content: &str) -> Result<Vec<ParsedEntry>> {
let mut entries = Vec::new();
let mut current_section = String::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with(';') || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
current_section = trimmed[1..trimmed.len() - 1].trim().to_string();
continue;
}
if let Some(eq_pos) = trimmed.find('=') {
let key = trimmed[..eq_pos].trim().to_string();
let raw_value = trimmed[eq_pos + 1..].trim().to_string();
let full_key = if current_section.is_empty() {
key
} else {
format!("{}.{}", current_section, key)
};
let (value, value_type) = classify_ini_value(&raw_value);
entries.push(ParsedEntry {
key: full_key,
value,
value_type,
});
}
}
Ok(entries)
}
fn classify_ini_value(s: &str) -> (String, ValueType) {
let s = if let Some(pos) = s.find(';') {
s[..pos].trim()
} else {
s
};
match s.to_lowercase().as_str() {
"true" | "yes" | "on" => return ("true".to_string(), ValueType::Bool),
"false" | "no" | "off" => return ("false".to_string(), ValueType::Bool),
_ => {}
}
if let Ok(_) = s.parse::<i64>() {
return (s.to_string(), ValueType::Int);
}
if let Ok(_) = s.parse::<f64>() {
return (s.to_string(), ValueType::Float);
}
(s.to_string(), ValueType::String)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_toml_simple() {
let content = r#"
[server]
port = 8080
host = "localhost"
debug = true
"#;
let entries = parse_toml(content).unwrap();
assert!(entries.iter().any(|e| e.key == "server.port" && e.value == "8080" && e.value_type == ValueType::Int));
assert!(entries.iter().any(|e| e.key == "server.host" && e.value == "localhost" && e.value_type == ValueType::String));
assert!(entries.iter().any(|e| e.key == "server.debug" && e.value == "true" && e.value_type == ValueType::Bool));
}
#[test]
fn test_parse_json_simple() {
let content = r#"{"port": 8080, "host": "localhost", "debug": true}"#;
let entries = parse_json(content).unwrap();
assert!(entries.iter().any(|e| e.key == "port" && e.value == "8080" && e.value_type == ValueType::Int));
assert!(entries.iter().any(|e| e.key == "host" && e.value == "localhost" && e.value_type == ValueType::String));
assert!(entries.iter().any(|e| e.key == "debug" && e.value == "true" && e.value_type == ValueType::Bool));
}
#[test]
fn test_parse_yaml_simple() {
let content = "server:\n port: 8080\n host: localhost\n debug: true\n";
let entries = parse_yaml(content).unwrap();
assert!(entries.iter().any(|e| e.key == "server.port" && e.value == "8080" && e.value_type == ValueType::Int));
assert!(entries.iter().any(|e| e.key == "server.host" && e.value == "localhost" && e.value_type == ValueType::String));
assert!(entries.iter().any(|e| e.key == "server.debug" && e.value == "true" && e.value_type == ValueType::Bool));
}
#[test]
fn test_parse_ini_simple() {
let content = "[server]\nport = 8080\nhost = localhost\ndebug = true\n";
let entries = parse_ini(content).unwrap();
assert!(entries.iter().any(|e| e.key == "server.port" && e.value == "8080" && e.value_type == ValueType::Int));
assert!(entries.iter().any(|e| e.key == "server.host" && e.value == "localhost" && e.value_type == ValueType::String));
assert!(entries.iter().any(|e| e.key == "server.debug" && e.value == "true" && e.value_type == ValueType::Bool));
}
}