1use std::collections::HashMap;
10use crate::entry_point::config_file::toml_str::{unquote, Quotes};
11
12pub type SectionMap = HashMap<String, Vec<(String, String)>>;
14
15pub fn parse(toml: &str) -> SectionMap {
22 let mut map: SectionMap = HashMap::new();
23
24 let mut outer_name: Option<String> = None; let mut outer_idx: usize = 0;
27 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();
32
33 for line in logical_lines(toml) {
34
35 if line.is_empty() {
36 continue;
37 }
38
39 if line.starts_with("[[") && line.ends_with("]]") {
40 let name = line[2..line.len() - 2].trim().to_string();
42
43 if let Some(ref on) = outer_name.clone() {
45 let prefix = format!("{}.", on);
46 if name.starts_with(&prefix) {
47 let base = format!("{}{}", outer_section_base(on, outer_idx), &name[on.len()..]);
49 let cnt = inner_counters.entry(base.clone()).or_insert(0);
50 let section_path = format!("{}[{}]", base, cnt);
51 *cnt += 1;
52 current_section = section_path.clone();
53 map.entry(current_section.clone()).or_default();
54 continue;
55 }
56 }
57
58 let idx = outer_counters.entry(name.clone()).or_insert(0);
60 outer_idx = *idx;
61 *idx += 1;
62 outer_name = Some(name.clone());
63 inner_counters.clear();
64 current_section = outer_section_base(&name, outer_idx);
65 map.entry(current_section.clone()).or_default();
66 } else if line.starts_with('[') && line.ends_with(']') {
67 let name = line[1..line.len() - 1].trim().to_string();
69
70 if let Some(ref on) = outer_name.clone() {
71 let prefix = format!("{}.", on);
72 if name.starts_with(&prefix) {
73 current_section = format!(
75 "{}{}",
76 outer_section_base(on, outer_idx),
77 &name[on.len()..]
78 );
79 map.entry(current_section.clone()).or_default();
80 continue;
81 }
82 }
83
84 outer_name = None;
86 inner_counters.clear();
87 current_section = name.clone();
88 map.entry(current_section.clone()).or_default();
89 } else if let Some(eq) = line.find('=') {
90 let key = line[..eq].trim().to_string();
92 let raw_val = line[eq + 1..].trim().to_string();
93
94 if raw_val.starts_with('{') {
95 let inner = &raw_val[1..raw_val.rfind('}').unwrap_or(raw_val.len())];
101 let sub_section = if current_section.is_empty() { key.clone() } else { format!("{}.{}", current_section, key) };
102 let entries = map.entry(sub_section).or_default();
103 for part in split_inline_table(inner) {
104 if let Some(ieq) = part.find('=') {
105 let ik = part[..ieq].trim().to_string();
106 let iv = parse_value(part[ieq + 1..].trim());
107 entries.push((ik, iv));
108 }
109 }
110 } else {
111 let value = parse_value(&raw_val);
112 map.entry(current_section.clone())
113 .or_default()
114 .push((key, value));
115 }
116 }
117 }
118
119 map
120}
121
122fn outer_section_base(name: &str, idx: usize) -> String {
124 format!("{}[{}]", name, idx)
125}
126
127#[doc(hidden)]
133pub fn logical_lines(toml: &str) -> Vec<String> {
134 let mut out = Vec::new();
135 let mut pending: Option<(String, i64)> = None;
136 for raw in toml.lines() {
137 let line = strip_comment(raw).trim();
138 if let Some((joined, depth)) = pending.as_mut() {
139 if !line.is_empty() {
140 joined.push(' ');
141 joined.push_str(line);
142 *depth += bracket_depth(line);
143 }
144 if *depth <= 0 {
145 out.push(pending.take().expect("pending").0);
146 }
147 continue;
148 }
149 let opens_array = line.split_once('=').is_some_and(|(key, value)| !key.trim().starts_with('[') && value.trim_start().starts_with('['));
150 let depth = if opens_array { bracket_depth(line) } else { 0 };
151 if depth > 0 {
152 pending = Some((line.to_string(), depth));
153 } else {
154 out.push(line.to_string());
155 }
156 }
157 if let Some((joined, _)) = pending {
158 out.push(joined);
159 }
160 out
161}
162
163fn bracket_depth(line: &str) -> i64 {
165 let mut depth = 0;
166 let mut quotes = Quotes::default();
167 for c in line.chars() {
168 if quotes.step(c) {
169 continue;
170 }
171 match c {
172 '[' => depth += 1,
173 ']' => depth -= 1,
174 _ => {}
175 }
176 }
177 depth
178}
179
180#[doc(hidden)]
181pub fn strip_comment(line: &str) -> &str {
182 let mut quotes = Quotes::default();
183 for (i, c) in line.char_indices() {
184 if !quotes.step(c) && c == '#' {
185 return &line[..i];
186 }
187 }
188 line
189}
190
191#[doc(hidden)]
198pub fn parse_value(raw: &str) -> String {
199 let raw = raw.trim();
200 if raw.starts_with('[') && raw.ends_with(']') {
201 let inner = &raw[1..raw.len() - 1];
203 let items: Vec<String> = split_array_items(inner)
204 .into_iter()
205 .map(|s| unquote(s.trim()))
206 .collect();
207 return items.join(",");
208 }
209 unquote(raw)
210}
211
212fn split_array_items(inner: &str) -> Vec<&str> {
215 let mut items = Vec::new();
216 let mut depth = 0i32;
217 let mut quotes = Quotes::default();
218 let mut start = 0;
219 for (i, c) in inner.char_indices() {
220 if quotes.step(c) {
221 continue;
222 }
223 match c {
224 '[' | '{' => depth += 1,
225 ']' | '}' => depth -= 1,
226 ',' if depth == 0 => {
227 items.push(inner[start..i].trim());
228 start = i + 1;
229 }
230 _ => {}
231 }
232 }
233 let last = inner[start..].trim();
234 if !last.is_empty() {
235 items.push(last);
236 }
237 items
238}
239
240fn split_inline_table(inner: &str) -> Vec<String> {
242 let mut parts = Vec::new();
243 let mut depth = 0i32;
244 let mut quotes = Quotes::default();
245 let mut start = 0;
246 for (i, c) in inner.char_indices() {
247 if quotes.step(c) {
248 continue;
249 }
250 match c {
251 '[' | '{' => depth += 1,
252 ']' | '}' => depth -= 1,
253 ',' if depth == 0 => {
254 let part = inner[start..i].trim();
255 if !part.is_empty() {
256 parts.push(part.to_string());
257 }
258 start = i + 1;
259 }
260 _ => {}
261 }
262 }
263 let part = inner[start..].trim();
264 if !part.is_empty() {
265 parts.push(part.to_string());
266 }
267 parts
268}
269
270#[doc(hidden)]
274pub fn get(map: &SectionMap, section: &str, key: &str) -> Option<String> {
275 map.get(section)?.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
276}
277
278#[doc(hidden)]
280pub fn get_str(map: &SectionMap, section: &str, key: &str) -> String {
281 get(map, section, key).unwrap_or_default()
282}
283
284#[doc(hidden)]
286pub fn get_u64(map: &SectionMap, section: &str, key: &str, default: u64) -> u64 {
287 get(map, section, key)
288 .and_then(|v| v.parse().ok())
289 .unwrap_or(default)
290}
291
292#[doc(hidden)]
294pub fn get_u32(map: &SectionMap, section: &str, key: &str, default: u32) -> u32 {
295 get(map, section, key)
296 .and_then(|v| v.parse().ok())
297 .unwrap_or(default)
298}
299
300#[doc(hidden)]
302pub fn get_array(map: &SectionMap, section: &str, key: &str) -> Vec<String> {
303 match get(map, section, key) {
304 Some(v) if !v.is_empty() => v.split(',').map(|s| s.trim().to_string()).collect(),
305 _ => vec![],
306 }
307}
308
309#[doc(hidden)]
311pub fn section_exists(map: &SectionMap, section: &str) -> bool {
312 map.contains_key(section)
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 const SAMPLE: &str = r#"
320[server]
321ip = "0.0.0.0"
322port = 8080
323
324[[upstream]]
325name = "api"
326backends = ["backend1:8080", "backend2:8080"]
327strategy = "round_robin"
328
329[upstream.health_check]
330path = "/health"
331interval_secs = 10
332timeout_ms = 2000
333healthy_threshold = 2
334unhealthy_threshold = 3
335
336[[route]]
337name = "api-route"
338
339[route.match]
340path = "/api/*"
341method = "GET"
342
343[route.action]
344type = "proxy"
345
346[route.action.proxy]
347upstream = "api"
348connect_timeout_ms = 5000
349read_timeout_ms = 30000
350
351[route.middleware]
352
353[route.middleware.rate_limit]
354max_requests = 100
355window_secs = 60
356
357[[route.middleware.rewrite.request]]
358type = "header_set"
359name = "X-Env"
360value = "production"
361
362[[tcp_proxy]]
363name = "db-proxy"
364listen = "0.0.0.0:5432"
365backends = ["db1:5432", "db2:5432"]
366connect_timeout_ms = 3000
367"#;
368
369 #[test]
370 fn parse_server_section() {
371 let map = parse(SAMPLE);
372 assert_eq!(get(&map, "server", "ip").as_deref(), Some("0.0.0.0"));
373 assert_eq!(get_u64(&map, "server", "port", 0), 8080);
374 }
375
376 #[test]
377 fn parse_upstream() {
378 let map = parse(SAMPLE);
379 assert!(section_exists(&map, "upstream[0]"));
380 assert_eq!(get_str(&map, "upstream[0]", "name"), "api");
381 assert_eq!(
382 get_array(&map, "upstream[0]", "backends"),
383 vec!["backend1:8080", "backend2:8080"]
384 );
385 }
386
387 #[test]
388 fn parse_upstream_health_check() {
389 let map = parse(SAMPLE);
390 assert!(section_exists(&map, "upstream[0].health_check"));
391 assert_eq!(get_str(&map, "upstream[0].health_check", "path"), "/health");
392 assert_eq!(get_u64(&map, "upstream[0].health_check", "interval_secs", 0), 10);
393 }
394
395 #[test]
396 fn parse_route() {
397 let map = parse(SAMPLE);
398 assert!(section_exists(&map, "route[0]"));
399 assert_eq!(get_str(&map, "route[0]", "name"), "api-route");
400 assert_eq!(get_str(&map, "route[0].match", "path"), "/api/*");
401 assert_eq!(get_str(&map, "route[0].match", "method"), "GET");
402 assert_eq!(get_str(&map, "route[0].action.proxy", "upstream"), "api");
403 assert_eq!(get_u64(&map, "route[0].action.proxy", "connect_timeout_ms", 0), 5000);
404 }
405
406 #[test]
407 fn parse_route_middleware() {
408 let map = parse(SAMPLE);
409 assert_eq!(get_u32(&map, "route[0].middleware.rate_limit", "max_requests", 0), 100);
410 assert_eq!(get_u64(&map, "route[0].middleware.rate_limit", "window_secs", 0), 60);
411 }
412
413 #[test]
414 fn parse_nested_rewrite_array() {
415 let map = parse(SAMPLE);
416 assert!(section_exists(&map, "route[0].middleware.rewrite.request[0]"));
417 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "type"), "header_set");
418 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "name"), "X-Env");
419 }
420
421 #[test]
422 fn parse_tcp_proxy() {
423 let map = parse(SAMPLE);
424 assert!(section_exists(&map, "tcp_proxy[0]"));
425 assert_eq!(get_str(&map, "tcp_proxy[0]", "name"), "db-proxy");
426 assert_eq!(get_str(&map, "tcp_proxy[0]", "listen"), "0.0.0.0:5432");
427 assert_eq!(
428 get_array(&map, "tcp_proxy[0]", "backends"),
429 vec!["db1:5432", "db2:5432"]
430 );
431 }
432
433 #[test]
434 fn multi_line_arrays_are_joined() {
435 let toml = "[authz]\ntuples = [\n \"doc:1#owner@user:a\", # first\n \"doc:2#owner@user:[b]\",\n]\nstore = \"memory\"\n";
436 let map = parse(toml);
437 assert_eq!(get_array(&map, "authz", "tuples"), ["doc:1#owner@user:a", "doc:2#owner@user:[b]"]);
438 assert_eq!(get_str(&map, "authz", "store"), "memory");
439 assert_eq!(logical_lines("a = [1,\n2]\n[t]\nb = []"), ["a = [1, 2]", "[t]", "b = []"]);
440 assert_eq!(logical_lines("a = [1,\n2"), ["a = [1, 2"], "an unclosed array ends with the file");
441 }
442
443 #[test]
444 fn strip_comments_test() {
445 assert_eq!(strip_comment("key = \"value\" # a comment"), "key = \"value\" ");
446 assert_eq!(strip_comment("# full comment"), "");
447 assert_eq!(strip_comment("url = \"http://example.com#fragment\""), "url = \"http://example.com#fragment\"");
448 }
449
450 #[test]
451 fn parse_value_array() {
452 assert_eq!(parse_value(r#"["a", "b", "c"]"#), "a,b,c");
453 }
454
455 #[test]
456 fn parse_value_string() {
457 assert_eq!(parse_value(r#""hello""#), "hello");
458 }
459}