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