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 sv.raw
350 .parse::<i64>()
351 .ok()
352 .or_else(|| crate::value::whole_float_to_i64(&sv.raw))
353 .ok_or_else(|| type_mismatch(path, "i64"))
354 }
355 _ => Err(type_mismatch(path, "i64")),
356 }
357 }
358
359 pub fn get_f64(&self, path: &str) -> Result<f64, ConfigError> {
365 match self.lookup_node(path) {
366 None => Err(missing(path)),
367 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
368 Some(HoconValue::Scalar(sv)) => sv
369 .raw
370 .parse::<f64>()
371 .map_err(|_| type_mismatch(path, "f64")),
372 _ => Err(type_mismatch(path, "f64")),
373 }
374 }
375
376 pub fn get_bool(&self, path: &str) -> Result<bool, ConfigError> {
382 match self.lookup_node(path) {
383 None => Err(missing(path)),
384 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
385 Some(HoconValue::Scalar(sv)) => match sv.raw.to_lowercase().as_str() {
386 "true" | "yes" | "on" => Ok(true),
387 "false" | "no" | "off" => Ok(false),
388 _ => Err(type_mismatch(path, "bool")),
389 },
390 _ => Err(type_mismatch(path, "bool")),
391 }
392 }
393
394 pub fn get_config(&self, path: &str) -> Result<Config, ConfigError> {
398 match self.lookup_node(path) {
399 None => Err(missing(path)),
400 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
401 Some(HoconValue::Object(map)) => Ok(Config::new(map.clone())),
402 _ => Err(type_mismatch(path, "Object")),
403 }
404 }
405
406 pub fn get_list(&self, path: &str) -> Result<Vec<HoconValue>, ConfigError> {
414 match self.lookup_node(path) {
415 None => Err(missing(path)),
416 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
417 Some(HoconValue::Array(items)) => Ok(items.clone()),
418 Some(v @ HoconValue::Object(_)) => {
419 numeric_object_to_array(v).ok_or_else(|| type_mismatch(path, "Array"))
424 }
425 _ => Err(type_mismatch(path, "Array")),
426 }
427 }
428
429 pub fn get_string_option(&self, path: &str) -> Option<String> {
431 self.get_string(path).ok()
432 }
433
434 pub fn get_i64_option(&self, path: &str) -> Option<i64> {
436 self.get_i64(path).ok()
437 }
438
439 pub fn get_f64_option(&self, path: &str) -> Option<f64> {
441 self.get_f64(path).ok()
442 }
443
444 pub fn get_bool_option(&self, path: &str) -> Option<bool> {
446 self.get_bool(path).ok()
447 }
448
449 pub fn get_config_option(&self, path: &str) -> Option<Config> {
451 self.get_config(path).ok()
452 }
453
454 pub fn get_list_option(&self, path: &str) -> Option<Vec<HoconValue>> {
456 self.get_list(path).ok()
457 }
458
459 pub fn get_duration(&self, path: &str) -> Result<std::time::Duration, ConfigError> {
474 match self.lookup_node(path) {
475 None => Err(missing(path)),
476 Some(HoconValue::Scalar(sv)) => {
477 if let Some(d) = parse_duration(&sv.raw) {
479 return Ok(d);
480 }
481 if sv.value_type == ScalarType::Number {
483 if let Ok(n) = sv.raw.parse::<i64>() {
484 if n < 0 {
485 return Err(ConfigError {
486 message: format!("negative duration at {}: {}", path, sv.raw),
487 path: path.to_string(),
488 });
489 }
490 return Ok(std::time::Duration::from_millis(n as u64));
491 }
492 if let Ok(f) = sv.raw.parse::<f64>() {
493 if f < 0.0 || !f.is_finite() {
494 return Err(ConfigError {
495 message: format!("invalid duration at {}: {}", path, sv.raw),
496 path: path.to_string(),
497 });
498 }
499 let secs = f / 1000.0;
500 if secs > u64::MAX as f64 {
501 return Err(ConfigError {
502 message: format!("duration too large at {}: {}", path, sv.raw),
503 path: path.to_string(),
504 });
505 }
506 return Ok(std::time::Duration::from_secs_f64(secs));
507 }
508 }
509 Err(ConfigError {
510 message: format!("invalid duration at {}: {}", path, sv.raw),
511 path: path.to_string(),
512 })
513 }
514 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
515 _ => Err(ConfigError {
516 message: format!("expected duration at {}", path),
517 path: path.to_string(),
518 }),
519 }
520 }
521
522 pub fn get_duration_option(&self, path: &str) -> Option<std::time::Duration> {
524 self.get_duration(path).ok()
525 }
526
527 pub fn get_bytes(&self, path: &str) -> Result<i64, ConfigError> {
538 let v = self.lookup_node(path).ok_or_else(|| ConfigError {
539 message: format!("path not found: {}", path),
540 path: path.to_string(),
541 })?;
542 match v {
543 HoconValue::Scalar(sv) => {
544 let n: i64 = if sv.value_type == ScalarType::Number {
545 sv.raw.parse::<i64>().map_err(|_| ConfigError {
548 message: format!("expected byte size at {}", path),
549 path: path.to_string(),
550 })?
551 } else {
552 parse_bytes(&sv.raw).ok_or_else(|| ConfigError {
554 message: format!("invalid byte size at {}: {}", path, sv.raw),
555 path: path.to_string(),
556 })?
557 };
558 if n < 0 {
562 return Err(ConfigError {
563 message: format!("negative byte size at {}: {}", path, sv.raw),
564 path: path.to_string(),
565 });
566 }
567 Ok(n)
568 }
569 HoconValue::Placeholder(_) => Err(not_resolved(path)),
570 _ => Err(ConfigError {
571 message: format!("expected byte size at {}", path),
572 path: path.to_string(),
573 }),
574 }
575 }
576
577 pub fn get_bytes_option(&self, path: &str) -> Option<i64> {
579 self.get_bytes(path).ok()
580 }
581
582 pub fn get_period(&self, path: &str) -> Result<Period, ConfigError> {
592 match self.lookup_node(path) {
593 None => Err(missing(path)),
594 Some(HoconValue::Scalar(sv)) => {
595 if let Some((y, mo, d)) = parse_period(&sv.raw) {
596 return Ok(Period::new(y, mo, d));
597 }
598 if sv.value_type == ScalarType::Number {
600 if let Ok(n) = sv.raw.parse::<i32>() {
601 return Ok(Period::new(0, 0, n));
602 }
603 }
604 Err(ConfigError {
605 message: format!("invalid period at {}: {}", path, sv.raw),
606 path: path.to_string(),
607 })
608 }
609 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
610 _ => Err(ConfigError {
611 message: format!("expected period at {}", path),
612 path: path.to_string(),
613 }),
614 }
615 }
616
617 pub fn get_period_option(&self, path: &str) -> Option<Period> {
619 self.get_period(path).ok()
620 }
621
622 pub fn has(&self, path: &str) -> bool {
624 self.lookup_node(path).is_some()
625 }
626
627 pub fn keys(&self) -> Vec<&str> {
629 self.root.keys().map(|s| s.as_str()).collect()
630 }
631
632 pub fn with_fallback(&self, fallback: &Config) -> Config {
654 let recv_obj = match &self.unresolved_tree {
655 Some(t) => t.clone(),
656 None => crate::resolver::hocon_map_to_res_obj(&self.root),
657 };
658 let fb_obj = match &fallback.unresolved_tree {
659 Some(t) => t.clone(),
660 None => crate::resolver::hocon_map_to_res_obj(&fallback.root),
661 };
662 let merged = crate::resolver::merge_unresolved(recv_obj, fb_obj);
663 Config::new_from_res_obj(
664 merged,
665 self.parse_base_dir.clone(),
666 self.origin_description.clone(),
667 )
668 }
669}
670
671fn split_config_path(path: &str) -> Vec<String> {
676 let mut segments = Vec::new();
677 let chars: Vec<char> = path.chars().collect();
678 let mut i = 0;
679 while i < chars.len() {
680 if chars[i] == '"' {
681 i += 1; let mut seg = String::new();
684 let mut closed = false;
685 while i < chars.len() {
686 if chars[i] == '\\' && i + 1 < chars.len() {
687 seg.push(chars[i + 1]);
688 i += 2;
689 continue;
690 }
691 if chars[i] == '"' {
692 closed = true;
693 i += 1;
694 break;
695 }
696 seg.push(chars[i]);
697 i += 1;
698 }
699 if !closed {
700 return vec![path.to_string()]; }
702 segments.push(seg);
703 if i < chars.len() && chars[i] == '.' {
705 i += 1;
706 }
707 } else {
708 let start = i;
711 while i < chars.len() && chars[i] != '.' && chars[i] != '"' {
712 i += 1;
713 }
714 segments.push(chars[start..i].iter().collect());
715 if i < chars.len() && chars[i] == '.' {
717 i += 1;
718 }
719 }
720 }
721 if path.ends_with('.') {
723 segments.push(String::new());
724 }
725 segments
726}
727
728fn lookup_in_map_by_segments<'a>(
729 map: &'a IndexMap<String, HoconValue>,
730 segments: &[String],
731) -> Option<&'a HoconValue> {
732 if segments.is_empty() {
733 return None;
734 }
735 let key = &segments[0];
736 let rest = &segments[1..];
737 let value = map.get(key)?;
738 if rest.is_empty() {
739 Some(value)
740 } else {
741 match value {
742 HoconValue::Object(inner) => lookup_in_map_by_segments(inner, rest),
743 _ => None,
744 }
745 }
746}
747
748#[cfg(feature = "serde")]
749impl Config {
750 pub fn deserialize<T: ::serde::de::DeserializeOwned>(
755 &self,
756 ) -> Result<T, crate::serde::DeserializeError> {
757 let value = HoconValue::Object(self.root.clone());
758 crate::serde::from_value(&value)
759 }
760
761 pub fn get_as<T: ::serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, ConfigError> {
773 let node = self.lookup_node(path).ok_or_else(|| missing(path))?;
774 if value_contains_placeholder(node) {
775 return Err(not_resolved(path));
776 }
777 crate::serde::from_value(node).map_err(|e| ConfigError {
778 message: e.message,
779 path: path.to_string(),
780 })
781 }
782}
783
784fn trim_hocon_ws(s: &str) -> &str {
789 s.trim_matches(is_hocon_whitespace)
790}
791
792fn is_integer_str(s: &str) -> bool {
797 let s = s.strip_prefix(['+', '-']).unwrap_or(s);
798 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
799}
800
801fn parse_duration(s: &str) -> Option<std::time::Duration> {
818 let s = trim_hocon_ws(s);
819 if s.is_empty() {
820 return None;
821 }
822
823 let num_end = s
825 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
826 .unwrap_or(s.len());
827 let num_str = s[..num_end].trim();
828 let unit_str = trim_hocon_ws(&s[num_end..]);
831
832 if num_str.is_empty() {
833 return None;
834 }
835
836 let nanos_per_unit: f64 = match unit_str {
837 "" | "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 1_000_000.0,
839 "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => 1.0,
840 "us" | "micro" | "micros" | "microsecond" | "microseconds" => 1_000.0,
841 "s" | "second" | "seconds" => 1_000_000_000.0,
842 "m" | "minute" | "minutes" => 60_000_000_000.0,
843 "h" | "hour" | "hours" => 3_600_000_000_000.0,
844 "d" | "day" | "days" => 86_400_000_000_000.0,
845 "w" | "week" | "weeks" => 604_800_000_000_000.0,
846 _ => return None,
847 };
848
849 if is_integer_str(num_str) {
851 let n_i128: i128 = num_str.parse().ok()?;
856 if n_i128 < 0 {
857 return None;
858 }
859 let n_u64: u64 = n_i128.try_into().ok()?;
860 let unit_u64 = nanos_per_unit as u64;
865 let nanos = n_u64.checked_mul(unit_u64)?;
866 return Some(std::time::Duration::from_nanos(nanos));
867 }
868
869 let f: f64 = num_str.parse().ok()?;
871 if f < 0.0 || !f.is_finite() {
872 return None;
873 }
874 let product = f * nanos_per_unit;
880 if !product.is_finite() || product >= 2f64.powi(64) {
881 return None;
882 }
883 Some(std::time::Duration::from_nanos(product as u64))
884}
885
886pub(crate) fn parse_period(s: &str) -> Option<(i32, i32, i32)> {
899 let s = trim_hocon_ws(s);
900 if s.is_empty() {
901 return None;
902 }
903
904 let num_end = s
906 .find(|c: char| !c.is_ascii_digit() && c != '-' && c != '+')
907 .unwrap_or(s.len());
908 let num_str = s[..num_end].trim();
909 let unit_str = trim_hocon_ws(&s[num_end..]);
910
911 if num_str.is_empty() {
912 return None;
913 }
914
915 if !is_integer_str(num_str) {
917 return None;
918 }
919
920 let n: i32 = num_str.parse().ok()?;
921
922 match unit_str {
925 "" | "d" | "day" | "days" => Some((0, 0, n)),
927 "w" | "week" | "weeks" => Some((0, 0, n.checked_mul(7)?)),
928 "m" | "mo" | "month" | "months" => Some((0, n, 0)),
929 "y" | "year" | "years" => Some((n, 0, 0)),
930 _ => None,
931 }
932}
933
934fn parse_bytes(s: &str) -> Option<i64> {
946 let s = trim_hocon_ws(s);
947 let num_end = s
948 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
949 .unwrap_or(s.len());
950 let num_str = s[..num_end].trim();
951 let unit_str = trim_hocon_ws(&s[num_end..]);
952
953 if num_str.is_empty() {
954 return None;
955 }
956
957 let multiplier: i64 = match unit_str {
968 "" | "B" | "byte" | "bytes" => 1,
969 "K" | "k" => 1_024,
971 "M" | "m" => 1_048_576,
972 "G" | "g" => 1_073_741_824,
973 "T" | "t" => 1_099_511_627_776,
974 "P" | "p" => 1_125_899_906_842_624,
975 "E" | "e" => 1_152_921_504_606_846_976,
976 "KB" | "kilobyte" | "kilobytes" => 1_000,
978 "KiB" | "Ki" | "kibibyte" | "kibibytes" => 1_024,
979 "MB" | "megabyte" | "megabytes" => 1_000_000,
980 "MiB" | "Mi" | "mebibyte" | "mebibytes" => 1_048_576,
981 "GB" | "gigabyte" | "gigabytes" => 1_000_000_000,
982 "GiB" | "Gi" | "gibibyte" | "gibibytes" => 1_073_741_824,
983 "TB" | "terabyte" | "terabytes" => 1_000_000_000_000,
984 "TiB" | "Ti" | "tebibyte" | "tebibytes" => 1_099_511_627_776,
985 _ => return None,
986 };
987
988 if is_integer_str(num_str) {
990 let n: i64 = num_str.parse().ok()?;
991 return n.checked_mul(multiplier);
992 }
993
994 let f: f64 = num_str.parse().ok()?;
996 if !f.is_finite() || f.abs() * multiplier as f64 >= 2f64.powi(63) {
1000 return None;
1001 }
1002 Some((f * multiplier as f64) as i64)
1003}
1004
1005fn missing(path: &str) -> ConfigError {
1006 ConfigError {
1007 message: "key not found".to_string(),
1008 path: path.to_string(),
1009 }
1010}
1011
1012fn type_mismatch(path: &str, expected: &str) -> ConfigError {
1013 ConfigError {
1014 message: format!("expected {}", expected),
1015 path: path.to_string(),
1016 }
1017}
1018
1019fn not_resolved(path: &str) -> ConfigError {
1020 ConfigError {
1021 message: "value is not resolved (call Config::resolve() before accessing values)"
1022 .to_string(),
1023 path: path.to_string(),
1024 }
1025}
1026
1027#[cfg(feature = "serde")]
1031fn value_contains_placeholder(value: &HoconValue) -> bool {
1032 match value {
1033 HoconValue::Placeholder(_) => true,
1034 HoconValue::Object(map) => map.values().any(value_contains_placeholder),
1035 HoconValue::Array(items) => items.iter().any(value_contains_placeholder),
1036 HoconValue::Scalar(_) => false,
1037 }
1038}
1039
1040fn filter_hocon_object_by_receiver(
1043 resolved: &mut IndexMap<String, HoconValue>,
1044 receiver_shape: &IndexMap<String, HoconValue>,
1045) {
1046 resolved.retain(|k, v| {
1047 if !receiver_shape.contains_key(k) {
1048 return false;
1049 }
1050 if let (HoconValue::Object(inner_res), Some(HoconValue::Object(inner_recv))) =
1051 (v, receiver_shape.get(k))
1052 {
1053 filter_hocon_object_by_receiver(inner_res, inner_recv);
1054 }
1055 true
1056 });
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062 use crate::value::{HoconValue, ScalarValue};
1063 use indexmap::IndexMap;
1064
1065 fn make_config(entries: Vec<(&str, HoconValue)>) -> Config {
1066 let mut map = IndexMap::new();
1067 for (k, v) in entries {
1068 map.insert(k.to_string(), v);
1069 }
1070 Config::new(map)
1071 }
1072
1073 fn sv(s: &str) -> HoconValue {
1074 HoconValue::Scalar(ScalarValue::string(s.into()))
1075 }
1076 fn iv(n: i64) -> HoconValue {
1077 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1078 }
1079 fn fv(n: f64) -> HoconValue {
1080 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1081 }
1082 fn bv(b: bool) -> HoconValue {
1083 HoconValue::Scalar(ScalarValue::boolean(b))
1084 }
1085
1086 #[test]
1087 fn get_returns_value_at_path() {
1088 let c = make_config(vec![("host", sv("localhost"))]);
1089 assert!(c.get("host").is_some());
1090 }
1091
1092 #[test]
1093 fn get_returns_none_for_missing() {
1094 let c = make_config(vec![]);
1095 assert!(c.get("missing").is_none());
1096 }
1097
1098 #[test]
1099 fn get_string_returns_string() {
1100 let c = make_config(vec![("host", sv("localhost"))]);
1101 assert_eq!(c.get_string("host").unwrap(), "localhost");
1102 }
1103
1104 #[test]
1105 fn get_string_coerces_int() {
1106 let c = make_config(vec![("port", iv(8080))]);
1107 assert_eq!(c.get_string("port").unwrap(), "8080");
1108 }
1109
1110 #[test]
1111 fn get_string_coerces_float() {
1112 let c = make_config(vec![("ratio", fv(2.72))]);
1113 let s = c.get_string("ratio").unwrap();
1115 let v: f64 = s.parse().unwrap();
1116 assert!((v - 2.72).abs() < 1e-10);
1117 }
1118
1119 #[test]
1120 fn get_string_coerces_bool() {
1121 let c = make_config(vec![("flag", bv(true))]);
1122 assert_eq!(c.get_string("flag").unwrap(), "true");
1123 }
1124
1125 #[test]
1126 fn get_string_error_on_null() {
1127 let c = make_config(vec![("v", HoconValue::Scalar(ScalarValue::null()))]);
1129 assert!(c.get_string("v").is_err());
1130 }
1131
1132 #[test]
1133 fn get_string_error_on_object() {
1134 let mut inner = IndexMap::new();
1135 inner.insert("x".into(), iv(1));
1136 let c = make_config(vec![("obj", HoconValue::Object(inner))]);
1137 assert!(c.get_string("obj").is_err());
1138 }
1139
1140 #[test]
1141 fn get_i64_returns_number() {
1142 let c = make_config(vec![("port", iv(8080))]);
1143 assert_eq!(c.get_i64("port").unwrap(), 8080);
1144 }
1145
1146 #[test]
1147 fn get_i64_coerces_numeric_string() {
1148 let c = make_config(vec![("port", sv("9999"))]);
1149 assert_eq!(c.get_i64("port").unwrap(), 9999);
1150 }
1151
1152 #[test]
1153 fn get_i64_error_on_non_numeric() {
1154 let c = make_config(vec![("host", sv("localhost"))]);
1155 assert!(c.get_i64("host").is_err());
1156 }
1157
1158 #[test]
1159 fn get_i64_error_on_overflow() {
1160 let c = make_config(vec![("big", sv("1e20"))]);
1162 assert!(c.get_i64("big").is_err());
1163 }
1164
1165 #[test]
1166 fn get_i64_error_on_i64_max_plus_one() {
1167 let c = make_config(vec![("big", sv("9223372036854775808"))]);
1169 assert!(c.get_i64("big").is_err());
1170 }
1171
1172 #[test]
1173 fn get_f64_returns_float() {
1174 let c = make_config(vec![("rate", fv(2.72))]);
1175 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1176 }
1177
1178 #[test]
1179 fn get_f64_coerces_numeric_string() {
1180 let c = make_config(vec![("rate", sv("2.72"))]);
1181 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1182 }
1183
1184 #[test]
1185 fn get_bool_returns_bool() {
1186 let c = make_config(vec![("debug", bv(true))]);
1187 assert!(c.get_bool("debug").unwrap());
1188 }
1189
1190 #[test]
1191 fn get_bool_coerces_string_true() {
1192 let c = make_config(vec![("debug", sv("true"))]);
1193 assert!(c.get_bool("debug").unwrap());
1194 }
1195
1196 #[test]
1197 fn get_bool_coerces_string_false() {
1198 let c = make_config(vec![("debug", sv("false"))]);
1199 assert!(!c.get_bool("debug").unwrap());
1200 }
1201
1202 #[test]
1203 fn get_bool_coerces_yes_no_on_off() {
1204 let c1 = make_config(vec![("v", sv("yes"))]);
1205 assert!(c1.get_bool("v").unwrap());
1206 let c2 = make_config(vec![("v", sv("no"))]);
1207 assert!(!c2.get_bool("v").unwrap());
1208 let c3 = make_config(vec![("v", sv("on"))]);
1209 assert!(c3.get_bool("v").unwrap());
1210 let c4 = make_config(vec![("v", sv("off"))]);
1211 assert!(!c4.get_bool("v").unwrap());
1212 }
1213
1214 #[test]
1215 fn get_bool_is_case_insensitive() {
1216 let c = make_config(vec![("v", sv("TRUE"))]);
1217 assert!(c.get_bool("v").unwrap());
1218 let c2 = make_config(vec![("v", sv("Off"))]);
1219 assert!(!c2.get_bool("v").unwrap());
1220 }
1221
1222 #[test]
1223 fn get_bool_error_on_non_boolean() {
1224 let c = make_config(vec![("v", sv("maybe"))]);
1225 assert!(c.get_bool("v").is_err());
1226 }
1227
1228 #[test]
1229 fn has_returns_true_for_existing() {
1230 let c = make_config(vec![("host", sv("localhost"))]);
1231 assert!(c.has("host"));
1232 }
1233
1234 #[test]
1235 fn has_returns_false_for_missing() {
1236 let c = make_config(vec![]);
1237 assert!(!c.has("missing"));
1238 }
1239
1240 #[test]
1241 fn keys_returns_in_order() {
1242 let c = make_config(vec![("b", iv(2)), ("a", iv(1))]);
1243 assert_eq!(c.keys(), vec!["b", "a"]);
1244 }
1245
1246 #[test]
1247 fn get_nested_dot_path() {
1248 let mut inner = IndexMap::new();
1249 inner.insert("host".into(), sv("localhost"));
1250 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1251 assert_eq!(c.get_string("server.host").unwrap(), "localhost");
1252 }
1253
1254 #[test]
1255 fn get_config_returns_sub_config() {
1256 let mut inner = IndexMap::new();
1257 inner.insert("host".into(), sv("localhost"));
1258 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1259 let sub = c.get_config("server").unwrap();
1260 assert_eq!(sub.get_string("host").unwrap(), "localhost");
1261 }
1262
1263 #[test]
1264 fn get_list_returns_array() {
1265 let items = vec![iv(1), iv(2), iv(3)];
1266 let c = make_config(vec![("list", HoconValue::Array(items))]);
1267 let list = c.get_list("list").unwrap();
1268 assert_eq!(list.len(), 3);
1269 }
1270
1271 #[test]
1272 fn with_fallback_receiver_wins() {
1273 let c1 = make_config(vec![("host", sv("prod"))]);
1274 let c2 = make_config(vec![("host", sv("dev")), ("port", iv(8080))]);
1275 let merged = c1.with_fallback(&c2);
1276 assert_eq!(merged.get_string("host").unwrap(), "prod");
1277 assert_eq!(merged.get_i64("port").unwrap(), 8080);
1278 }
1279
1280 #[test]
1281 fn option_variants_return_none_on_missing() {
1282 let c = make_config(vec![]);
1283 assert!(c.get_string_option("x").is_none());
1284 assert!(c.get_i64_option("x").is_none());
1285 assert!(c.get_f64_option("x").is_none());
1286 assert!(c.get_bool_option("x").is_none());
1287 }
1288
1289 #[test]
1290 fn get_duration_nanoseconds() {
1291 let c = make_config(vec![("t", sv("100 ns"))]);
1292 assert_eq!(
1293 c.get_duration("t").unwrap(),
1294 std::time::Duration::from_nanos(100)
1295 );
1296 }
1297
1298 #[test]
1299 fn get_duration_milliseconds() {
1300 let c = make_config(vec![("t", sv("500 ms"))]);
1301 assert_eq!(
1302 c.get_duration("t").unwrap(),
1303 std::time::Duration::from_millis(500)
1304 );
1305 }
1306
1307 #[test]
1308 fn get_duration_seconds() {
1309 let c = make_config(vec![("t", sv("30 seconds"))]);
1310 assert_eq!(
1311 c.get_duration("t").unwrap(),
1312 std::time::Duration::from_secs(30)
1313 );
1314 }
1315
1316 #[test]
1317 fn get_duration_minutes() {
1318 let c = make_config(vec![("t", sv("5 m"))]);
1319 assert_eq!(
1320 c.get_duration("t").unwrap(),
1321 std::time::Duration::from_secs(300)
1322 );
1323 }
1324
1325 #[test]
1326 fn get_duration_hours() {
1327 let c = make_config(vec![("t", sv("2 hours"))]);
1328 assert_eq!(
1329 c.get_duration("t").unwrap(),
1330 std::time::Duration::from_secs(7200)
1331 );
1332 }
1333
1334 #[test]
1335 fn get_duration_days() {
1336 let c = make_config(vec![("t", sv("1 d"))]);
1337 assert_eq!(
1338 c.get_duration("t").unwrap(),
1339 std::time::Duration::from_secs(86400)
1340 );
1341 }
1342
1343 #[test]
1344 fn get_duration_fractional() {
1345 let c = make_config(vec![("t", sv("1.5 hours"))]);
1346 assert_eq!(
1347 c.get_duration("t").unwrap(),
1348 std::time::Duration::from_secs(5400)
1349 );
1350 }
1351
1352 #[test]
1353 fn get_duration_no_space() {
1354 let c = make_config(vec![("t", sv("100ms"))]);
1355 assert_eq!(
1356 c.get_duration("t").unwrap(),
1357 std::time::Duration::from_millis(100)
1358 );
1359 }
1360
1361 #[test]
1362 fn get_duration_singular_unit() {
1363 let c = make_config(vec![("t", sv("1 second"))]);
1364 assert_eq!(
1365 c.get_duration("t").unwrap(),
1366 std::time::Duration::from_secs(1)
1367 );
1368 }
1369
1370 #[test]
1371 fn get_duration_error_invalid_unit() {
1372 let c = make_config(vec![("t", sv("100 foos"))]);
1373 assert!(c.get_duration("t").is_err());
1374 }
1375
1376 #[test]
1377 fn get_duration_option_missing() {
1378 let c = make_config(vec![]);
1379 assert!(c.get_duration_option("t").is_none());
1380 }
1381
1382 #[test]
1383 fn get_bytes_plain() {
1384 let c = make_config(vec![("s", sv("100 B"))]);
1385 assert_eq!(c.get_bytes("s").unwrap(), 100);
1386 }
1387
1388 #[test]
1389 fn get_bytes_kilobytes() {
1390 let c = make_config(vec![("s", sv("10 KB"))]);
1391 assert_eq!(c.get_bytes("s").unwrap(), 10_000);
1392 }
1393
1394 #[test]
1395 fn get_bytes_kibibytes() {
1396 let c = make_config(vec![("s", sv("1 KiB"))]);
1397 assert_eq!(c.get_bytes("s").unwrap(), 1_024);
1398 }
1399
1400 #[test]
1401 fn get_bytes_megabytes() {
1402 let c = make_config(vec![("s", sv("5 MB"))]);
1403 assert_eq!(c.get_bytes("s").unwrap(), 5_000_000);
1404 }
1405
1406 #[test]
1407 fn get_bytes_mebibytes() {
1408 let c = make_config(vec![("s", sv("1 MiB"))]);
1409 assert_eq!(c.get_bytes("s").unwrap(), 1_048_576);
1410 }
1411
1412 #[test]
1413 fn get_bytes_gigabytes() {
1414 let c = make_config(vec![("s", sv("2 GB"))]);
1415 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000_000);
1416 }
1417
1418 #[test]
1419 fn get_bytes_gibibytes() {
1420 let c = make_config(vec![("s", sv("1 GiB"))]);
1421 assert_eq!(c.get_bytes("s").unwrap(), 1_073_741_824);
1422 }
1423
1424 #[test]
1425 fn get_bytes_terabytes() {
1426 let c = make_config(vec![("s", sv("1 TB"))]);
1427 assert_eq!(c.get_bytes("s").unwrap(), 1_000_000_000_000);
1428 }
1429
1430 #[test]
1431 fn get_bytes_tebibytes() {
1432 let c = make_config(vec![("s", sv("1 TiB"))]);
1433 assert_eq!(c.get_bytes("s").unwrap(), 1_099_511_627_776);
1434 }
1435
1436 #[test]
1437 fn get_bytes_no_space() {
1438 let c = make_config(vec![("s", sv("512MB"))]);
1439 assert_eq!(c.get_bytes("s").unwrap(), 512_000_000);
1440 }
1441
1442 #[test]
1443 fn get_bytes_long_unit() {
1444 let c = make_config(vec![("s", sv("2 megabytes"))]);
1445 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000);
1446 }
1447
1448 #[test]
1449 fn get_bytes_error_invalid_unit() {
1450 let c = make_config(vec![("s", sv("100 XB"))]);
1451 assert!(c.get_bytes("s").is_err());
1452 }
1453
1454 #[test]
1455 fn get_bytes_option_missing() {
1456 let c = make_config(vec![]);
1457 assert!(c.get_bytes_option("s").is_none());
1458 }
1459
1460 #[test]
1461 fn get_bytes_fractional_rounds() {
1462 let c = make_config(vec![("s", sv("1.5 KiB"))]);
1464 assert_eq!(c.get_bytes("s").unwrap(), 1536);
1465 }
1466
1467 #[test]
1472 fn parse_duration_bare_integer_uses_ms_default() {
1473 assert_eq!(
1474 parse_duration("500"),
1475 Some(std::time::Duration::from_millis(500))
1476 );
1477 }
1478 #[test]
1479 fn parse_duration_leading_ws_bare() {
1480 assert_eq!(
1481 parse_duration(" 500"),
1482 Some(std::time::Duration::from_millis(500))
1483 );
1484 }
1485 #[test]
1486 fn parse_duration_trailing_ws_bare() {
1487 assert_eq!(
1488 parse_duration("500 "),
1489 Some(std::time::Duration::from_millis(500))
1490 );
1491 }
1492 #[test]
1493 fn parse_duration_both_ws_bare() {
1494 assert_eq!(
1495 parse_duration(" 500 "),
1496 Some(std::time::Duration::from_millis(500))
1497 );
1498 }
1499 #[test]
1500 fn parse_duration_fractional_bare_uses_nanos() {
1501 let d = parse_duration("500.5").unwrap();
1502 assert_eq!(d.as_nanos(), 500_500_000);
1503 }
1504 #[test]
1505 fn parse_duration_empty_is_none() {
1506 assert!(parse_duration("").is_none());
1507 }
1508 #[test]
1509 fn parse_duration_ws_only_is_none() {
1510 assert!(parse_duration(" ").is_none());
1511 }
1512 #[test]
1513 fn parse_duration_unit_only_is_none() {
1514 assert!(parse_duration("ms").is_none());
1515 }
1516
1517 #[test]
1526 fn parse_duration_integer_overflow_weeks_is_none() {
1527 assert!(parse_duration("9223372036854775807 weeks").is_none());
1529 }
1530
1531 #[test]
1532 fn parse_duration_integer_overflow_days_is_none() {
1533 assert!(parse_duration("9223372036854775807 days").is_none());
1535 }
1536
1537 #[test]
1538 fn parse_duration_integer_max_u64_nanos_succeeds() {
1539 let d = parse_duration("18446744073709551615ns").unwrap();
1541 assert_eq!(d.as_nanos(), u64::MAX as u128);
1542 }
1543
1544 #[test]
1545 fn parse_duration_fractional_overflow_is_none() {
1546 assert!(parse_duration("1e30 d").is_none());
1548 }
1549
1550 #[test]
1551 fn parse_duration_fractional_above_u64_max_is_none() {
1552 assert!(parse_duration("18446744073709551616ns").is_none());
1555 }
1556
1557 #[test]
1558 fn parse_duration_fractional_succeeds_below_boundary() {
1559 let d = parse_duration("1.5w").unwrap();
1561 assert_eq!(d.as_nanos(), 907_200_000_000_000u128);
1562 }
1563
1564 #[test]
1569 fn parse_bytes_leading_trailing_ws_bare() {
1570 assert_eq!(parse_bytes(" 1024 "), Some(1024));
1571 }
1572 #[test]
1573 fn parse_bytes_fractional_truncated() {
1574 assert_eq!(parse_bytes("1024.5"), Some(1024));
1575 }
1576 #[test]
1577 fn get_bytes_negative_accessor_rejects() {
1578 use std::collections::HashMap;
1579 let cfg = crate::parse_with_env(r#"b = "-1""#, &HashMap::new()).unwrap();
1580 assert!(
1581 cfg.get_bytes("b").is_err(),
1582 "ub04: negative byte size must error at accessor (string path)"
1583 );
1584 }
1585 #[test]
1586 fn get_bytes_negative_bare_number_rejects() {
1587 use std::collections::HashMap;
1588 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1590 assert!(
1591 cfg.get_bytes("b").is_err(),
1592 "ub04-bare: bare numeric -1 must error at accessor (both paths must hit guard)"
1593 );
1594 }
1595 #[test]
1596 fn get_bytes_option_negative_bare_number_is_none() {
1597 use std::collections::HashMap;
1598 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1599 assert!(
1600 cfg.get_bytes_option("b").is_none(),
1601 "ub04-bare-option: get_bytes_option must return None for bare numeric -1"
1602 );
1603 }
1604
1605 #[test]
1610 fn parse_period_bare_integer_uses_days_default() {
1611 assert_eq!(parse_period("7"), Some((0, 0, 7)));
1612 }
1613 #[test]
1614 fn parse_period_leading_trailing_ws() {
1615 assert_eq!(parse_period(" 7 "), Some((0, 0, 7)));
1616 }
1617 #[test]
1618 fn parse_period_fractional_rejected() {
1619 assert!(parse_period("7.5").is_none());
1620 }
1621 #[test]
1622 fn parse_period_negative_allowed() {
1623 assert_eq!(parse_period("-7"), Some((0, 0, -7)));
1624 }
1625 #[test]
1626 fn parse_period_weeks_unit() {
1627 assert_eq!(parse_period("7w"), Some((0, 0, 49)));
1628 }
1629 #[test]
1630 fn parse_period_months_unit() {
1631 assert_eq!(parse_period("3m"), Some((0, 3, 0)));
1632 }
1633 #[test]
1634 fn parse_period_years_unit() {
1635 assert_eq!(parse_period("2y"), Some((2, 0, 0)));
1636 }
1637 #[test]
1638 fn parse_period_days_explicit() {
1639 assert_eq!(parse_period("5d"), Some((0, 0, 5)));
1640 }
1641 #[test]
1642 fn parse_period_empty_is_none() {
1643 assert!(parse_period("").is_none());
1644 }
1645
1646 #[test]
1647 fn split_config_path_consecutive_dots_preserve_empty() {
1648 let segs = split_config_path("a..b");
1649 assert_eq!(segs, vec!["a", "", "b"]);
1650 }
1651
1652 #[test]
1653 fn split_config_path_trailing_dot_empty_segment() {
1654 let segs = split_config_path("a.b.");
1655 assert_eq!(segs, vec!["a", "b", ""]);
1656 }
1657
1658 #[test]
1659 fn split_config_path_quoted_escape() {
1660 let segs = split_config_path(r#""a\"b""#);
1662 assert_eq!(segs, vec!["a\"b"]);
1663 }
1664
1665 #[test]
1666 fn split_config_path_quoted_with_dot() {
1667 let segs = split_config_path(r#"server."web.api".port"#);
1668 assert_eq!(segs, vec!["server", "web.api", "port"]);
1669 }
1670}