use std::collections::HashMap;
use crate::entry_point::config_file::toml_str::{unquote, Quotes};
pub type SectionMap = HashMap<String, Vec<(String, String)>>;
pub fn parse(toml: &str) -> SectionMap {
let mut map: SectionMap = HashMap::new();
let mut outer_name: Option<String> = None; let mut outer_idx: usize = 0;
let mut outer_counters: HashMap<String, usize> = HashMap::new(); let mut inner_counters: HashMap<String, usize> = HashMap::new();
let mut current_section: String = String::new();
for line in logical_lines(toml) {
if line.is_empty() {
continue;
}
if line.starts_with("[[") && line.ends_with("]]") {
let name = line[2..line.len() - 2].trim().to_string();
if let Some(ref on) = outer_name.clone() {
let prefix = format!("{}.", on);
if name.starts_with(&prefix) {
let base = format!("{}{}", outer_section_base(on, outer_idx), &name[on.len()..]);
let cnt = inner_counters.entry(base.clone()).or_insert(0);
let section_path = format!("{}[{}]", base, cnt);
*cnt += 1;
current_section = section_path.clone();
map.entry(current_section.clone()).or_default();
continue;
}
}
let idx = outer_counters.entry(name.clone()).or_insert(0);
outer_idx = *idx;
*idx += 1;
outer_name = Some(name.clone());
inner_counters.clear();
current_section = outer_section_base(&name, outer_idx);
map.entry(current_section.clone()).or_default();
} else if line.starts_with('[') && line.ends_with(']') {
let name = line[1..line.len() - 1].trim().to_string();
if let Some(ref on) = outer_name.clone() {
let prefix = format!("{}.", on);
if name.starts_with(&prefix) {
current_section = format!(
"{}{}",
outer_section_base(on, outer_idx),
&name[on.len()..]
);
map.entry(current_section.clone()).or_default();
continue;
}
}
outer_name = None;
inner_counters.clear();
current_section = name.clone();
map.entry(current_section.clone()).or_default();
} else if let Some(eq) = line.find('=') {
let key = line[..eq].trim().to_string();
let raw_val = line[eq + 1..].trim().to_string();
if raw_val.starts_with('{') {
let inner = &raw_val[1..raw_val.rfind('}').unwrap_or(raw_val.len())];
let sub_section = if current_section.is_empty() { key.clone() } else { format!("{}.{}", current_section, key) };
let entries = map.entry(sub_section).or_default();
for part in split_inline_table(inner) {
if let Some(ieq) = part.find('=') {
let ik = part[..ieq].trim().to_string();
let iv = parse_value(part[ieq + 1..].trim());
entries.push((ik, iv));
}
}
} else {
let value = parse_value(&raw_val);
map.entry(current_section.clone())
.or_default()
.push((key, value));
}
}
}
map
}
fn outer_section_base(name: &str, idx: usize) -> String {
format!("{}[{}]", name, idx)
}
pub(crate) fn logical_lines(toml: &str) -> Vec<String> {
let mut out = Vec::new();
let mut pending: Option<(String, i64)> = None;
for raw in toml.lines() {
let line = strip_comment(raw).trim();
if let Some((joined, depth)) = pending.as_mut() {
if !line.is_empty() {
joined.push(' ');
joined.push_str(line);
*depth += bracket_depth(line);
}
if *depth <= 0 {
out.push(pending.take().expect("pending").0);
}
continue;
}
let opens_array = line.split_once('=').is_some_and(|(key, value)| !key.trim().starts_with('[') && value.trim_start().starts_with('['));
let depth = if opens_array { bracket_depth(line) } else { 0 };
if depth > 0 {
pending = Some((line.to_string(), depth));
} else {
out.push(line.to_string());
}
}
if let Some((joined, _)) = pending {
out.push(joined);
}
out
}
fn bracket_depth(line: &str) -> i64 {
let mut depth = 0;
let mut quotes = Quotes::default();
for c in line.chars() {
if quotes.step(c) {
continue;
}
match c {
'[' => depth += 1,
']' => depth -= 1,
_ => {}
}
}
depth
}
pub(crate) fn strip_comment(line: &str) -> &str {
let mut quotes = Quotes::default();
for (i, c) in line.char_indices() {
if !quotes.step(c) && c == '#' {
return &line[..i];
}
}
line
}
pub(crate) fn parse_value(raw: &str) -> String {
let raw = raw.trim();
if raw.starts_with('[') && raw.ends_with(']') {
let inner = &raw[1..raw.len() - 1];
let items: Vec<String> = split_array_items(inner)
.into_iter()
.map(|s| unquote(s.trim()))
.collect();
return items.join(",");
}
unquote(raw)
}
fn split_array_items(inner: &str) -> Vec<&str> {
let mut items = Vec::new();
let mut depth = 0i32;
let mut quotes = Quotes::default();
let mut start = 0;
for (i, c) in inner.char_indices() {
if quotes.step(c) {
continue;
}
match c {
'[' | '{' => depth += 1,
']' | '}' => depth -= 1,
',' if depth == 0 => {
items.push(inner[start..i].trim());
start = i + 1;
}
_ => {}
}
}
let last = inner[start..].trim();
if !last.is_empty() {
items.push(last);
}
items
}
fn split_inline_table(inner: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0i32;
let mut quotes = Quotes::default();
let mut start = 0;
for (i, c) in inner.char_indices() {
if quotes.step(c) {
continue;
}
match c {
'[' | '{' => depth += 1,
']' | '}' => depth -= 1,
',' if depth == 0 => {
let part = inner[start..i].trim();
if !part.is_empty() {
parts.push(part.to_string());
}
start = i + 1;
}
_ => {}
}
}
let part = inner[start..].trim();
if !part.is_empty() {
parts.push(part.to_string());
}
parts
}
pub(crate) fn get(map: &SectionMap, section: &str, key: &str) -> Option<String> {
map.get(section)?.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
pub(crate) fn get_str(map: &SectionMap, section: &str, key: &str) -> String {
get(map, section, key).unwrap_or_default()
}
pub(crate) fn get_u64(map: &SectionMap, section: &str, key: &str, default: u64) -> u64 {
get(map, section, key)
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
pub(crate) fn get_u32(map: &SectionMap, section: &str, key: &str, default: u32) -> u32 {
get(map, section, key)
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
pub(crate) fn get_array(map: &SectionMap, section: &str, key: &str) -> Vec<String> {
match get(map, section, key) {
Some(v) if !v.is_empty() => v.split(',').map(|s| s.trim().to_string()).collect(),
_ => vec![],
}
}
pub(crate) fn section_exists(map: &SectionMap, section: &str) -> bool {
map.contains_key(section)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"
[server]
ip = "0.0.0.0"
port = 8080
[[upstream]]
name = "api"
backends = ["backend1:8080", "backend2:8080"]
strategy = "round_robin"
[upstream.health_check]
path = "/health"
interval_secs = 10
timeout_ms = 2000
healthy_threshold = 2
unhealthy_threshold = 3
[[route]]
name = "api-route"
[route.match]
path = "/api/*"
method = "GET"
[route.action]
type = "proxy"
[route.action.proxy]
upstream = "api"
connect_timeout_ms = 5000
read_timeout_ms = 30000
[route.middleware]
[route.middleware.rate_limit]
max_requests = 100
window_secs = 60
[[route.middleware.rewrite.request]]
type = "header_set"
name = "X-Env"
value = "production"
[[tcp_proxy]]
name = "db-proxy"
listen = "0.0.0.0:5432"
backends = ["db1:5432", "db2:5432"]
connect_timeout_ms = 3000
"#;
#[test]
fn parse_server_section() {
let map = parse(SAMPLE);
assert_eq!(get(&map, "server", "ip").as_deref(), Some("0.0.0.0"));
assert_eq!(get_u64(&map, "server", "port", 0), 8080);
}
#[test]
fn parse_upstream() {
let map = parse(SAMPLE);
assert!(section_exists(&map, "upstream[0]"));
assert_eq!(get_str(&map, "upstream[0]", "name"), "api");
assert_eq!(
get_array(&map, "upstream[0]", "backends"),
vec!["backend1:8080", "backend2:8080"]
);
}
#[test]
fn parse_upstream_health_check() {
let map = parse(SAMPLE);
assert!(section_exists(&map, "upstream[0].health_check"));
assert_eq!(get_str(&map, "upstream[0].health_check", "path"), "/health");
assert_eq!(get_u64(&map, "upstream[0].health_check", "interval_secs", 0), 10);
}
#[test]
fn parse_route() {
let map = parse(SAMPLE);
assert!(section_exists(&map, "route[0]"));
assert_eq!(get_str(&map, "route[0]", "name"), "api-route");
assert_eq!(get_str(&map, "route[0].match", "path"), "/api/*");
assert_eq!(get_str(&map, "route[0].match", "method"), "GET");
assert_eq!(get_str(&map, "route[0].action.proxy", "upstream"), "api");
assert_eq!(get_u64(&map, "route[0].action.proxy", "connect_timeout_ms", 0), 5000);
}
#[test]
fn parse_route_middleware() {
let map = parse(SAMPLE);
assert_eq!(get_u32(&map, "route[0].middleware.rate_limit", "max_requests", 0), 100);
assert_eq!(get_u64(&map, "route[0].middleware.rate_limit", "window_secs", 0), 60);
}
#[test]
fn parse_nested_rewrite_array() {
let map = parse(SAMPLE);
assert!(section_exists(&map, "route[0].middleware.rewrite.request[0]"));
assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "type"), "header_set");
assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "name"), "X-Env");
}
#[test]
fn parse_tcp_proxy() {
let map = parse(SAMPLE);
assert!(section_exists(&map, "tcp_proxy[0]"));
assert_eq!(get_str(&map, "tcp_proxy[0]", "name"), "db-proxy");
assert_eq!(get_str(&map, "tcp_proxy[0]", "listen"), "0.0.0.0:5432");
assert_eq!(
get_array(&map, "tcp_proxy[0]", "backends"),
vec!["db1:5432", "db2:5432"]
);
}
#[test]
fn multi_line_arrays_are_joined() {
let toml = "[authz]\ntuples = [\n \"doc:1#owner@user:a\", # first\n \"doc:2#owner@user:[b]\",\n]\nstore = \"memory\"\n";
let map = parse(toml);
assert_eq!(get_array(&map, "authz", "tuples"), ["doc:1#owner@user:a", "doc:2#owner@user:[b]"]);
assert_eq!(get_str(&map, "authz", "store"), "memory");
assert_eq!(logical_lines("a = [1,\n2]\n[t]\nb = []"), ["a = [1, 2]", "[t]", "b = []"]);
assert_eq!(logical_lines("a = [1,\n2"), ["a = [1, 2"], "an unclosed array ends with the file");
}
#[test]
fn strip_comments_test() {
assert_eq!(strip_comment("key = \"value\" # a comment"), "key = \"value\" ");
assert_eq!(strip_comment("# full comment"), "");
assert_eq!(strip_comment("url = \"http://example.com#fragment\""), "url = \"http://example.com#fragment\"");
}
#[test]
fn parse_value_array() {
assert_eq!(parse_value(r#"["a", "b", "c"]"#), "a,b,c");
}
#[test]
fn parse_value_string() {
assert_eq!(parse_value(r#""hello""#), "hello");
}
}