1use crate::error::{ConfigError, NotResolvedError};
2use crate::lexer::is_hocon_whitespace;
3use crate::numeric_array::numeric_object_to_array;
4use crate::value::{HoconValue, ScalarType};
5use indexmap::IndexMap;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub struct Period {
18 pub years: i32,
19 pub months: i32,
20 pub days: i32,
21}
22
23impl Period {
24 pub fn new(years: i32, months: i32, days: i32) -> Self {
26 Self {
27 years,
28 months,
29 days,
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
44pub struct Config {
45 pub(crate) root: IndexMap<String, HoconValue>,
46 pub(crate) resolved: bool,
48 pub(crate) parse_base_dir: Option<PathBuf>,
50 pub(crate) origin_description: Option<String>,
52 pub(crate) unresolved_tree: Option<crate::resolver::types::ResObj>,
55}
56
57impl PartialEq for Config {
58 fn eq(&self, other: &Self) -> bool {
59 self.root == other.root
60 && self.resolved == other.resolved
61 && self.parse_base_dir == other.parse_base_dir
62 && self.origin_description == other.origin_description
63 }
64}
65
66impl Config {
67 pub fn new(root: IndexMap<String, HoconValue>) -> Self {
70 Self {
71 root,
72 resolved: true,
73 parse_base_dir: None,
74 origin_description: None,
75 unresolved_tree: None,
76 }
77 }
78
79 pub(crate) fn new_with_meta(
81 root: IndexMap<String, HoconValue>,
82 origin_description: Option<String>,
83 ) -> Self {
84 Self {
85 root,
86 resolved: true,
87 parse_base_dir: None,
88 origin_description,
89 unresolved_tree: None,
90 }
91 }
92
93 pub(crate) fn new_from_res_obj(
97 tree: crate::resolver::types::ResObj,
98 parse_base_dir: Option<PathBuf>,
99 origin_description: Option<String>,
100 ) -> Self {
101 let root = crate::resolver::res_obj_to_hocon_partial(&tree);
102 let resolved = !crate::resolver::contains_placeholders_in_hocon_map(&root);
103 let has_priors = crate::resolver::res_obj_has_priors(&tree);
109 Self {
110 root,
111 resolved,
112 parse_base_dir,
113 origin_description,
114 unresolved_tree: if resolved && !has_priors {
115 None
116 } else {
117 Some(tree)
118 },
119 }
120 }
121
122 pub fn is_resolved(&self) -> bool {
125 if self.resolved {
126 return true;
127 }
128 !crate::resolver::contains_placeholders_in_hocon_map(&self.root)
129 }
130
131 pub fn origin_description(&self) -> Option<&str> {
133 self.origin_description.as_deref()
134 }
135
136 pub fn resolve(
142 &self,
143 opts: crate::options::ResolveOptions,
144 ) -> Result<Config, crate::error::HoconError> {
145 use crate::error::{HoconError, ParseError};
146 if self.is_resolved() {
147 return Ok(Config {
148 root: self.root.clone(),
149 resolved: true,
150 parse_base_dir: self.parse_base_dir.clone(),
151 origin_description: self.origin_description.clone(),
152 unresolved_tree: None,
153 });
154 }
155
156 let tree = match &self.unresolved_tree {
157 Some(t) => t.clone(),
158 None => crate::resolver::hocon_map_to_res_obj(&self.root),
159 };
160
161 let env: std::collections::HashMap<String, String> = if opts.use_system_environment {
162 std::env::vars().collect()
163 } else {
164 std::collections::HashMap::new()
165 };
166 let internal_opts = crate::resolver::InternalResolveOptions::new(env)
167 .with_base_dir_opt(self.parse_base_dir.clone())
168 .with_allow_unresolved(opts.allow_unresolved)
169 .with_use_system_environment(opts.use_system_environment);
170
171 let pre_resolution_tree = if opts.allow_unresolved {
181 Some(tree.clone())
182 } else {
183 None
184 };
185
186 let resolved_value = crate::resolver::resolve_tree(tree, &internal_opts)?;
187 match resolved_value {
188 HoconValue::Object(fields) => {
189 let resolved = !crate::resolver::contains_placeholders_in_hocon_map(&fields);
190 let unresolved_tree = if resolved {
191 None
192 } else {
193 pre_resolution_tree
195 };
196 Ok(Config {
197 root: fields,
198 resolved,
199 parse_base_dir: self.parse_base_dir.clone(),
200 origin_description: self.origin_description.clone(),
201 unresolved_tree,
202 })
203 }
204 _ => Err(HoconError::Parse(ParseError {
205 message: "root must be an object".into(),
206 line: 1,
207 col: 1,
208 })),
209 }
210 }
211
212 pub fn resolve_with(
222 &self,
223 source: &Config,
224 opts: crate::options::ResolveOptions,
225 ) -> Result<Config, crate::error::HoconError> {
226 use crate::error::{HoconError, ParseError};
227 if !source.is_resolved() {
228 return Err(HoconError::NotResolved(NotResolvedError {
229 path: "<source>".into(),
230 }));
231 }
232
233 if self.is_resolved() {
234 return Ok(Config {
235 root: self.root.clone(),
236 resolved: true,
237 parse_base_dir: self.parse_base_dir.clone(),
238 origin_description: self.origin_description.clone(),
239 unresolved_tree: None,
240 });
241 }
242
243 let receiver_root_snapshot = self.root.clone();
245
246 let recv_obj = match &self.unresolved_tree {
247 Some(t) => t.clone(),
248 None => crate::resolver::hocon_map_to_res_obj(&self.root),
249 };
250 let src_obj = crate::resolver::hocon_map_to_res_obj(&source.root);
251 let merged = crate::resolver::merge_unresolved(recv_obj, src_obj);
252
253 let env: std::collections::HashMap<String, String> = if opts.use_system_environment {
254 std::env::vars().collect()
255 } else {
256 std::collections::HashMap::new()
257 };
258 let internal_opts = crate::resolver::InternalResolveOptions::new(env)
259 .with_base_dir_opt(self.parse_base_dir.clone())
260 .with_allow_unresolved(opts.allow_unresolved)
261 .with_use_system_environment(opts.use_system_environment);
262
263 let pre_resolution_tree = if opts.allow_unresolved {
267 Some(merged.clone())
268 } else {
269 None
270 };
271
272 let resolved_value = crate::resolver::resolve_tree(merged, &internal_opts)?;
273
274 let filtered = match resolved_value {
275 HoconValue::Object(mut fields) => {
276 filter_hocon_object_by_receiver(&mut fields, &receiver_root_snapshot);
278 fields
279 }
280 _ => {
281 return Err(HoconError::Parse(ParseError {
282 message: "root must be an object".into(),
283 line: 1,
284 col: 1,
285 }));
286 }
287 };
288
289 let resolved = !crate::resolver::contains_placeholders_in_hocon_map(&filtered);
290 let unresolved_tree = if resolved {
291 None
292 } else {
293 pre_resolution_tree
295 };
296 Ok(Config {
297 root: filtered,
298 resolved,
299 parse_base_dir: self.parse_base_dir.clone(),
300 origin_description: self.origin_description.clone(),
301 unresolved_tree,
302 })
303 }
304
305 fn lookup_node(&self, path: &str) -> Option<&HoconValue> {
307 let segments = split_config_path(path);
308 lookup_in_map_by_segments(&self.root, &segments)
309 }
310
311 pub fn get(&self, path: &str) -> Option<&HoconValue> {
314 self.lookup_node(path)
315 }
316
317 pub fn get_string(&self, path: &str) -> Result<String, ConfigError> {
324 match self.lookup_node(path) {
325 None => Err(missing(path)),
326 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
327 Some(HoconValue::Scalar(sv)) => {
328 if sv.value_type == ScalarType::Null {
329 return Err(type_mismatch(path, "String"));
330 }
331 Ok(sv.raw.clone())
332 }
333 _ => Err(type_mismatch(path, "String")),
334 }
335 }
336
337 pub fn get_i64(&self, path: &str) -> Result<i64, ConfigError> {
343 match self.lookup_node(path) {
344 None => Err(missing(path)),
345 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
346 Some(HoconValue::Scalar(sv)) => {
347 if let Ok(n) = sv.raw.parse::<i64>() {
349 return Ok(n);
350 }
351 let is_float_like =
353 sv.raw.contains('.') || sv.raw.contains('e') || sv.raw.contains('E');
354 if is_float_like {
355 if let Ok(f) = sv.raw.parse::<f64>() {
356 if f.fract() == 0.0
357 && f.is_finite()
358 && f >= i64::MIN as f64
359 && f < (i64::MAX as f64)
360 {
361 return Ok(f as i64);
362 }
363 }
364 }
365 Err(type_mismatch(path, "i64"))
366 }
367 _ => Err(type_mismatch(path, "i64")),
368 }
369 }
370
371 pub fn get_f64(&self, path: &str) -> Result<f64, ConfigError> {
377 match self.lookup_node(path) {
378 None => Err(missing(path)),
379 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
380 Some(HoconValue::Scalar(sv)) => sv
381 .raw
382 .parse::<f64>()
383 .map_err(|_| type_mismatch(path, "f64")),
384 _ => Err(type_mismatch(path, "f64")),
385 }
386 }
387
388 pub fn get_bool(&self, path: &str) -> Result<bool, ConfigError> {
394 match self.lookup_node(path) {
395 None => Err(missing(path)),
396 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
397 Some(HoconValue::Scalar(sv)) => match sv.raw.to_lowercase().as_str() {
398 "true" | "yes" | "on" => Ok(true),
399 "false" | "no" | "off" => Ok(false),
400 _ => Err(type_mismatch(path, "bool")),
401 },
402 _ => Err(type_mismatch(path, "bool")),
403 }
404 }
405
406 pub fn get_config(&self, path: &str) -> Result<Config, ConfigError> {
410 match self.lookup_node(path) {
411 None => Err(missing(path)),
412 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
413 Some(HoconValue::Object(map)) => Ok(Config::new(map.clone())),
414 _ => Err(type_mismatch(path, "Object")),
415 }
416 }
417
418 pub fn get_list(&self, path: &str) -> Result<Vec<HoconValue>, ConfigError> {
426 match self.lookup_node(path) {
427 None => Err(missing(path)),
428 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
429 Some(HoconValue::Array(items)) => Ok(items.clone()),
430 Some(v @ HoconValue::Object(_)) => {
431 numeric_object_to_array(v).ok_or_else(|| type_mismatch(path, "Array"))
436 }
437 _ => Err(type_mismatch(path, "Array")),
438 }
439 }
440
441 pub fn get_string_option(&self, path: &str) -> Option<String> {
443 self.get_string(path).ok()
444 }
445
446 pub fn get_i64_option(&self, path: &str) -> Option<i64> {
448 self.get_i64(path).ok()
449 }
450
451 pub fn get_f64_option(&self, path: &str) -> Option<f64> {
453 self.get_f64(path).ok()
454 }
455
456 pub fn get_bool_option(&self, path: &str) -> Option<bool> {
458 self.get_bool(path).ok()
459 }
460
461 pub fn get_config_option(&self, path: &str) -> Option<Config> {
463 self.get_config(path).ok()
464 }
465
466 pub fn get_list_option(&self, path: &str) -> Option<Vec<HoconValue>> {
468 self.get_list(path).ok()
469 }
470
471 pub fn get_duration(&self, path: &str) -> Result<std::time::Duration, ConfigError> {
482 match self.lookup_node(path) {
483 None => Err(missing(path)),
484 Some(HoconValue::Scalar(sv)) => {
485 if let Some(d) = parse_duration(&sv.raw) {
487 return Ok(d);
488 }
489 if sv.value_type == ScalarType::Number {
491 if let Ok(n) = sv.raw.parse::<i64>() {
492 if n < 0 {
493 return Err(ConfigError {
494 message: format!("negative duration at {}: {}", path, sv.raw),
495 path: path.to_string(),
496 });
497 }
498 return Ok(std::time::Duration::from_millis(n as u64));
499 }
500 if let Ok(f) = sv.raw.parse::<f64>() {
501 if f < 0.0 || !f.is_finite() {
502 return Err(ConfigError {
503 message: format!("invalid duration at {}: {}", path, sv.raw),
504 path: path.to_string(),
505 });
506 }
507 let secs = f / 1000.0;
508 if secs > u64::MAX as f64 {
509 return Err(ConfigError {
510 message: format!("duration too large at {}: {}", path, sv.raw),
511 path: path.to_string(),
512 });
513 }
514 return Ok(std::time::Duration::from_secs_f64(secs));
515 }
516 }
517 Err(ConfigError {
518 message: format!("invalid duration at {}: {}", path, sv.raw),
519 path: path.to_string(),
520 })
521 }
522 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
523 _ => Err(ConfigError {
524 message: format!("expected duration at {}", path),
525 path: path.to_string(),
526 }),
527 }
528 }
529
530 pub fn get_duration_option(&self, path: &str) -> Option<std::time::Duration> {
532 self.get_duration(path).ok()
533 }
534
535 pub fn get_bytes(&self, path: &str) -> Result<i64, ConfigError> {
546 let v = self.lookup_node(path).ok_or_else(|| ConfigError {
547 message: format!("path not found: {}", path),
548 path: path.to_string(),
549 })?;
550 match v {
551 HoconValue::Scalar(sv) => {
552 let n: i64 = if sv.value_type == ScalarType::Number {
553 sv.raw.parse::<i64>().map_err(|_| ConfigError {
556 message: format!("expected byte size at {}", path),
557 path: path.to_string(),
558 })?
559 } else {
560 parse_bytes(&sv.raw).ok_or_else(|| ConfigError {
562 message: format!("invalid byte size at {}: {}", path, sv.raw),
563 path: path.to_string(),
564 })?
565 };
566 if n < 0 {
570 return Err(ConfigError {
571 message: format!("negative byte size at {}: {}", path, sv.raw),
572 path: path.to_string(),
573 });
574 }
575 Ok(n)
576 }
577 HoconValue::Placeholder(_) => Err(not_resolved(path)),
578 _ => Err(ConfigError {
579 message: format!("expected byte size at {}", path),
580 path: path.to_string(),
581 }),
582 }
583 }
584
585 pub fn get_bytes_option(&self, path: &str) -> Option<i64> {
587 self.get_bytes(path).ok()
588 }
589
590 pub fn get_period(&self, path: &str) -> Result<Period, ConfigError> {
600 match self.lookup_node(path) {
601 None => Err(missing(path)),
602 Some(HoconValue::Scalar(sv)) => {
603 if let Some((y, mo, d)) = parse_period(&sv.raw) {
604 return Ok(Period::new(y, mo, d));
605 }
606 if sv.value_type == ScalarType::Number {
608 if let Ok(n) = sv.raw.parse::<i32>() {
609 return Ok(Period::new(0, 0, n));
610 }
611 }
612 Err(ConfigError {
613 message: format!("invalid period at {}: {}", path, sv.raw),
614 path: path.to_string(),
615 })
616 }
617 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
618 _ => Err(ConfigError {
619 message: format!("expected period at {}", path),
620 path: path.to_string(),
621 }),
622 }
623 }
624
625 pub fn get_period_option(&self, path: &str) -> Option<Period> {
627 self.get_period(path).ok()
628 }
629
630 pub fn has(&self, path: &str) -> bool {
632 self.lookup_node(path).is_some()
633 }
634
635 pub fn keys(&self) -> Vec<&str> {
637 self.root.keys().map(|s| s.as_str()).collect()
638 }
639
640 pub fn with_fallback(&self, fallback: &Config) -> Config {
662 let recv_obj = match &self.unresolved_tree {
663 Some(t) => t.clone(),
664 None => crate::resolver::hocon_map_to_res_obj(&self.root),
665 };
666 let fb_obj = match &fallback.unresolved_tree {
667 Some(t) => t.clone(),
668 None => crate::resolver::hocon_map_to_res_obj(&fallback.root),
669 };
670 let merged = crate::resolver::merge_unresolved(recv_obj, fb_obj);
671 Config::new_from_res_obj(
672 merged,
673 self.parse_base_dir.clone(),
674 self.origin_description.clone(),
675 )
676 }
677}
678
679fn split_config_path(path: &str) -> Vec<String> {
684 let mut segments = Vec::new();
685 let chars: Vec<char> = path.chars().collect();
686 let mut i = 0;
687 while i < chars.len() {
688 if chars[i] == '"' {
689 i += 1; let mut seg = String::new();
692 let mut closed = false;
693 while i < chars.len() {
694 if chars[i] == '\\' && i + 1 < chars.len() {
695 seg.push(chars[i + 1]);
696 i += 2;
697 continue;
698 }
699 if chars[i] == '"' {
700 closed = true;
701 i += 1;
702 break;
703 }
704 seg.push(chars[i]);
705 i += 1;
706 }
707 if !closed {
708 return vec![path.to_string()]; }
710 segments.push(seg);
711 if i < chars.len() && chars[i] == '.' {
713 i += 1;
714 }
715 } else {
716 let start = i;
719 while i < chars.len() && chars[i] != '.' && chars[i] != '"' {
720 i += 1;
721 }
722 segments.push(chars[start..i].iter().collect());
723 if i < chars.len() && chars[i] == '.' {
725 i += 1;
726 }
727 }
728 }
729 if path.ends_with('.') {
731 segments.push(String::new());
732 }
733 segments
734}
735
736fn lookup_in_map_by_segments<'a>(
737 map: &'a IndexMap<String, HoconValue>,
738 segments: &[String],
739) -> Option<&'a HoconValue> {
740 if segments.is_empty() {
741 return None;
742 }
743 let key = &segments[0];
744 let rest = &segments[1..];
745 let value = map.get(key)?;
746 if rest.is_empty() {
747 Some(value)
748 } else {
749 match value {
750 HoconValue::Object(inner) => lookup_in_map_by_segments(inner, rest),
751 _ => None,
752 }
753 }
754}
755
756#[cfg(feature = "serde")]
757impl Config {
758 pub fn deserialize<T: ::serde::de::DeserializeOwned>(
763 &self,
764 ) -> Result<T, crate::serde::DeserializeError> {
765 let value = HoconValue::Object(self.root.clone());
766 T::deserialize(crate::serde::HoconDeserializer::new(&value))
767 }
768}
769
770fn trim_hocon_ws(s: &str) -> &str {
775 s.trim_matches(is_hocon_whitespace)
776}
777
778fn is_integer_str(s: &str) -> bool {
783 let s = s.strip_prefix(['+', '-']).unwrap_or(s);
784 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
785}
786
787fn parse_duration(s: &str) -> Option<std::time::Duration> {
804 let s = trim_hocon_ws(s);
805 if s.is_empty() {
806 return None;
807 }
808
809 let num_end = s
811 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
812 .unwrap_or(s.len());
813 let num_str = s[..num_end].trim();
814 let unit_str = trim_hocon_ws(&s[num_end..]).to_lowercase();
816
817 if num_str.is_empty() {
818 return None;
819 }
820
821 let nanos_per_unit: f64 = match unit_str.as_str() {
822 "" | "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 1_000_000.0,
824 "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => 1.0,
825 "us" | "micro" | "micros" | "microsecond" | "microseconds" => 1_000.0,
826 "s" | "second" | "seconds" => 1_000_000_000.0,
827 "m" | "minute" | "minutes" => 60_000_000_000.0,
828 "h" | "hour" | "hours" => 3_600_000_000_000.0,
829 "d" | "day" | "days" => 86_400_000_000_000.0,
830 "w" | "week" | "weeks" => 604_800_000_000_000.0,
831 _ => return None,
832 };
833
834 if is_integer_str(num_str) {
836 let n_i128: i128 = num_str.parse().ok()?;
841 if n_i128 < 0 {
842 return None;
843 }
844 let n_u64: u64 = n_i128.try_into().ok()?;
845 let unit_u64 = nanos_per_unit as u64;
850 let nanos = n_u64.checked_mul(unit_u64)?;
851 return Some(std::time::Duration::from_nanos(nanos));
852 }
853
854 let f: f64 = num_str.parse().ok()?;
856 if f < 0.0 || !f.is_finite() {
857 return None;
858 }
859 let product = f * nanos_per_unit;
865 if !product.is_finite() || product >= 2f64.powi(64) {
866 return None;
867 }
868 Some(std::time::Duration::from_nanos(product as u64))
869}
870
871pub(crate) fn parse_period(s: &str) -> Option<(i32, i32, i32)> {
884 let s = trim_hocon_ws(s);
885 if s.is_empty() {
886 return None;
887 }
888
889 let num_end = s
891 .find(|c: char| !c.is_ascii_digit() && c != '-' && c != '+')
892 .unwrap_or(s.len());
893 let num_str = s[..num_end].trim();
894 let unit_str = trim_hocon_ws(&s[num_end..]);
895
896 if num_str.is_empty() {
897 return None;
898 }
899
900 if !is_integer_str(num_str) {
902 return None;
903 }
904
905 let n: i32 = num_str.parse().ok()?;
906
907 match unit_str {
910 "" | "d" | "day" | "days" => Some((0, 0, n)),
912 "w" | "week" | "weeks" => Some((0, 0, n.checked_mul(7)?)),
913 "m" | "mo" | "month" | "months" => Some((0, n, 0)),
914 "y" | "year" | "years" => Some((n, 0, 0)),
915 _ => None,
916 }
917}
918
919fn parse_bytes(s: &str) -> Option<i64> {
931 let s = trim_hocon_ws(s);
932 let num_end = s
933 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
934 .unwrap_or(s.len());
935 let num_str = s[..num_end].trim();
936 let unit_str = trim_hocon_ws(&s[num_end..]);
937
938 if num_str.is_empty() {
939 return None;
940 }
941
942 let multiplier: i64 = match unit_str {
953 "" | "B" | "byte" | "bytes" => 1,
954 "K" | "k" => 1_024,
956 "M" | "m" => 1_048_576,
957 "G" | "g" => 1_073_741_824,
958 "T" | "t" => 1_099_511_627_776,
959 "P" | "p" => 1_125_899_906_842_624,
960 "E" | "e" => 1_152_921_504_606_846_976,
961 "KB" | "kilobyte" | "kilobytes" => 1_000,
963 "KiB" | "Ki" | "kibibyte" | "kibibytes" => 1_024,
964 "MB" | "megabyte" | "megabytes" => 1_000_000,
965 "MiB" | "Mi" | "mebibyte" | "mebibytes" => 1_048_576,
966 "GB" | "gigabyte" | "gigabytes" => 1_000_000_000,
967 "GiB" | "Gi" | "gibibyte" | "gibibytes" => 1_073_741_824,
968 "TB" | "terabyte" | "terabytes" => 1_000_000_000_000,
969 "TiB" | "Ti" | "tebibyte" | "tebibytes" => 1_099_511_627_776,
970 _ => return None,
971 };
972
973 if is_integer_str(num_str) {
975 let n: i64 = num_str.parse().ok()?;
976 return n.checked_mul(multiplier);
977 }
978
979 let f: f64 = num_str.parse().ok()?;
981 if !f.is_finite() || f.abs() * multiplier as f64 >= 2f64.powi(63) {
985 return None;
986 }
987 Some((f * multiplier as f64) as i64)
988}
989
990fn missing(path: &str) -> ConfigError {
991 ConfigError {
992 message: "key not found".to_string(),
993 path: path.to_string(),
994 }
995}
996
997fn type_mismatch(path: &str, expected: &str) -> ConfigError {
998 ConfigError {
999 message: format!("expected {}", expected),
1000 path: path.to_string(),
1001 }
1002}
1003
1004fn not_resolved(path: &str) -> ConfigError {
1005 ConfigError {
1006 message: "value is not resolved (call Config::resolve() before accessing values)"
1007 .to_string(),
1008 path: path.to_string(),
1009 }
1010}
1011
1012fn filter_hocon_object_by_receiver(
1015 resolved: &mut IndexMap<String, HoconValue>,
1016 receiver_shape: &IndexMap<String, HoconValue>,
1017) {
1018 resolved.retain(|k, v| {
1019 if !receiver_shape.contains_key(k) {
1020 return false;
1021 }
1022 if let (HoconValue::Object(inner_res), Some(HoconValue::Object(inner_recv))) =
1023 (v, receiver_shape.get(k))
1024 {
1025 filter_hocon_object_by_receiver(inner_res, inner_recv);
1026 }
1027 true
1028 });
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034 use crate::value::{HoconValue, ScalarValue};
1035 use indexmap::IndexMap;
1036
1037 fn make_config(entries: Vec<(&str, HoconValue)>) -> Config {
1038 let mut map = IndexMap::new();
1039 for (k, v) in entries {
1040 map.insert(k.to_string(), v);
1041 }
1042 Config::new(map)
1043 }
1044
1045 fn sv(s: &str) -> HoconValue {
1046 HoconValue::Scalar(ScalarValue::string(s.into()))
1047 }
1048 fn iv(n: i64) -> HoconValue {
1049 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1050 }
1051 fn fv(n: f64) -> HoconValue {
1052 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1053 }
1054 fn bv(b: bool) -> HoconValue {
1055 HoconValue::Scalar(ScalarValue::boolean(b))
1056 }
1057
1058 #[test]
1059 fn get_returns_value_at_path() {
1060 let c = make_config(vec![("host", sv("localhost"))]);
1061 assert!(c.get("host").is_some());
1062 }
1063
1064 #[test]
1065 fn get_returns_none_for_missing() {
1066 let c = make_config(vec![]);
1067 assert!(c.get("missing").is_none());
1068 }
1069
1070 #[test]
1071 fn get_string_returns_string() {
1072 let c = make_config(vec![("host", sv("localhost"))]);
1073 assert_eq!(c.get_string("host").unwrap(), "localhost");
1074 }
1075
1076 #[test]
1077 fn get_string_coerces_int() {
1078 let c = make_config(vec![("port", iv(8080))]);
1079 assert_eq!(c.get_string("port").unwrap(), "8080");
1080 }
1081
1082 #[test]
1083 fn get_string_coerces_float() {
1084 let c = make_config(vec![("ratio", fv(2.72))]);
1085 let s = c.get_string("ratio").unwrap();
1087 let v: f64 = s.parse().unwrap();
1088 assert!((v - 2.72).abs() < 1e-10);
1089 }
1090
1091 #[test]
1092 fn get_string_coerces_bool() {
1093 let c = make_config(vec![("flag", bv(true))]);
1094 assert_eq!(c.get_string("flag").unwrap(), "true");
1095 }
1096
1097 #[test]
1098 fn get_string_error_on_null() {
1099 let c = make_config(vec![("v", HoconValue::Scalar(ScalarValue::null()))]);
1101 assert!(c.get_string("v").is_err());
1102 }
1103
1104 #[test]
1105 fn get_string_error_on_object() {
1106 let mut inner = IndexMap::new();
1107 inner.insert("x".into(), iv(1));
1108 let c = make_config(vec![("obj", HoconValue::Object(inner))]);
1109 assert!(c.get_string("obj").is_err());
1110 }
1111
1112 #[test]
1113 fn get_i64_returns_number() {
1114 let c = make_config(vec![("port", iv(8080))]);
1115 assert_eq!(c.get_i64("port").unwrap(), 8080);
1116 }
1117
1118 #[test]
1119 fn get_i64_coerces_numeric_string() {
1120 let c = make_config(vec![("port", sv("9999"))]);
1121 assert_eq!(c.get_i64("port").unwrap(), 9999);
1122 }
1123
1124 #[test]
1125 fn get_i64_error_on_non_numeric() {
1126 let c = make_config(vec![("host", sv("localhost"))]);
1127 assert!(c.get_i64("host").is_err());
1128 }
1129
1130 #[test]
1131 fn get_i64_error_on_overflow() {
1132 let c = make_config(vec![("big", sv("1e20"))]);
1134 assert!(c.get_i64("big").is_err());
1135 }
1136
1137 #[test]
1138 fn get_i64_error_on_i64_max_plus_one() {
1139 let c = make_config(vec![("big", sv("9223372036854775808"))]);
1141 assert!(c.get_i64("big").is_err());
1142 }
1143
1144 #[test]
1145 fn get_f64_returns_float() {
1146 let c = make_config(vec![("rate", fv(2.72))]);
1147 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1148 }
1149
1150 #[test]
1151 fn get_f64_coerces_numeric_string() {
1152 let c = make_config(vec![("rate", sv("2.72"))]);
1153 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1154 }
1155
1156 #[test]
1157 fn get_bool_returns_bool() {
1158 let c = make_config(vec![("debug", bv(true))]);
1159 assert!(c.get_bool("debug").unwrap());
1160 }
1161
1162 #[test]
1163 fn get_bool_coerces_string_true() {
1164 let c = make_config(vec![("debug", sv("true"))]);
1165 assert!(c.get_bool("debug").unwrap());
1166 }
1167
1168 #[test]
1169 fn get_bool_coerces_string_false() {
1170 let c = make_config(vec![("debug", sv("false"))]);
1171 assert!(!c.get_bool("debug").unwrap());
1172 }
1173
1174 #[test]
1175 fn get_bool_coerces_yes_no_on_off() {
1176 let c1 = make_config(vec![("v", sv("yes"))]);
1177 assert!(c1.get_bool("v").unwrap());
1178 let c2 = make_config(vec![("v", sv("no"))]);
1179 assert!(!c2.get_bool("v").unwrap());
1180 let c3 = make_config(vec![("v", sv("on"))]);
1181 assert!(c3.get_bool("v").unwrap());
1182 let c4 = make_config(vec![("v", sv("off"))]);
1183 assert!(!c4.get_bool("v").unwrap());
1184 }
1185
1186 #[test]
1187 fn get_bool_is_case_insensitive() {
1188 let c = make_config(vec![("v", sv("TRUE"))]);
1189 assert!(c.get_bool("v").unwrap());
1190 let c2 = make_config(vec![("v", sv("Off"))]);
1191 assert!(!c2.get_bool("v").unwrap());
1192 }
1193
1194 #[test]
1195 fn get_bool_error_on_non_boolean() {
1196 let c = make_config(vec![("v", sv("maybe"))]);
1197 assert!(c.get_bool("v").is_err());
1198 }
1199
1200 #[test]
1201 fn has_returns_true_for_existing() {
1202 let c = make_config(vec![("host", sv("localhost"))]);
1203 assert!(c.has("host"));
1204 }
1205
1206 #[test]
1207 fn has_returns_false_for_missing() {
1208 let c = make_config(vec![]);
1209 assert!(!c.has("missing"));
1210 }
1211
1212 #[test]
1213 fn keys_returns_in_order() {
1214 let c = make_config(vec![("b", iv(2)), ("a", iv(1))]);
1215 assert_eq!(c.keys(), vec!["b", "a"]);
1216 }
1217
1218 #[test]
1219 fn get_nested_dot_path() {
1220 let mut inner = IndexMap::new();
1221 inner.insert("host".into(), sv("localhost"));
1222 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1223 assert_eq!(c.get_string("server.host").unwrap(), "localhost");
1224 }
1225
1226 #[test]
1227 fn get_config_returns_sub_config() {
1228 let mut inner = IndexMap::new();
1229 inner.insert("host".into(), sv("localhost"));
1230 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1231 let sub = c.get_config("server").unwrap();
1232 assert_eq!(sub.get_string("host").unwrap(), "localhost");
1233 }
1234
1235 #[test]
1236 fn get_list_returns_array() {
1237 let items = vec![iv(1), iv(2), iv(3)];
1238 let c = make_config(vec![("list", HoconValue::Array(items))]);
1239 let list = c.get_list("list").unwrap();
1240 assert_eq!(list.len(), 3);
1241 }
1242
1243 #[test]
1244 fn with_fallback_receiver_wins() {
1245 let c1 = make_config(vec![("host", sv("prod"))]);
1246 let c2 = make_config(vec![("host", sv("dev")), ("port", iv(8080))]);
1247 let merged = c1.with_fallback(&c2);
1248 assert_eq!(merged.get_string("host").unwrap(), "prod");
1249 assert_eq!(merged.get_i64("port").unwrap(), 8080);
1250 }
1251
1252 #[test]
1253 fn option_variants_return_none_on_missing() {
1254 let c = make_config(vec![]);
1255 assert!(c.get_string_option("x").is_none());
1256 assert!(c.get_i64_option("x").is_none());
1257 assert!(c.get_f64_option("x").is_none());
1258 assert!(c.get_bool_option("x").is_none());
1259 }
1260
1261 #[test]
1262 fn get_duration_nanoseconds() {
1263 let c = make_config(vec![("t", sv("100 ns"))]);
1264 assert_eq!(
1265 c.get_duration("t").unwrap(),
1266 std::time::Duration::from_nanos(100)
1267 );
1268 }
1269
1270 #[test]
1271 fn get_duration_milliseconds() {
1272 let c = make_config(vec![("t", sv("500 ms"))]);
1273 assert_eq!(
1274 c.get_duration("t").unwrap(),
1275 std::time::Duration::from_millis(500)
1276 );
1277 }
1278
1279 #[test]
1280 fn get_duration_seconds() {
1281 let c = make_config(vec![("t", sv("30 seconds"))]);
1282 assert_eq!(
1283 c.get_duration("t").unwrap(),
1284 std::time::Duration::from_secs(30)
1285 );
1286 }
1287
1288 #[test]
1289 fn get_duration_minutes() {
1290 let c = make_config(vec![("t", sv("5 m"))]);
1291 assert_eq!(
1292 c.get_duration("t").unwrap(),
1293 std::time::Duration::from_secs(300)
1294 );
1295 }
1296
1297 #[test]
1298 fn get_duration_hours() {
1299 let c = make_config(vec![("t", sv("2 hours"))]);
1300 assert_eq!(
1301 c.get_duration("t").unwrap(),
1302 std::time::Duration::from_secs(7200)
1303 );
1304 }
1305
1306 #[test]
1307 fn get_duration_days() {
1308 let c = make_config(vec![("t", sv("1 d"))]);
1309 assert_eq!(
1310 c.get_duration("t").unwrap(),
1311 std::time::Duration::from_secs(86400)
1312 );
1313 }
1314
1315 #[test]
1316 fn get_duration_fractional() {
1317 let c = make_config(vec![("t", sv("1.5 hours"))]);
1318 assert_eq!(
1319 c.get_duration("t").unwrap(),
1320 std::time::Duration::from_secs(5400)
1321 );
1322 }
1323
1324 #[test]
1325 fn get_duration_no_space() {
1326 let c = make_config(vec![("t", sv("100ms"))]);
1327 assert_eq!(
1328 c.get_duration("t").unwrap(),
1329 std::time::Duration::from_millis(100)
1330 );
1331 }
1332
1333 #[test]
1334 fn get_duration_singular_unit() {
1335 let c = make_config(vec![("t", sv("1 second"))]);
1336 assert_eq!(
1337 c.get_duration("t").unwrap(),
1338 std::time::Duration::from_secs(1)
1339 );
1340 }
1341
1342 #[test]
1343 fn get_duration_error_invalid_unit() {
1344 let c = make_config(vec![("t", sv("100 foos"))]);
1345 assert!(c.get_duration("t").is_err());
1346 }
1347
1348 #[test]
1349 fn get_duration_option_missing() {
1350 let c = make_config(vec![]);
1351 assert!(c.get_duration_option("t").is_none());
1352 }
1353
1354 #[test]
1355 fn get_bytes_plain() {
1356 let c = make_config(vec![("s", sv("100 B"))]);
1357 assert_eq!(c.get_bytes("s").unwrap(), 100);
1358 }
1359
1360 #[test]
1361 fn get_bytes_kilobytes() {
1362 let c = make_config(vec![("s", sv("10 KB"))]);
1363 assert_eq!(c.get_bytes("s").unwrap(), 10_000);
1364 }
1365
1366 #[test]
1367 fn get_bytes_kibibytes() {
1368 let c = make_config(vec![("s", sv("1 KiB"))]);
1369 assert_eq!(c.get_bytes("s").unwrap(), 1_024);
1370 }
1371
1372 #[test]
1373 fn get_bytes_megabytes() {
1374 let c = make_config(vec![("s", sv("5 MB"))]);
1375 assert_eq!(c.get_bytes("s").unwrap(), 5_000_000);
1376 }
1377
1378 #[test]
1379 fn get_bytes_mebibytes() {
1380 let c = make_config(vec![("s", sv("1 MiB"))]);
1381 assert_eq!(c.get_bytes("s").unwrap(), 1_048_576);
1382 }
1383
1384 #[test]
1385 fn get_bytes_gigabytes() {
1386 let c = make_config(vec![("s", sv("2 GB"))]);
1387 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000_000);
1388 }
1389
1390 #[test]
1391 fn get_bytes_gibibytes() {
1392 let c = make_config(vec![("s", sv("1 GiB"))]);
1393 assert_eq!(c.get_bytes("s").unwrap(), 1_073_741_824);
1394 }
1395
1396 #[test]
1397 fn get_bytes_terabytes() {
1398 let c = make_config(vec![("s", sv("1 TB"))]);
1399 assert_eq!(c.get_bytes("s").unwrap(), 1_000_000_000_000);
1400 }
1401
1402 #[test]
1403 fn get_bytes_tebibytes() {
1404 let c = make_config(vec![("s", sv("1 TiB"))]);
1405 assert_eq!(c.get_bytes("s").unwrap(), 1_099_511_627_776);
1406 }
1407
1408 #[test]
1409 fn get_bytes_no_space() {
1410 let c = make_config(vec![("s", sv("512MB"))]);
1411 assert_eq!(c.get_bytes("s").unwrap(), 512_000_000);
1412 }
1413
1414 #[test]
1415 fn get_bytes_long_unit() {
1416 let c = make_config(vec![("s", sv("2 megabytes"))]);
1417 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000);
1418 }
1419
1420 #[test]
1421 fn get_bytes_error_invalid_unit() {
1422 let c = make_config(vec![("s", sv("100 XB"))]);
1423 assert!(c.get_bytes("s").is_err());
1424 }
1425
1426 #[test]
1427 fn get_bytes_option_missing() {
1428 let c = make_config(vec![]);
1429 assert!(c.get_bytes_option("s").is_none());
1430 }
1431
1432 #[test]
1433 fn get_bytes_fractional_rounds() {
1434 let c = make_config(vec![("s", sv("1.5 KiB"))]);
1436 assert_eq!(c.get_bytes("s").unwrap(), 1536);
1437 }
1438
1439 #[test]
1444 fn parse_duration_bare_integer_uses_ms_default() {
1445 assert_eq!(
1446 parse_duration("500"),
1447 Some(std::time::Duration::from_millis(500))
1448 );
1449 }
1450 #[test]
1451 fn parse_duration_leading_ws_bare() {
1452 assert_eq!(
1453 parse_duration(" 500"),
1454 Some(std::time::Duration::from_millis(500))
1455 );
1456 }
1457 #[test]
1458 fn parse_duration_trailing_ws_bare() {
1459 assert_eq!(
1460 parse_duration("500 "),
1461 Some(std::time::Duration::from_millis(500))
1462 );
1463 }
1464 #[test]
1465 fn parse_duration_both_ws_bare() {
1466 assert_eq!(
1467 parse_duration(" 500 "),
1468 Some(std::time::Duration::from_millis(500))
1469 );
1470 }
1471 #[test]
1472 fn parse_duration_fractional_bare_uses_nanos() {
1473 let d = parse_duration("500.5").unwrap();
1474 assert_eq!(d.as_nanos(), 500_500_000);
1475 }
1476 #[test]
1477 fn parse_duration_empty_is_none() {
1478 assert!(parse_duration("").is_none());
1479 }
1480 #[test]
1481 fn parse_duration_ws_only_is_none() {
1482 assert!(parse_duration(" ").is_none());
1483 }
1484 #[test]
1485 fn parse_duration_unit_only_is_none() {
1486 assert!(parse_duration("ms").is_none());
1487 }
1488
1489 #[test]
1498 fn parse_duration_integer_overflow_weeks_is_none() {
1499 assert!(parse_duration("9223372036854775807 weeks").is_none());
1501 }
1502
1503 #[test]
1504 fn parse_duration_integer_overflow_days_is_none() {
1505 assert!(parse_duration("9223372036854775807 days").is_none());
1507 }
1508
1509 #[test]
1510 fn parse_duration_integer_max_u64_nanos_succeeds() {
1511 let d = parse_duration("18446744073709551615ns").unwrap();
1513 assert_eq!(d.as_nanos(), u64::MAX as u128);
1514 }
1515
1516 #[test]
1517 fn parse_duration_fractional_overflow_is_none() {
1518 assert!(parse_duration("1e30 d").is_none());
1520 }
1521
1522 #[test]
1523 fn parse_duration_fractional_above_u64_max_is_none() {
1524 assert!(parse_duration("18446744073709551616ns").is_none());
1527 }
1528
1529 #[test]
1530 fn parse_duration_fractional_succeeds_below_boundary() {
1531 let d = parse_duration("1.5w").unwrap();
1533 assert_eq!(d.as_nanos(), 907_200_000_000_000u128);
1534 }
1535
1536 #[test]
1541 fn parse_bytes_leading_trailing_ws_bare() {
1542 assert_eq!(parse_bytes(" 1024 "), Some(1024));
1543 }
1544 #[test]
1545 fn parse_bytes_fractional_truncated() {
1546 assert_eq!(parse_bytes("1024.5"), Some(1024));
1547 }
1548 #[test]
1549 fn get_bytes_negative_accessor_rejects() {
1550 use std::collections::HashMap;
1551 let cfg = crate::parse_with_env(r#"b = "-1""#, &HashMap::new()).unwrap();
1552 assert!(
1553 cfg.get_bytes("b").is_err(),
1554 "ub04: negative byte size must error at accessor (string path)"
1555 );
1556 }
1557 #[test]
1558 fn get_bytes_negative_bare_number_rejects() {
1559 use std::collections::HashMap;
1560 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1562 assert!(
1563 cfg.get_bytes("b").is_err(),
1564 "ub04-bare: bare numeric -1 must error at accessor (both paths must hit guard)"
1565 );
1566 }
1567 #[test]
1568 fn get_bytes_option_negative_bare_number_is_none() {
1569 use std::collections::HashMap;
1570 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1571 assert!(
1572 cfg.get_bytes_option("b").is_none(),
1573 "ub04-bare-option: get_bytes_option must return None for bare numeric -1"
1574 );
1575 }
1576
1577 #[test]
1582 fn parse_period_bare_integer_uses_days_default() {
1583 assert_eq!(parse_period("7"), Some((0, 0, 7)));
1584 }
1585 #[test]
1586 fn parse_period_leading_trailing_ws() {
1587 assert_eq!(parse_period(" 7 "), Some((0, 0, 7)));
1588 }
1589 #[test]
1590 fn parse_period_fractional_rejected() {
1591 assert!(parse_period("7.5").is_none());
1592 }
1593 #[test]
1594 fn parse_period_negative_allowed() {
1595 assert_eq!(parse_period("-7"), Some((0, 0, -7)));
1596 }
1597 #[test]
1598 fn parse_period_weeks_unit() {
1599 assert_eq!(parse_period("7w"), Some((0, 0, 49)));
1600 }
1601 #[test]
1602 fn parse_period_months_unit() {
1603 assert_eq!(parse_period("3m"), Some((0, 3, 0)));
1604 }
1605 #[test]
1606 fn parse_period_years_unit() {
1607 assert_eq!(parse_period("2y"), Some((2, 0, 0)));
1608 }
1609 #[test]
1610 fn parse_period_days_explicit() {
1611 assert_eq!(parse_period("5d"), Some((0, 0, 5)));
1612 }
1613 #[test]
1614 fn parse_period_empty_is_none() {
1615 assert!(parse_period("").is_none());
1616 }
1617
1618 #[test]
1619 fn split_config_path_consecutive_dots_preserve_empty() {
1620 let segs = split_config_path("a..b");
1621 assert_eq!(segs, vec!["a", "", "b"]);
1622 }
1623
1624 #[test]
1625 fn split_config_path_trailing_dot_empty_segment() {
1626 let segs = split_config_path("a.b.");
1627 assert_eq!(segs, vec!["a", "b", ""]);
1628 }
1629
1630 #[test]
1631 fn split_config_path_quoted_escape() {
1632 let segs = split_config_path(r#""a\"b""#);
1634 assert_eq!(segs, vec!["a\"b"]);
1635 }
1636
1637 #[test]
1638 fn split_config_path_quoted_with_dot() {
1639 let segs = split_config_path(r#"server."web.api".port"#);
1640 assert_eq!(segs, vec!["server", "web.api", "port"]);
1641 }
1642}