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(
147 &self,
148 opts: crate::options::ResolveOptions,
149 ) -> Result<Config, crate::error::HoconError> {
150 use crate::error::{HoconError, ParseError};
151 if self.is_resolved() {
152 return Ok(Config {
153 root: self.root.clone(),
154 resolved: true,
155 parse_base_dir: self.parse_base_dir.clone(),
156 origin_description: self.origin_description.clone(),
157 unresolved_tree: None,
158 });
159 }
160
161 let tree = match &self.unresolved_tree {
162 Some(t) => t.clone(),
163 None => crate::resolver::hocon_map_to_res_obj(&self.root),
164 };
165
166 let env: std::collections::HashMap<String, String> = if opts.use_system_environment {
167 crate::sysenv::vars().collect()
168 } else {
169 std::collections::HashMap::new()
170 };
171 let internal_opts = crate::resolver::InternalResolveOptions::new(env)
172 .with_base_dir_opt(self.parse_base_dir.clone())
173 .with_allow_unresolved(opts.allow_unresolved)
174 .with_use_system_environment(opts.use_system_environment);
175
176 let pre_resolution_tree = if opts.allow_unresolved {
186 Some(tree.clone())
187 } else {
188 None
189 };
190
191 let resolved_value = crate::resolver::resolve_tree(tree, &internal_opts)?;
192 match resolved_value {
193 HoconValue::Object(fields) => {
194 let resolved = !crate::resolver::contains_placeholders_in_hocon_map(&fields);
195 let unresolved_tree = if resolved {
196 None
197 } else {
198 pre_resolution_tree
200 };
201 Ok(Config {
202 root: fields,
203 resolved,
204 parse_base_dir: self.parse_base_dir.clone(),
205 origin_description: self.origin_description.clone(),
206 unresolved_tree,
207 })
208 }
209 _ => Err(HoconError::Parse(ParseError {
210 message: "root must be an object".into(),
211 line: 1,
212 col: 1,
213 })),
214 }
215 }
216
217 pub fn resolve_with(
232 &self,
233 source: &Config,
234 opts: crate::options::ResolveOptions,
235 ) -> Result<Config, crate::error::HoconError> {
236 use crate::error::{HoconError, ParseError};
237 if !source.is_resolved() {
238 return Err(HoconError::NotResolved(NotResolvedError {
239 path: "<source>".into(),
240 }));
241 }
242
243 if self.is_resolved() {
244 return Ok(Config {
245 root: self.root.clone(),
246 resolved: true,
247 parse_base_dir: self.parse_base_dir.clone(),
248 origin_description: self.origin_description.clone(),
249 unresolved_tree: None,
250 });
251 }
252
253 let receiver_root_snapshot = self.root.clone();
255
256 let recv_obj = match &self.unresolved_tree {
257 Some(t) => t.clone(),
258 None => crate::resolver::hocon_map_to_res_obj(&self.root),
259 };
260 let src_obj = crate::resolver::hocon_map_to_res_obj(&source.root);
261 let merged = crate::resolver::merge_unresolved(recv_obj, src_obj);
262
263 let env: std::collections::HashMap<String, String> = if opts.use_system_environment {
264 crate::sysenv::vars().collect()
265 } else {
266 std::collections::HashMap::new()
267 };
268 let internal_opts = crate::resolver::InternalResolveOptions::new(env)
269 .with_base_dir_opt(self.parse_base_dir.clone())
270 .with_allow_unresolved(opts.allow_unresolved)
271 .with_use_system_environment(opts.use_system_environment);
272
273 let pre_resolution_tree = if opts.allow_unresolved {
277 Some(merged.clone())
278 } else {
279 None
280 };
281
282 let resolved_value = crate::resolver::resolve_tree(merged, &internal_opts)?;
283
284 let filtered = match resolved_value {
285 HoconValue::Object(mut fields) => {
286 filter_hocon_object_by_receiver(&mut fields, &receiver_root_snapshot);
288 fields
289 }
290 _ => {
291 return Err(HoconError::Parse(ParseError {
292 message: "root must be an object".into(),
293 line: 1,
294 col: 1,
295 }));
296 }
297 };
298
299 let resolved = !crate::resolver::contains_placeholders_in_hocon_map(&filtered);
300 let unresolved_tree = if resolved {
301 None
302 } else {
303 pre_resolution_tree
305 };
306 Ok(Config {
307 root: filtered,
308 resolved,
309 parse_base_dir: self.parse_base_dir.clone(),
310 origin_description: self.origin_description.clone(),
311 unresolved_tree,
312 })
313 }
314
315 fn lookup_node(&self, path: &str) -> Option<&HoconValue> {
317 let segments = split_config_path(path);
318 lookup_in_map_by_segments(&self.root, &segments)
319 }
320
321 pub fn get(&self, path: &str) -> Option<&HoconValue> {
324 self.lookup_node(path)
325 }
326
327 pub fn get_string(&self, path: &str) -> Result<String, ConfigError> {
334 match self.lookup_node(path) {
335 None => Err(missing(path)),
336 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
337 Some(HoconValue::Scalar(sv)) => {
338 if sv.value_type == ScalarType::Null {
339 return Err(type_mismatch(path, "String"));
340 }
341 Ok(sv.raw.clone())
342 }
343 _ => Err(type_mismatch(path, "String")),
344 }
345 }
346
347 pub fn get_i64(&self, path: &str) -> Result<i64, ConfigError> {
353 match self.lookup_node(path) {
354 None => Err(missing(path)),
355 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
356 Some(HoconValue::Scalar(sv)) => {
357 sv.raw
360 .parse::<i64>()
361 .ok()
362 .or_else(|| crate::value::whole_float_to_i64(&sv.raw))
363 .ok_or_else(|| type_mismatch(path, "i64"))
364 }
365 _ => Err(type_mismatch(path, "i64")),
366 }
367 }
368
369 pub fn get_f64(&self, path: &str) -> Result<f64, ConfigError> {
375 match self.lookup_node(path) {
376 None => Err(missing(path)),
377 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
378 Some(HoconValue::Scalar(sv)) => sv
379 .raw
380 .parse::<f64>()
381 .map_err(|_| type_mismatch(path, "f64")),
382 _ => Err(type_mismatch(path, "f64")),
383 }
384 }
385
386 pub fn get_bool(&self, path: &str) -> Result<bool, ConfigError> {
392 match self.lookup_node(path) {
393 None => Err(missing(path)),
394 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
395 Some(HoconValue::Scalar(sv)) => match sv.raw.to_lowercase().as_str() {
396 "true" | "yes" | "on" => Ok(true),
397 "false" | "no" | "off" => Ok(false),
398 _ => Err(type_mismatch(path, "bool")),
399 },
400 _ => Err(type_mismatch(path, "bool")),
401 }
402 }
403
404 pub fn get_config(&self, path: &str) -> Result<Config, ConfigError> {
408 match self.lookup_node(path) {
409 None => Err(missing(path)),
410 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
411 Some(HoconValue::Object(map)) => Ok(Config::new(map.clone())),
412 _ => Err(type_mismatch(path, "Object")),
413 }
414 }
415
416 pub fn get_list(&self, path: &str) -> Result<Vec<HoconValue>, ConfigError> {
424 match self.lookup_node(path) {
425 None => Err(missing(path)),
426 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
427 Some(HoconValue::Array(items)) => Ok(items.clone()),
428 Some(v @ HoconValue::Object(_)) => {
429 numeric_object_to_array(v).ok_or_else(|| type_mismatch(path, "Array"))
434 }
435 _ => Err(type_mismatch(path, "Array")),
436 }
437 }
438
439 pub fn get_string_option(&self, path: &str) -> Option<String> {
441 self.get_string(path).ok()
442 }
443
444 pub fn get_i64_option(&self, path: &str) -> Option<i64> {
446 self.get_i64(path).ok()
447 }
448
449 pub fn get_f64_option(&self, path: &str) -> Option<f64> {
451 self.get_f64(path).ok()
452 }
453
454 pub fn get_bool_option(&self, path: &str) -> Option<bool> {
456 self.get_bool(path).ok()
457 }
458
459 pub fn get_config_option(&self, path: &str) -> Option<Config> {
461 self.get_config(path).ok()
462 }
463
464 pub fn get_list_option(&self, path: &str) -> Option<Vec<HoconValue>> {
466 self.get_list(path).ok()
467 }
468
469 pub fn get_duration(&self, path: &str) -> Result<std::time::Duration, ConfigError> {
484 match self.lookup_node(path) {
485 None => Err(missing(path)),
486 Some(HoconValue::Scalar(sv)) => {
487 if let Some(d) = parse_duration(&sv.raw) {
489 return Ok(d);
490 }
491 if sv.value_type == ScalarType::Number {
493 if let Ok(n) = sv.raw.parse::<i64>() {
494 if n < 0 {
495 return Err(ConfigError {
496 message: format!("negative duration at {}: {}", path, sv.raw),
497 path: path.to_string(),
498 });
499 }
500 return Ok(std::time::Duration::from_millis(n as u64));
501 }
502 if let Ok(f) = sv.raw.parse::<f64>() {
503 if f < 0.0 || !f.is_finite() {
504 return Err(ConfigError {
505 message: format!("invalid duration at {}: {}", path, sv.raw),
506 path: path.to_string(),
507 });
508 }
509 let secs = f / 1000.0;
510 if secs > u64::MAX as f64 {
511 return Err(ConfigError {
512 message: format!("duration too large at {}: {}", path, sv.raw),
513 path: path.to_string(),
514 });
515 }
516 return Ok(std::time::Duration::from_secs_f64(secs));
517 }
518 }
519 Err(ConfigError {
520 message: format!("invalid duration at {}: {}", path, sv.raw),
521 path: path.to_string(),
522 })
523 }
524 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
525 _ => Err(ConfigError {
526 message: format!("expected duration at {}", path),
527 path: path.to_string(),
528 }),
529 }
530 }
531
532 pub fn get_duration_option(&self, path: &str) -> Option<std::time::Duration> {
534 self.get_duration(path).ok()
535 }
536
537 pub fn get_bytes(&self, path: &str) -> Result<i64, ConfigError> {
548 let v = self.lookup_node(path).ok_or_else(|| ConfigError {
549 message: format!("path not found: {}", path),
550 path: path.to_string(),
551 })?;
552 match v {
553 HoconValue::Scalar(sv) => {
554 let n: i64 = if sv.value_type == ScalarType::Number {
555 sv.raw.parse::<i64>().map_err(|_| ConfigError {
558 message: format!("expected byte size at {}", path),
559 path: path.to_string(),
560 })?
561 } else {
562 parse_bytes(&sv.raw).ok_or_else(|| ConfigError {
564 message: format!("invalid byte size at {}: {}", path, sv.raw),
565 path: path.to_string(),
566 })?
567 };
568 if n < 0 {
572 return Err(ConfigError {
573 message: format!("negative byte size at {}: {}", path, sv.raw),
574 path: path.to_string(),
575 });
576 }
577 Ok(n)
578 }
579 HoconValue::Placeholder(_) => Err(not_resolved(path)),
580 _ => Err(ConfigError {
581 message: format!("expected byte size at {}", path),
582 path: path.to_string(),
583 }),
584 }
585 }
586
587 pub fn get_bytes_option(&self, path: &str) -> Option<i64> {
589 self.get_bytes(path).ok()
590 }
591
592 pub fn get_period(&self, path: &str) -> Result<Period, ConfigError> {
602 match self.lookup_node(path) {
603 None => Err(missing(path)),
604 Some(HoconValue::Scalar(sv)) => {
605 if let Some((y, mo, d)) = parse_period(&sv.raw) {
606 return Ok(Period::new(y, mo, d));
607 }
608 if sv.value_type == ScalarType::Number {
610 if let Ok(n) = sv.raw.parse::<i32>() {
611 return Ok(Period::new(0, 0, n));
612 }
613 }
614 Err(ConfigError {
615 message: format!("invalid period at {}: {}", path, sv.raw),
616 path: path.to_string(),
617 })
618 }
619 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
620 _ => Err(ConfigError {
621 message: format!("expected period at {}", path),
622 path: path.to_string(),
623 }),
624 }
625 }
626
627 pub fn get_period_option(&self, path: &str) -> Option<Period> {
629 self.get_period(path).ok()
630 }
631
632 pub fn has(&self, path: &str) -> bool {
634 self.lookup_node(path).is_some()
635 }
636
637 pub fn keys(&self) -> Vec<&str> {
639 self.root.keys().map(|s| s.as_str()).collect()
640 }
641
642 pub fn with_fallback(&self, fallback: &Config) -> Config {
664 let recv_obj = match &self.unresolved_tree {
665 Some(t) => t.clone(),
666 None => crate::resolver::hocon_map_to_res_obj(&self.root),
667 };
668 let fb_obj = match &fallback.unresolved_tree {
669 Some(t) => t.clone(),
670 None => crate::resolver::hocon_map_to_res_obj(&fallback.root),
671 };
672 let merged = crate::resolver::merge_unresolved(recv_obj, fb_obj);
673 Config::new_from_res_obj(
674 merged,
675 self.parse_base_dir.clone(),
676 self.origin_description.clone(),
677 )
678 }
679}
680
681fn split_config_path(path: &str) -> Vec<String> {
686 let mut segments = Vec::new();
687 let chars: Vec<char> = path.chars().collect();
688 let mut i = 0;
689 while i < chars.len() {
690 if chars[i] == '"' {
691 i += 1; let mut seg = String::new();
694 let mut closed = false;
695 while i < chars.len() {
696 if chars[i] == '\\' && i + 1 < chars.len() {
697 seg.push(chars[i + 1]);
698 i += 2;
699 continue;
700 }
701 if chars[i] == '"' {
702 closed = true;
703 i += 1;
704 break;
705 }
706 seg.push(chars[i]);
707 i += 1;
708 }
709 if !closed {
710 return vec![path.to_string()]; }
712 segments.push(seg);
713 if i < chars.len() && chars[i] == '.' {
715 i += 1;
716 }
717 } else {
718 let start = i;
721 while i < chars.len() && chars[i] != '.' && chars[i] != '"' {
722 i += 1;
723 }
724 segments.push(chars[start..i].iter().collect());
725 if i < chars.len() && chars[i] == '.' {
727 i += 1;
728 }
729 }
730 }
731 if path.ends_with('.') {
733 segments.push(String::new());
734 }
735 segments
736}
737
738fn lookup_in_map_by_segments<'a>(
739 map: &'a IndexMap<String, HoconValue>,
740 segments: &[String],
741) -> Option<&'a HoconValue> {
742 if segments.is_empty() {
743 return None;
744 }
745 let key = &segments[0];
746 let rest = &segments[1..];
747 let value = map.get(key)?;
748 if rest.is_empty() {
749 Some(value)
750 } else {
751 match value {
752 HoconValue::Object(inner) => lookup_in_map_by_segments(inner, rest),
753 _ => None,
754 }
755 }
756}
757
758#[cfg(feature = "serde")]
759impl Config {
760 pub fn deserialize<T: ::serde::de::DeserializeOwned>(
765 &self,
766 ) -> Result<T, crate::serde::DeserializeError> {
767 let value = HoconValue::Object(self.root.clone());
768 crate::serde::from_value(&value)
769 }
770
771 pub fn get_as<T: ::serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, ConfigError> {
783 let node = self.lookup_node(path).ok_or_else(|| missing(path))?;
784 if value_contains_placeholder(node) {
785 return Err(not_resolved(path));
786 }
787 crate::serde::from_value(node).map_err(|e| ConfigError {
788 message: e.message,
789 path: path.to_string(),
790 })
791 }
792}
793
794fn trim_hocon_ws(s: &str) -> &str {
799 s.trim_matches(is_hocon_whitespace)
800}
801
802fn is_integer_str(s: &str) -> bool {
807 let s = s.strip_prefix(['+', '-']).unwrap_or(s);
808 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
809}
810
811fn parse_duration(s: &str) -> Option<std::time::Duration> {
828 let s = trim_hocon_ws(s);
829 if s.is_empty() {
830 return None;
831 }
832
833 let num_end = s
835 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
836 .unwrap_or(s.len());
837 let num_str = s[..num_end].trim();
838 let unit_str = trim_hocon_ws(&s[num_end..]);
841
842 if num_str.is_empty() {
843 return None;
844 }
845
846 let nanos_per_unit: f64 = match unit_str {
847 "" | "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 1_000_000.0,
849 "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => 1.0,
850 "us" | "micro" | "micros" | "microsecond" | "microseconds" => 1_000.0,
851 "s" | "second" | "seconds" => 1_000_000_000.0,
852 "m" | "minute" | "minutes" => 60_000_000_000.0,
853 "h" | "hour" | "hours" => 3_600_000_000_000.0,
854 "d" | "day" | "days" => 86_400_000_000_000.0,
855 "w" | "week" | "weeks" => 604_800_000_000_000.0,
856 _ => return None,
857 };
858
859 if is_integer_str(num_str) {
861 let n_i128: i128 = num_str.parse().ok()?;
866 if n_i128 < 0 {
867 return None;
868 }
869 let n_u64: u64 = n_i128.try_into().ok()?;
870 let unit_u64 = nanos_per_unit as u64;
875 let nanos = n_u64.checked_mul(unit_u64)?;
876 return Some(std::time::Duration::from_nanos(nanos));
877 }
878
879 let f: f64 = num_str.parse().ok()?;
881 if f < 0.0 || !f.is_finite() {
882 return None;
883 }
884 let product = f * nanos_per_unit;
890 if !product.is_finite() || product >= 2f64.powi(64) {
891 return None;
892 }
893 Some(std::time::Duration::from_nanos(product as u64))
894}
895
896pub(crate) fn parse_period(s: &str) -> Option<(i32, i32, i32)> {
909 let s = trim_hocon_ws(s);
910 if s.is_empty() {
911 return None;
912 }
913
914 let num_end = s
916 .find(|c: char| !c.is_ascii_digit() && c != '-' && c != '+')
917 .unwrap_or(s.len());
918 let num_str = s[..num_end].trim();
919 let unit_str = trim_hocon_ws(&s[num_end..]);
920
921 if num_str.is_empty() {
922 return None;
923 }
924
925 if !is_integer_str(num_str) {
927 return None;
928 }
929
930 let n: i32 = num_str.parse().ok()?;
931
932 match unit_str {
935 "" | "d" | "day" | "days" => Some((0, 0, n)),
937 "w" | "week" | "weeks" => Some((0, 0, n.checked_mul(7)?)),
938 "m" | "mo" | "month" | "months" => Some((0, n, 0)),
939 "y" | "year" | "years" => Some((n, 0, 0)),
940 _ => None,
941 }
942}
943
944fn parse_bytes(s: &str) -> Option<i64> {
956 let s = trim_hocon_ws(s);
957 let num_end = s
958 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
959 .unwrap_or(s.len());
960 let num_str = s[..num_end].trim();
961 let unit_str = trim_hocon_ws(&s[num_end..]);
962
963 if num_str.is_empty() {
964 return None;
965 }
966
967 let multiplier: i64 = match unit_str {
978 "" | "B" | "byte" | "bytes" => 1,
979 "K" | "k" => 1_024,
981 "M" | "m" => 1_048_576,
982 "G" | "g" => 1_073_741_824,
983 "T" | "t" => 1_099_511_627_776,
984 "P" | "p" => 1_125_899_906_842_624,
985 "E" | "e" => 1_152_921_504_606_846_976,
986 "KB" | "kilobyte" | "kilobytes" => 1_000,
988 "KiB" | "Ki" | "kibibyte" | "kibibytes" => 1_024,
989 "MB" | "megabyte" | "megabytes" => 1_000_000,
990 "MiB" | "Mi" | "mebibyte" | "mebibytes" => 1_048_576,
991 "GB" | "gigabyte" | "gigabytes" => 1_000_000_000,
992 "GiB" | "Gi" | "gibibyte" | "gibibytes" => 1_073_741_824,
993 "TB" | "terabyte" | "terabytes" => 1_000_000_000_000,
994 "TiB" | "Ti" | "tebibyte" | "tebibytes" => 1_099_511_627_776,
995 _ => return None,
996 };
997
998 if is_integer_str(num_str) {
1000 let n: i64 = num_str.parse().ok()?;
1001 return n.checked_mul(multiplier);
1002 }
1003
1004 let f: f64 = num_str.parse().ok()?;
1006 if !f.is_finite() || f.abs() * multiplier as f64 >= 2f64.powi(63) {
1010 return None;
1011 }
1012 Some((f * multiplier as f64) as i64)
1013}
1014
1015fn missing(path: &str) -> ConfigError {
1016 ConfigError {
1017 message: "key not found".to_string(),
1018 path: path.to_string(),
1019 }
1020}
1021
1022fn type_mismatch(path: &str, expected: &str) -> ConfigError {
1023 ConfigError {
1024 message: format!("expected {}", expected),
1025 path: path.to_string(),
1026 }
1027}
1028
1029fn not_resolved(path: &str) -> ConfigError {
1030 ConfigError {
1031 message: "value is not resolved (call Config::resolve() before accessing values)"
1032 .to_string(),
1033 path: path.to_string(),
1034 }
1035}
1036
1037#[cfg(feature = "serde")]
1041fn value_contains_placeholder(value: &HoconValue) -> bool {
1042 match value {
1043 HoconValue::Placeholder(_) => true,
1044 HoconValue::Object(map) => map.values().any(value_contains_placeholder),
1045 HoconValue::Array(items) => items.iter().any(value_contains_placeholder),
1046 HoconValue::Scalar(_) => false,
1047 }
1048}
1049
1050fn filter_hocon_object_by_receiver(
1053 resolved: &mut IndexMap<String, HoconValue>,
1054 receiver_shape: &IndexMap<String, HoconValue>,
1055) {
1056 resolved.retain(|k, v| {
1057 if !receiver_shape.contains_key(k) {
1058 return false;
1059 }
1060 if let (HoconValue::Object(inner_res), Some(HoconValue::Object(inner_recv))) =
1061 (v, receiver_shape.get(k))
1062 {
1063 filter_hocon_object_by_receiver(inner_res, inner_recv);
1064 }
1065 true
1066 });
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use super::*;
1072 use crate::value::{HoconValue, ScalarValue};
1073 use indexmap::IndexMap;
1074
1075 fn make_config(entries: Vec<(&str, HoconValue)>) -> Config {
1076 let mut map = IndexMap::new();
1077 for (k, v) in entries {
1078 map.insert(k.to_string(), v);
1079 }
1080 Config::new(map)
1081 }
1082
1083 fn sv(s: &str) -> HoconValue {
1084 HoconValue::Scalar(ScalarValue::string(s.into()))
1085 }
1086 fn iv(n: i64) -> HoconValue {
1087 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1088 }
1089 fn fv(n: f64) -> HoconValue {
1090 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1091 }
1092 fn bv(b: bool) -> HoconValue {
1093 HoconValue::Scalar(ScalarValue::boolean(b))
1094 }
1095
1096 #[test]
1097 fn get_returns_value_at_path() {
1098 let c = make_config(vec![("host", sv("localhost"))]);
1099 assert!(c.get("host").is_some());
1100 }
1101
1102 #[test]
1103 fn get_returns_none_for_missing() {
1104 let c = make_config(vec![]);
1105 assert!(c.get("missing").is_none());
1106 }
1107
1108 #[test]
1109 fn get_string_returns_string() {
1110 let c = make_config(vec![("host", sv("localhost"))]);
1111 assert_eq!(c.get_string("host").unwrap(), "localhost");
1112 }
1113
1114 #[test]
1115 fn get_string_coerces_int() {
1116 let c = make_config(vec![("port", iv(8080))]);
1117 assert_eq!(c.get_string("port").unwrap(), "8080");
1118 }
1119
1120 #[test]
1121 fn get_string_coerces_float() {
1122 let c = make_config(vec![("ratio", fv(2.72))]);
1123 let s = c.get_string("ratio").unwrap();
1125 let v: f64 = s.parse().unwrap();
1126 assert!((v - 2.72).abs() < 1e-10);
1127 }
1128
1129 #[test]
1130 fn get_string_coerces_bool() {
1131 let c = make_config(vec![("flag", bv(true))]);
1132 assert_eq!(c.get_string("flag").unwrap(), "true");
1133 }
1134
1135 #[test]
1136 fn get_string_error_on_null() {
1137 let c = make_config(vec![("v", HoconValue::Scalar(ScalarValue::null()))]);
1139 assert!(c.get_string("v").is_err());
1140 }
1141
1142 #[test]
1143 fn get_string_error_on_object() {
1144 let mut inner = IndexMap::new();
1145 inner.insert("x".into(), iv(1));
1146 let c = make_config(vec![("obj", HoconValue::Object(inner))]);
1147 assert!(c.get_string("obj").is_err());
1148 }
1149
1150 #[test]
1151 fn get_i64_returns_number() {
1152 let c = make_config(vec![("port", iv(8080))]);
1153 assert_eq!(c.get_i64("port").unwrap(), 8080);
1154 }
1155
1156 #[test]
1157 fn get_i64_coerces_numeric_string() {
1158 let c = make_config(vec![("port", sv("9999"))]);
1159 assert_eq!(c.get_i64("port").unwrap(), 9999);
1160 }
1161
1162 #[test]
1163 fn get_i64_error_on_non_numeric() {
1164 let c = make_config(vec![("host", sv("localhost"))]);
1165 assert!(c.get_i64("host").is_err());
1166 }
1167
1168 #[test]
1169 fn get_i64_error_on_overflow() {
1170 let c = make_config(vec![("big", sv("1e20"))]);
1172 assert!(c.get_i64("big").is_err());
1173 }
1174
1175 #[test]
1176 fn get_i64_error_on_i64_max_plus_one() {
1177 let c = make_config(vec![("big", sv("9223372036854775808"))]);
1179 assert!(c.get_i64("big").is_err());
1180 }
1181
1182 #[test]
1183 fn get_f64_returns_float() {
1184 let c = make_config(vec![("rate", fv(2.72))]);
1185 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1186 }
1187
1188 #[test]
1189 fn get_f64_coerces_numeric_string() {
1190 let c = make_config(vec![("rate", sv("2.72"))]);
1191 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1192 }
1193
1194 #[test]
1195 fn get_bool_returns_bool() {
1196 let c = make_config(vec![("debug", bv(true))]);
1197 assert!(c.get_bool("debug").unwrap());
1198 }
1199
1200 #[test]
1201 fn get_bool_coerces_string_true() {
1202 let c = make_config(vec![("debug", sv("true"))]);
1203 assert!(c.get_bool("debug").unwrap());
1204 }
1205
1206 #[test]
1207 fn get_bool_coerces_string_false() {
1208 let c = make_config(vec![("debug", sv("false"))]);
1209 assert!(!c.get_bool("debug").unwrap());
1210 }
1211
1212 #[test]
1213 fn get_bool_coerces_yes_no_on_off() {
1214 let c1 = make_config(vec![("v", sv("yes"))]);
1215 assert!(c1.get_bool("v").unwrap());
1216 let c2 = make_config(vec![("v", sv("no"))]);
1217 assert!(!c2.get_bool("v").unwrap());
1218 let c3 = make_config(vec![("v", sv("on"))]);
1219 assert!(c3.get_bool("v").unwrap());
1220 let c4 = make_config(vec![("v", sv("off"))]);
1221 assert!(!c4.get_bool("v").unwrap());
1222 }
1223
1224 #[test]
1225 fn get_bool_is_case_insensitive() {
1226 let c = make_config(vec![("v", sv("TRUE"))]);
1227 assert!(c.get_bool("v").unwrap());
1228 let c2 = make_config(vec![("v", sv("Off"))]);
1229 assert!(!c2.get_bool("v").unwrap());
1230 }
1231
1232 #[test]
1233 fn get_bool_error_on_non_boolean() {
1234 let c = make_config(vec![("v", sv("maybe"))]);
1235 assert!(c.get_bool("v").is_err());
1236 }
1237
1238 #[test]
1239 fn has_returns_true_for_existing() {
1240 let c = make_config(vec![("host", sv("localhost"))]);
1241 assert!(c.has("host"));
1242 }
1243
1244 #[test]
1245 fn has_returns_false_for_missing() {
1246 let c = make_config(vec![]);
1247 assert!(!c.has("missing"));
1248 }
1249
1250 #[test]
1251 fn keys_returns_in_order() {
1252 let c = make_config(vec![("b", iv(2)), ("a", iv(1))]);
1253 assert_eq!(c.keys(), vec!["b", "a"]);
1254 }
1255
1256 #[test]
1257 fn get_nested_dot_path() {
1258 let mut inner = IndexMap::new();
1259 inner.insert("host".into(), sv("localhost"));
1260 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1261 assert_eq!(c.get_string("server.host").unwrap(), "localhost");
1262 }
1263
1264 #[test]
1265 fn get_config_returns_sub_config() {
1266 let mut inner = IndexMap::new();
1267 inner.insert("host".into(), sv("localhost"));
1268 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1269 let sub = c.get_config("server").unwrap();
1270 assert_eq!(sub.get_string("host").unwrap(), "localhost");
1271 }
1272
1273 #[test]
1274 fn get_list_returns_array() {
1275 let items = vec![iv(1), iv(2), iv(3)];
1276 let c = make_config(vec![("list", HoconValue::Array(items))]);
1277 let list = c.get_list("list").unwrap();
1278 assert_eq!(list.len(), 3);
1279 }
1280
1281 #[test]
1282 fn with_fallback_receiver_wins() {
1283 let c1 = make_config(vec![("host", sv("prod"))]);
1284 let c2 = make_config(vec![("host", sv("dev")), ("port", iv(8080))]);
1285 let merged = c1.with_fallback(&c2);
1286 assert_eq!(merged.get_string("host").unwrap(), "prod");
1287 assert_eq!(merged.get_i64("port").unwrap(), 8080);
1288 }
1289
1290 #[test]
1291 fn option_variants_return_none_on_missing() {
1292 let c = make_config(vec![]);
1293 assert!(c.get_string_option("x").is_none());
1294 assert!(c.get_i64_option("x").is_none());
1295 assert!(c.get_f64_option("x").is_none());
1296 assert!(c.get_bool_option("x").is_none());
1297 }
1298
1299 #[test]
1300 fn get_duration_nanoseconds() {
1301 let c = make_config(vec![("t", sv("100 ns"))]);
1302 assert_eq!(
1303 c.get_duration("t").unwrap(),
1304 std::time::Duration::from_nanos(100)
1305 );
1306 }
1307
1308 #[test]
1309 fn get_duration_milliseconds() {
1310 let c = make_config(vec![("t", sv("500 ms"))]);
1311 assert_eq!(
1312 c.get_duration("t").unwrap(),
1313 std::time::Duration::from_millis(500)
1314 );
1315 }
1316
1317 #[test]
1318 fn get_duration_seconds() {
1319 let c = make_config(vec![("t", sv("30 seconds"))]);
1320 assert_eq!(
1321 c.get_duration("t").unwrap(),
1322 std::time::Duration::from_secs(30)
1323 );
1324 }
1325
1326 #[test]
1327 fn get_duration_minutes() {
1328 let c = make_config(vec![("t", sv("5 m"))]);
1329 assert_eq!(
1330 c.get_duration("t").unwrap(),
1331 std::time::Duration::from_secs(300)
1332 );
1333 }
1334
1335 #[test]
1336 fn get_duration_hours() {
1337 let c = make_config(vec![("t", sv("2 hours"))]);
1338 assert_eq!(
1339 c.get_duration("t").unwrap(),
1340 std::time::Duration::from_secs(7200)
1341 );
1342 }
1343
1344 #[test]
1345 fn get_duration_days() {
1346 let c = make_config(vec![("t", sv("1 d"))]);
1347 assert_eq!(
1348 c.get_duration("t").unwrap(),
1349 std::time::Duration::from_secs(86400)
1350 );
1351 }
1352
1353 #[test]
1354 fn get_duration_fractional() {
1355 let c = make_config(vec![("t", sv("1.5 hours"))]);
1356 assert_eq!(
1357 c.get_duration("t").unwrap(),
1358 std::time::Duration::from_secs(5400)
1359 );
1360 }
1361
1362 #[test]
1363 fn get_duration_no_space() {
1364 let c = make_config(vec![("t", sv("100ms"))]);
1365 assert_eq!(
1366 c.get_duration("t").unwrap(),
1367 std::time::Duration::from_millis(100)
1368 );
1369 }
1370
1371 #[test]
1372 fn get_duration_singular_unit() {
1373 let c = make_config(vec![("t", sv("1 second"))]);
1374 assert_eq!(
1375 c.get_duration("t").unwrap(),
1376 std::time::Duration::from_secs(1)
1377 );
1378 }
1379
1380 #[test]
1381 fn get_duration_error_invalid_unit() {
1382 let c = make_config(vec![("t", sv("100 foos"))]);
1383 assert!(c.get_duration("t").is_err());
1384 }
1385
1386 #[test]
1387 fn get_duration_option_missing() {
1388 let c = make_config(vec![]);
1389 assert!(c.get_duration_option("t").is_none());
1390 }
1391
1392 #[test]
1393 fn get_bytes_plain() {
1394 let c = make_config(vec![("s", sv("100 B"))]);
1395 assert_eq!(c.get_bytes("s").unwrap(), 100);
1396 }
1397
1398 #[test]
1399 fn get_bytes_kilobytes() {
1400 let c = make_config(vec![("s", sv("10 KB"))]);
1401 assert_eq!(c.get_bytes("s").unwrap(), 10_000);
1402 }
1403
1404 #[test]
1405 fn get_bytes_kibibytes() {
1406 let c = make_config(vec![("s", sv("1 KiB"))]);
1407 assert_eq!(c.get_bytes("s").unwrap(), 1_024);
1408 }
1409
1410 #[test]
1411 fn get_bytes_megabytes() {
1412 let c = make_config(vec![("s", sv("5 MB"))]);
1413 assert_eq!(c.get_bytes("s").unwrap(), 5_000_000);
1414 }
1415
1416 #[test]
1417 fn get_bytes_mebibytes() {
1418 let c = make_config(vec![("s", sv("1 MiB"))]);
1419 assert_eq!(c.get_bytes("s").unwrap(), 1_048_576);
1420 }
1421
1422 #[test]
1423 fn get_bytes_gigabytes() {
1424 let c = make_config(vec![("s", sv("2 GB"))]);
1425 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000_000);
1426 }
1427
1428 #[test]
1429 fn get_bytes_gibibytes() {
1430 let c = make_config(vec![("s", sv("1 GiB"))]);
1431 assert_eq!(c.get_bytes("s").unwrap(), 1_073_741_824);
1432 }
1433
1434 #[test]
1435 fn get_bytes_terabytes() {
1436 let c = make_config(vec![("s", sv("1 TB"))]);
1437 assert_eq!(c.get_bytes("s").unwrap(), 1_000_000_000_000);
1438 }
1439
1440 #[test]
1441 fn get_bytes_tebibytes() {
1442 let c = make_config(vec![("s", sv("1 TiB"))]);
1443 assert_eq!(c.get_bytes("s").unwrap(), 1_099_511_627_776);
1444 }
1445
1446 #[test]
1447 fn get_bytes_no_space() {
1448 let c = make_config(vec![("s", sv("512MB"))]);
1449 assert_eq!(c.get_bytes("s").unwrap(), 512_000_000);
1450 }
1451
1452 #[test]
1453 fn get_bytes_long_unit() {
1454 let c = make_config(vec![("s", sv("2 megabytes"))]);
1455 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000);
1456 }
1457
1458 #[test]
1459 fn get_bytes_error_invalid_unit() {
1460 let c = make_config(vec![("s", sv("100 XB"))]);
1461 assert!(c.get_bytes("s").is_err());
1462 }
1463
1464 #[test]
1465 fn get_bytes_option_missing() {
1466 let c = make_config(vec![]);
1467 assert!(c.get_bytes_option("s").is_none());
1468 }
1469
1470 #[test]
1471 fn get_bytes_fractional_rounds() {
1472 let c = make_config(vec![("s", sv("1.5 KiB"))]);
1474 assert_eq!(c.get_bytes("s").unwrap(), 1536);
1475 }
1476
1477 #[test]
1482 fn parse_duration_bare_integer_uses_ms_default() {
1483 assert_eq!(
1484 parse_duration("500"),
1485 Some(std::time::Duration::from_millis(500))
1486 );
1487 }
1488 #[test]
1489 fn parse_duration_leading_ws_bare() {
1490 assert_eq!(
1491 parse_duration(" 500"),
1492 Some(std::time::Duration::from_millis(500))
1493 );
1494 }
1495 #[test]
1496 fn parse_duration_trailing_ws_bare() {
1497 assert_eq!(
1498 parse_duration("500 "),
1499 Some(std::time::Duration::from_millis(500))
1500 );
1501 }
1502 #[test]
1503 fn parse_duration_both_ws_bare() {
1504 assert_eq!(
1505 parse_duration(" 500 "),
1506 Some(std::time::Duration::from_millis(500))
1507 );
1508 }
1509 #[test]
1510 fn parse_duration_fractional_bare_uses_nanos() {
1511 let d = parse_duration("500.5").unwrap();
1512 assert_eq!(d.as_nanos(), 500_500_000);
1513 }
1514 #[test]
1515 fn parse_duration_empty_is_none() {
1516 assert!(parse_duration("").is_none());
1517 }
1518 #[test]
1519 fn parse_duration_ws_only_is_none() {
1520 assert!(parse_duration(" ").is_none());
1521 }
1522 #[test]
1523 fn parse_duration_unit_only_is_none() {
1524 assert!(parse_duration("ms").is_none());
1525 }
1526
1527 #[test]
1536 fn parse_duration_integer_overflow_weeks_is_none() {
1537 assert!(parse_duration("9223372036854775807 weeks").is_none());
1539 }
1540
1541 #[test]
1542 fn parse_duration_integer_overflow_days_is_none() {
1543 assert!(parse_duration("9223372036854775807 days").is_none());
1545 }
1546
1547 #[test]
1548 fn parse_duration_integer_max_u64_nanos_succeeds() {
1549 let d = parse_duration("18446744073709551615ns").unwrap();
1551 assert_eq!(d.as_nanos(), u64::MAX as u128);
1552 }
1553
1554 #[test]
1555 fn parse_duration_fractional_overflow_is_none() {
1556 assert!(parse_duration("1e30 d").is_none());
1558 }
1559
1560 #[test]
1561 fn parse_duration_fractional_above_u64_max_is_none() {
1562 assert!(parse_duration("18446744073709551616ns").is_none());
1565 }
1566
1567 #[test]
1568 fn parse_duration_fractional_succeeds_below_boundary() {
1569 let d = parse_duration("1.5w").unwrap();
1571 assert_eq!(d.as_nanos(), 907_200_000_000_000u128);
1572 }
1573
1574 #[test]
1579 fn parse_bytes_leading_trailing_ws_bare() {
1580 assert_eq!(parse_bytes(" 1024 "), Some(1024));
1581 }
1582 #[test]
1583 fn parse_bytes_fractional_truncated() {
1584 assert_eq!(parse_bytes("1024.5"), Some(1024));
1585 }
1586 #[test]
1587 fn get_bytes_negative_accessor_rejects() {
1588 use std::collections::HashMap;
1589 let cfg = crate::parse_with_env(r#"b = "-1""#, &HashMap::new()).unwrap();
1590 assert!(
1591 cfg.get_bytes("b").is_err(),
1592 "ub04: negative byte size must error at accessor (string path)"
1593 );
1594 }
1595 #[test]
1596 fn get_bytes_negative_bare_number_rejects() {
1597 use std::collections::HashMap;
1598 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1600 assert!(
1601 cfg.get_bytes("b").is_err(),
1602 "ub04-bare: bare numeric -1 must error at accessor (both paths must hit guard)"
1603 );
1604 }
1605 #[test]
1606 fn get_bytes_option_negative_bare_number_is_none() {
1607 use std::collections::HashMap;
1608 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1609 assert!(
1610 cfg.get_bytes_option("b").is_none(),
1611 "ub04-bare-option: get_bytes_option must return None for bare numeric -1"
1612 );
1613 }
1614
1615 #[test]
1620 fn parse_period_bare_integer_uses_days_default() {
1621 assert_eq!(parse_period("7"), Some((0, 0, 7)));
1622 }
1623 #[test]
1624 fn parse_period_leading_trailing_ws() {
1625 assert_eq!(parse_period(" 7 "), Some((0, 0, 7)));
1626 }
1627 #[test]
1628 fn parse_period_fractional_rejected() {
1629 assert!(parse_period("7.5").is_none());
1630 }
1631 #[test]
1632 fn parse_period_negative_allowed() {
1633 assert_eq!(parse_period("-7"), Some((0, 0, -7)));
1634 }
1635 #[test]
1636 fn parse_period_weeks_unit() {
1637 assert_eq!(parse_period("7w"), Some((0, 0, 49)));
1638 }
1639 #[test]
1640 fn parse_period_months_unit() {
1641 assert_eq!(parse_period("3m"), Some((0, 3, 0)));
1642 }
1643 #[test]
1644 fn parse_period_years_unit() {
1645 assert_eq!(parse_period("2y"), Some((2, 0, 0)));
1646 }
1647 #[test]
1648 fn parse_period_days_explicit() {
1649 assert_eq!(parse_period("5d"), Some((0, 0, 5)));
1650 }
1651 #[test]
1652 fn parse_period_empty_is_none() {
1653 assert!(parse_period("").is_none());
1654 }
1655
1656 #[test]
1657 fn split_config_path_consecutive_dots_preserve_empty() {
1658 let segs = split_config_path("a..b");
1659 assert_eq!(segs, vec!["a", "", "b"]);
1660 }
1661
1662 #[test]
1663 fn split_config_path_trailing_dot_empty_segment() {
1664 let segs = split_config_path("a.b.");
1665 assert_eq!(segs, vec!["a", "b", ""]);
1666 }
1667
1668 #[test]
1669 fn split_config_path_quoted_escape() {
1670 let segs = split_config_path(r#""a\"b""#);
1672 assert_eq!(segs, vec!["a\"b"]);
1673 }
1674
1675 #[test]
1676 fn split_config_path_quoted_with_dot() {
1677 let segs = split_config_path(r#"server."web.api".port"#);
1678 assert_eq!(segs, vec!["server", "web.api", "port"]);
1679 }
1680}