1use crate::error::ConfigError;
2use crate::value::{HoconValue, ScalarValue};
3use indexmap::IndexMap;
4
5#[derive(Debug, Clone, PartialEq)]
11pub struct Config {
12 root: IndexMap<String, HoconValue>,
13}
14
15impl Config {
16 pub fn new(root: IndexMap<String, HoconValue>) -> Self {
18 Self { root }
19 }
20
21 fn lookup_node(&self, path: &str) -> Option<&HoconValue> {
23 let segments = split_config_path(path);
24 lookup_in_map_by_segments(&self.root, &segments)
25 }
26
27 pub fn get(&self, path: &str) -> Option<&HoconValue> {
30 self.lookup_node(path)
31 }
32
33 pub fn get_string(&self, path: &str) -> Result<String, ConfigError> {
39 match self.lookup_node(path) {
40 None => Err(missing(path)),
41 Some(HoconValue::Scalar(ScalarValue::String(s))) => Ok(s.clone()),
42 Some(HoconValue::Scalar(ScalarValue::Int(n))) => Ok(n.to_string()),
43 Some(HoconValue::Scalar(ScalarValue::Float(f))) => Ok(f.to_string()),
44 Some(HoconValue::Scalar(ScalarValue::Bool(b))) => Ok(b.to_string()),
45 Some(HoconValue::Scalar(ScalarValue::Null)) => Ok("null".to_string()),
46 _ => Err(type_mismatch(path, "String")),
47 }
48 }
49
50 pub fn get_i64(&self, path: &str) -> Result<i64, ConfigError> {
56 match self.lookup_node(path) {
57 None => Err(missing(path)),
58 Some(HoconValue::Scalar(ScalarValue::Int(n))) => Ok(*n),
59 Some(HoconValue::Scalar(ScalarValue::Float(f))) => {
60 if f.fract() == 0.0 && f.is_finite() {
62 Ok(*f as i64)
63 } else {
64 Err(type_mismatch(path, "i64"))
65 }
66 }
67 Some(HoconValue::Scalar(ScalarValue::String(s))) => {
68 s.parse::<i64>().map_err(|_| type_mismatch(path, "i64"))
70 }
71 _ => Err(type_mismatch(path, "i64")),
72 }
73 }
74
75 pub fn get_f64(&self, path: &str) -> Result<f64, ConfigError> {
81 match self.lookup_node(path) {
82 None => Err(missing(path)),
83 Some(HoconValue::Scalar(ScalarValue::Float(f))) => Ok(*f),
84 Some(HoconValue::Scalar(ScalarValue::Int(n))) => Ok(*n as f64),
85 Some(HoconValue::Scalar(ScalarValue::String(s))) => {
86 s.parse::<f64>().map_err(|_| type_mismatch(path, "f64"))
87 }
88 _ => Err(type_mismatch(path, "f64")),
89 }
90 }
91
92 pub fn get_bool(&self, path: &str) -> Result<bool, ConfigError> {
98 match self.lookup_node(path) {
99 None => Err(missing(path)),
100 Some(HoconValue::Scalar(ScalarValue::Bool(b))) => Ok(*b),
101 Some(HoconValue::Scalar(ScalarValue::String(s))) => match s.to_lowercase().as_str() {
102 "true" | "yes" | "on" => Ok(true),
103 "false" | "no" | "off" => Ok(false),
104 _ => Err(type_mismatch(path, "bool")),
105 },
106 _ => Err(type_mismatch(path, "bool")),
107 }
108 }
109
110 pub fn get_config(&self, path: &str) -> Result<Config, ConfigError> {
114 match self.lookup_node(path) {
115 None => Err(missing(path)),
116 Some(HoconValue::Object(map)) => Ok(Config::new(map.clone())),
117 _ => Err(type_mismatch(path, "Object")),
118 }
119 }
120
121 pub fn get_list(&self, path: &str) -> Result<Vec<HoconValue>, ConfigError> {
125 match self.lookup_node(path) {
126 None => Err(missing(path)),
127 Some(HoconValue::Array(items)) => Ok(items.clone()),
128 _ => Err(type_mismatch(path, "Array")),
129 }
130 }
131
132 pub fn get_string_option(&self, path: &str) -> Option<String> {
134 self.get_string(path).ok()
135 }
136
137 pub fn get_i64_option(&self, path: &str) -> Option<i64> {
139 self.get_i64(path).ok()
140 }
141
142 pub fn get_f64_option(&self, path: &str) -> Option<f64> {
144 self.get_f64(path).ok()
145 }
146
147 pub fn get_bool_option(&self, path: &str) -> Option<bool> {
149 self.get_bool(path).ok()
150 }
151
152 pub fn get_config_option(&self, path: &str) -> Option<Config> {
154 self.get_config(path).ok()
155 }
156
157 pub fn get_list_option(&self, path: &str) -> Option<Vec<HoconValue>> {
159 self.get_list(path).ok()
160 }
161
162 pub fn get_duration(&self, path: &str) -> Result<std::time::Duration, ConfigError> {
173 match self.lookup_node(path) {
174 None => Err(missing(path)),
175 Some(HoconValue::Scalar(ScalarValue::String(s))) => {
176 parse_duration(s).ok_or_else(|| ConfigError {
177 message: format!("invalid duration at {}: {}", path, s),
178 path: path.to_string(),
179 })
180 }
181 Some(HoconValue::Scalar(ScalarValue::Int(n))) => {
182 Ok(std::time::Duration::from_millis(*n as u64))
183 }
184 Some(HoconValue::Scalar(ScalarValue::Float(f))) => {
185 Ok(std::time::Duration::from_secs_f64(*f / 1000.0))
186 }
187 _ => Err(ConfigError {
188 message: format!("expected duration at {}", path),
189 path: path.to_string(),
190 }),
191 }
192 }
193
194 pub fn get_duration_option(&self, path: &str) -> Option<std::time::Duration> {
196 self.get_duration(path).ok()
197 }
198
199 pub fn get_bytes(&self, path: &str) -> Result<i64, ConfigError> {
210 let v = self.lookup_node(path).ok_or_else(|| ConfigError {
211 message: format!("path not found: {}", path),
212 path: path.to_string(),
213 })?;
214 match v {
215 HoconValue::Scalar(ScalarValue::String(s)) => {
216 parse_bytes(s).ok_or_else(|| ConfigError {
217 message: format!("invalid byte size at {}: {}", path, s),
218 path: path.to_string(),
219 })
220 }
221 HoconValue::Scalar(ScalarValue::Int(n)) => Ok(*n),
222 _ => Err(ConfigError {
223 message: format!("expected byte size at {}", path),
224 path: path.to_string(),
225 }),
226 }
227 }
228
229 pub fn get_bytes_option(&self, path: &str) -> Option<i64> {
231 self.get_bytes(path).ok()
232 }
233
234 pub fn has(&self, path: &str) -> bool {
236 self.lookup_node(path).is_some()
237 }
238
239 pub fn keys(&self) -> Vec<&str> {
241 self.root.keys().map(|s| s.as_str()).collect()
242 }
243
244 pub fn with_fallback(&self, fallback: &Config) -> Config {
259 let mut merged = self.root.clone();
260 for (key, fallback_val) in &fallback.root {
261 if let Some(receiver_val) = merged.get(key) {
262 if let (HoconValue::Object(recv_map), HoconValue::Object(fb_map)) =
264 (receiver_val, fallback_val)
265 {
266 let recv_cfg = Config::new(recv_map.clone());
267 let fb_cfg = Config::new(fb_map.clone());
268 let deep = recv_cfg.with_fallback(&fb_cfg);
269 merged.insert(key.clone(), HoconValue::Object(deep.root));
270 }
271 } else {
273 merged.insert(key.clone(), fallback_val.clone());
275 }
276 }
277 Config::new(merged)
278 }
279}
280
281fn split_config_path(path: &str) -> Vec<String> {
286 let mut segments = Vec::new();
287 let chars: Vec<char> = path.chars().collect();
288 let mut i = 0;
289 while i < chars.len() {
290 if chars[i] == '"' {
291 i += 1; let mut seg = String::new();
294 let mut closed = false;
295 while i < chars.len() {
296 if chars[i] == '\\' && i + 1 < chars.len() {
297 seg.push(chars[i + 1]);
298 i += 2;
299 continue;
300 }
301 if chars[i] == '"' {
302 closed = true;
303 i += 1;
304 break;
305 }
306 seg.push(chars[i]);
307 i += 1;
308 }
309 if !closed {
310 return vec![path.to_string()]; }
312 segments.push(seg);
313 if i < chars.len() && chars[i] == '.' {
315 i += 1;
316 }
317 } else {
318 let start = i;
321 while i < chars.len() && chars[i] != '.' && chars[i] != '"' {
322 i += 1;
323 }
324 segments.push(chars[start..i].iter().collect());
325 if i < chars.len() && chars[i] == '.' {
327 i += 1;
328 }
329 }
330 }
331 if path.ends_with('.') {
333 segments.push(String::new());
334 }
335 segments
336}
337
338fn lookup_in_map_by_segments<'a>(
339 map: &'a IndexMap<String, HoconValue>,
340 segments: &[String],
341) -> Option<&'a HoconValue> {
342 if segments.is_empty() {
343 return None;
344 }
345 let key = &segments[0];
346 let rest = &segments[1..];
347 let value = map.get(key)?;
348 if rest.is_empty() {
349 Some(value)
350 } else {
351 match value {
352 HoconValue::Object(inner) => lookup_in_map_by_segments(inner, rest),
353 _ => None,
354 }
355 }
356}
357
358#[cfg(feature = "serde")]
359impl Config {
360 pub fn deserialize<T: ::serde::de::DeserializeOwned>(
365 &self,
366 ) -> Result<T, crate::serde::DeserializeError> {
367 let value = HoconValue::Object(self.root.clone());
368 T::deserialize(crate::serde::HoconDeserializer::new(&value))
369 }
370}
371
372fn parse_duration(s: &str) -> Option<std::time::Duration> {
373 let s = s.trim();
374 let num_end = s
375 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
376 .unwrap_or(s.len());
377 let num_str = s[..num_end].trim();
378 let unit_str = s[num_end..].trim().to_lowercase();
379
380 let num: f64 = num_str.parse().ok()?;
381
382 let nanos_per_unit: f64 = match unit_str.as_str() {
383 "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => 1.0,
384 "us" | "micro" | "micros" | "microsecond" | "microseconds" => 1_000.0,
385 "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 1_000_000.0,
386 "s" | "second" | "seconds" => 1_000_000_000.0,
387 "m" | "minute" | "minutes" => 60_000_000_000.0,
388 "h" | "hour" | "hours" => 3_600_000_000_000.0,
389 "d" | "day" | "days" => 86_400_000_000_000.0,
390 "w" | "week" | "weeks" => 604_800_000_000_000.0,
391 _ => return None,
392 };
393
394 Some(std::time::Duration::from_nanos(
395 (num * nanos_per_unit) as u64,
396 ))
397}
398
399fn parse_bytes(s: &str) -> Option<i64> {
400 let s = s.trim();
401 let num_end = s
402 .find(|c: char| !c.is_ascii_digit() && c != '.')
403 .unwrap_or(s.len());
404 let num_str = s[..num_end].trim();
405 let unit_str = s[num_end..].trim();
406
407 let multiplier: i64 = match unit_str {
410 "" | "B" | "byte" | "bytes" => 1,
411 "K" | "KB" | "kilobyte" | "kilobytes" => 1_000,
412 "KiB" | "kibibyte" | "kibibytes" => 1_024,
413 "M" | "MB" | "megabyte" | "megabytes" => 1_000_000,
414 "MiB" | "mebibyte" | "mebibytes" => 1_048_576,
415 "G" | "GB" | "gigabyte" | "gigabytes" => 1_000_000_000,
416 "GiB" | "gibibyte" | "gibibytes" => 1_073_741_824,
417 "T" | "TB" | "terabyte" | "terabytes" => 1_000_000_000_000,
418 "TiB" | "tebibyte" | "tebibytes" => 1_099_511_627_776,
419 _ => return None,
420 };
421
422 if let Ok(n) = num_str.parse::<i64>() {
424 n.checked_mul(multiplier)
425 } else {
426 let num: f64 = num_str.parse().ok()?;
427 let result = (num * multiplier as f64).round();
428 if !result.is_finite() || result > i64::MAX as f64 || result < i64::MIN as f64 {
429 return None;
430 }
431 Some(result as i64)
432 }
433}
434
435fn missing(path: &str) -> ConfigError {
436 ConfigError {
437 message: "key not found".to_string(),
438 path: path.to_string(),
439 }
440}
441
442fn type_mismatch(path: &str, expected: &str) -> ConfigError {
443 ConfigError {
444 message: format!("expected {}", expected),
445 path: path.to_string(),
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::value::{HoconValue, ScalarValue};
453 use indexmap::IndexMap;
454
455 fn make_config(entries: Vec<(&str, HoconValue)>) -> Config {
456 let mut map = IndexMap::new();
457 for (k, v) in entries {
458 map.insert(k.to_string(), v);
459 }
460 Config::new(map)
461 }
462
463 fn sv(s: &str) -> HoconValue {
464 HoconValue::Scalar(ScalarValue::String(s.into()))
465 }
466 fn iv(n: i64) -> HoconValue {
467 HoconValue::Scalar(ScalarValue::Int(n))
468 }
469 fn fv(n: f64) -> HoconValue {
470 HoconValue::Scalar(ScalarValue::Float(n))
471 }
472 fn bv(b: bool) -> HoconValue {
473 HoconValue::Scalar(ScalarValue::Bool(b))
474 }
475
476 #[test]
477 fn get_returns_value_at_path() {
478 let c = make_config(vec![("host", sv("localhost"))]);
479 assert!(c.get("host").is_some());
480 }
481
482 #[test]
483 fn get_returns_none_for_missing() {
484 let c = make_config(vec![]);
485 assert!(c.get("missing").is_none());
486 }
487
488 #[test]
489 fn get_string_returns_string() {
490 let c = make_config(vec![("host", sv("localhost"))]);
491 assert_eq!(c.get_string("host").unwrap(), "localhost");
492 }
493
494 #[test]
495 fn get_string_coerces_int() {
496 let c = make_config(vec![("port", iv(8080))]);
497 assert_eq!(c.get_string("port").unwrap(), "8080");
498 }
499
500 #[test]
501 fn get_string_coerces_float() {
502 let c = make_config(vec![("ratio", fv(3.14))]);
503 let s = c.get_string("ratio").unwrap();
505 let v: f64 = s.parse().unwrap();
506 assert!((v - 3.14).abs() < 1e-10);
507 }
508
509 #[test]
510 fn get_string_coerces_bool() {
511 let c = make_config(vec![("flag", bv(true))]);
512 assert_eq!(c.get_string("flag").unwrap(), "true");
513 }
514
515 #[test]
516 fn get_string_coerces_null() {
517 let c = make_config(vec![("v", HoconValue::Scalar(ScalarValue::Null))]);
518 assert_eq!(c.get_string("v").unwrap(), "null");
519 }
520
521 #[test]
522 fn get_string_error_on_object() {
523 let mut inner = IndexMap::new();
524 inner.insert("x".into(), iv(1));
525 let c = make_config(vec![("obj", HoconValue::Object(inner))]);
526 assert!(c.get_string("obj").is_err());
527 }
528
529 #[test]
530 fn get_i64_returns_number() {
531 let c = make_config(vec![("port", iv(8080))]);
532 assert_eq!(c.get_i64("port").unwrap(), 8080);
533 }
534
535 #[test]
536 fn get_i64_coerces_numeric_string() {
537 let c = make_config(vec![("port", sv("9999"))]);
538 assert_eq!(c.get_i64("port").unwrap(), 9999);
539 }
540
541 #[test]
542 fn get_i64_error_on_non_numeric() {
543 let c = make_config(vec![("host", sv("localhost"))]);
544 assert!(c.get_i64("host").is_err());
545 }
546
547 #[test]
548 fn get_f64_returns_float() {
549 let c = make_config(vec![("rate", fv(3.14))]);
550 assert!((c.get_f64("rate").unwrap() - 3.14).abs() < f64::EPSILON);
551 }
552
553 #[test]
554 fn get_f64_coerces_numeric_string() {
555 let c = make_config(vec![("rate", sv("3.14"))]);
556 assert!((c.get_f64("rate").unwrap() - 3.14).abs() < f64::EPSILON);
557 }
558
559 #[test]
560 fn get_bool_returns_bool() {
561 let c = make_config(vec![("debug", bv(true))]);
562 assert!(c.get_bool("debug").unwrap());
563 }
564
565 #[test]
566 fn get_bool_coerces_string_true() {
567 let c = make_config(vec![("debug", sv("true"))]);
568 assert!(c.get_bool("debug").unwrap());
569 }
570
571 #[test]
572 fn get_bool_coerces_string_false() {
573 let c = make_config(vec![("debug", sv("false"))]);
574 assert!(!c.get_bool("debug").unwrap());
575 }
576
577 #[test]
578 fn get_bool_coerces_yes_no_on_off() {
579 let c1 = make_config(vec![("v", sv("yes"))]);
580 assert!(c1.get_bool("v").unwrap());
581 let c2 = make_config(vec![("v", sv("no"))]);
582 assert!(!c2.get_bool("v").unwrap());
583 let c3 = make_config(vec![("v", sv("on"))]);
584 assert!(c3.get_bool("v").unwrap());
585 let c4 = make_config(vec![("v", sv("off"))]);
586 assert!(!c4.get_bool("v").unwrap());
587 }
588
589 #[test]
590 fn get_bool_is_case_insensitive() {
591 let c = make_config(vec![("v", sv("TRUE"))]);
592 assert!(c.get_bool("v").unwrap());
593 let c2 = make_config(vec![("v", sv("Off"))]);
594 assert!(!c2.get_bool("v").unwrap());
595 }
596
597 #[test]
598 fn get_bool_error_on_non_boolean() {
599 let c = make_config(vec![("v", sv("maybe"))]);
600 assert!(c.get_bool("v").is_err());
601 }
602
603 #[test]
604 fn has_returns_true_for_existing() {
605 let c = make_config(vec![("host", sv("localhost"))]);
606 assert!(c.has("host"));
607 }
608
609 #[test]
610 fn has_returns_false_for_missing() {
611 let c = make_config(vec![]);
612 assert!(!c.has("missing"));
613 }
614
615 #[test]
616 fn keys_returns_in_order() {
617 let c = make_config(vec![("b", iv(2)), ("a", iv(1))]);
618 assert_eq!(c.keys(), vec!["b", "a"]);
619 }
620
621 #[test]
622 fn get_nested_dot_path() {
623 let mut inner = IndexMap::new();
624 inner.insert("host".into(), sv("localhost"));
625 let c = make_config(vec![("server", HoconValue::Object(inner))]);
626 assert_eq!(c.get_string("server.host").unwrap(), "localhost");
627 }
628
629 #[test]
630 fn get_config_returns_sub_config() {
631 let mut inner = IndexMap::new();
632 inner.insert("host".into(), sv("localhost"));
633 let c = make_config(vec![("server", HoconValue::Object(inner))]);
634 let sub = c.get_config("server").unwrap();
635 assert_eq!(sub.get_string("host").unwrap(), "localhost");
636 }
637
638 #[test]
639 fn get_list_returns_array() {
640 let items = vec![iv(1), iv(2), iv(3)];
641 let c = make_config(vec![("list", HoconValue::Array(items))]);
642 let list = c.get_list("list").unwrap();
643 assert_eq!(list.len(), 3);
644 }
645
646 #[test]
647 fn with_fallback_receiver_wins() {
648 let c1 = make_config(vec![("host", sv("prod"))]);
649 let c2 = make_config(vec![("host", sv("dev")), ("port", iv(8080))]);
650 let merged = c1.with_fallback(&c2);
651 assert_eq!(merged.get_string("host").unwrap(), "prod");
652 assert_eq!(merged.get_i64("port").unwrap(), 8080);
653 }
654
655 #[test]
656 fn option_variants_return_none_on_missing() {
657 let c = make_config(vec![]);
658 assert!(c.get_string_option("x").is_none());
659 assert!(c.get_i64_option("x").is_none());
660 assert!(c.get_f64_option("x").is_none());
661 assert!(c.get_bool_option("x").is_none());
662 }
663
664 #[test]
665 fn get_duration_nanoseconds() {
666 let c = make_config(vec![("t", sv("100 ns"))]);
667 assert_eq!(
668 c.get_duration("t").unwrap(),
669 std::time::Duration::from_nanos(100)
670 );
671 }
672
673 #[test]
674 fn get_duration_milliseconds() {
675 let c = make_config(vec![("t", sv("500 ms"))]);
676 assert_eq!(
677 c.get_duration("t").unwrap(),
678 std::time::Duration::from_millis(500)
679 );
680 }
681
682 #[test]
683 fn get_duration_seconds() {
684 let c = make_config(vec![("t", sv("30 seconds"))]);
685 assert_eq!(
686 c.get_duration("t").unwrap(),
687 std::time::Duration::from_secs(30)
688 );
689 }
690
691 #[test]
692 fn get_duration_minutes() {
693 let c = make_config(vec![("t", sv("5 m"))]);
694 assert_eq!(
695 c.get_duration("t").unwrap(),
696 std::time::Duration::from_secs(300)
697 );
698 }
699
700 #[test]
701 fn get_duration_hours() {
702 let c = make_config(vec![("t", sv("2 hours"))]);
703 assert_eq!(
704 c.get_duration("t").unwrap(),
705 std::time::Duration::from_secs(7200)
706 );
707 }
708
709 #[test]
710 fn get_duration_days() {
711 let c = make_config(vec![("t", sv("1 d"))]);
712 assert_eq!(
713 c.get_duration("t").unwrap(),
714 std::time::Duration::from_secs(86400)
715 );
716 }
717
718 #[test]
719 fn get_duration_fractional() {
720 let c = make_config(vec![("t", sv("1.5 hours"))]);
721 assert_eq!(
722 c.get_duration("t").unwrap(),
723 std::time::Duration::from_secs(5400)
724 );
725 }
726
727 #[test]
728 fn get_duration_no_space() {
729 let c = make_config(vec![("t", sv("100ms"))]);
730 assert_eq!(
731 c.get_duration("t").unwrap(),
732 std::time::Duration::from_millis(100)
733 );
734 }
735
736 #[test]
737 fn get_duration_singular_unit() {
738 let c = make_config(vec![("t", sv("1 second"))]);
739 assert_eq!(
740 c.get_duration("t").unwrap(),
741 std::time::Duration::from_secs(1)
742 );
743 }
744
745 #[test]
746 fn get_duration_error_invalid_unit() {
747 let c = make_config(vec![("t", sv("100 foos"))]);
748 assert!(c.get_duration("t").is_err());
749 }
750
751 #[test]
752 fn get_duration_option_missing() {
753 let c = make_config(vec![]);
754 assert!(c.get_duration_option("t").is_none());
755 }
756
757 #[test]
758 fn get_bytes_plain() {
759 let c = make_config(vec![("s", sv("100 B"))]);
760 assert_eq!(c.get_bytes("s").unwrap(), 100);
761 }
762
763 #[test]
764 fn get_bytes_kilobytes() {
765 let c = make_config(vec![("s", sv("10 KB"))]);
766 assert_eq!(c.get_bytes("s").unwrap(), 10_000);
767 }
768
769 #[test]
770 fn get_bytes_kibibytes() {
771 let c = make_config(vec![("s", sv("1 KiB"))]);
772 assert_eq!(c.get_bytes("s").unwrap(), 1_024);
773 }
774
775 #[test]
776 fn get_bytes_megabytes() {
777 let c = make_config(vec![("s", sv("5 MB"))]);
778 assert_eq!(c.get_bytes("s").unwrap(), 5_000_000);
779 }
780
781 #[test]
782 fn get_bytes_mebibytes() {
783 let c = make_config(vec![("s", sv("1 MiB"))]);
784 assert_eq!(c.get_bytes("s").unwrap(), 1_048_576);
785 }
786
787 #[test]
788 fn get_bytes_gigabytes() {
789 let c = make_config(vec![("s", sv("2 GB"))]);
790 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000_000);
791 }
792
793 #[test]
794 fn get_bytes_gibibytes() {
795 let c = make_config(vec![("s", sv("1 GiB"))]);
796 assert_eq!(c.get_bytes("s").unwrap(), 1_073_741_824);
797 }
798
799 #[test]
800 fn get_bytes_terabytes() {
801 let c = make_config(vec![("s", sv("1 TB"))]);
802 assert_eq!(c.get_bytes("s").unwrap(), 1_000_000_000_000);
803 }
804
805 #[test]
806 fn get_bytes_tebibytes() {
807 let c = make_config(vec![("s", sv("1 TiB"))]);
808 assert_eq!(c.get_bytes("s").unwrap(), 1_099_511_627_776);
809 }
810
811 #[test]
812 fn get_bytes_no_space() {
813 let c = make_config(vec![("s", sv("512MB"))]);
814 assert_eq!(c.get_bytes("s").unwrap(), 512_000_000);
815 }
816
817 #[test]
818 fn get_bytes_long_unit() {
819 let c = make_config(vec![("s", sv("2 megabytes"))]);
820 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000);
821 }
822
823 #[test]
824 fn get_bytes_error_invalid_unit() {
825 let c = make_config(vec![("s", sv("100 XB"))]);
826 assert!(c.get_bytes("s").is_err());
827 }
828
829 #[test]
830 fn get_bytes_option_missing() {
831 let c = make_config(vec![]);
832 assert!(c.get_bytes_option("s").is_none());
833 }
834
835 #[test]
836 fn get_bytes_fractional_rounds() {
837 let c = make_config(vec![("s", sv("1.5 KiB"))]);
839 assert_eq!(c.get_bytes("s").unwrap(), 1536);
840 }
841
842 #[test]
843 fn split_config_path_consecutive_dots_preserve_empty() {
844 let segs = split_config_path("a..b");
845 assert_eq!(segs, vec!["a", "", "b"]);
846 }
847
848 #[test]
849 fn split_config_path_trailing_dot_empty_segment() {
850 let segs = split_config_path("a.b.");
851 assert_eq!(segs, vec!["a", "b", ""]);
852 }
853
854 #[test]
855 fn split_config_path_quoted_escape() {
856 let segs = split_config_path(r#""a\"b""#);
858 assert_eq!(segs, vec!["a\"b"]);
859 }
860
861 #[test]
862 fn split_config_path_quoted_with_dot() {
863 let segs = split_config_path(r#"server."web.api".port"#);
864 assert_eq!(segs, vec!["server", "web.api", "port"]);
865 }
866}