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> {
470 match self.lookup_node(path) {
471 None => Err(missing(path)),
472 Some(HoconValue::Scalar(sv)) => {
473 if let Some(d) = parse_duration(&sv.raw) {
475 return Ok(d);
476 }
477 if sv.value_type == ScalarType::Number {
479 if let Ok(n) = sv.raw.parse::<i64>() {
480 if n < 0 {
481 return Err(ConfigError {
482 message: format!("negative duration at {}: {}", path, sv.raw),
483 path: path.to_string(),
484 });
485 }
486 return Ok(std::time::Duration::from_millis(n as u64));
487 }
488 if let Ok(f) = sv.raw.parse::<f64>() {
489 if f < 0.0 || !f.is_finite() {
490 return Err(ConfigError {
491 message: format!("invalid duration at {}: {}", path, sv.raw),
492 path: path.to_string(),
493 });
494 }
495 let secs = f / 1000.0;
496 if secs > u64::MAX as f64 {
497 return Err(ConfigError {
498 message: format!("duration too large at {}: {}", path, sv.raw),
499 path: path.to_string(),
500 });
501 }
502 return Ok(std::time::Duration::from_secs_f64(secs));
503 }
504 }
505 Err(ConfigError {
506 message: format!("invalid duration at {}: {}", path, sv.raw),
507 path: path.to_string(),
508 })
509 }
510 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
511 _ => Err(ConfigError {
512 message: format!("expected duration at {}", path),
513 path: path.to_string(),
514 }),
515 }
516 }
517
518 pub fn get_duration_option(&self, path: &str) -> Option<std::time::Duration> {
520 self.get_duration(path).ok()
521 }
522
523 pub fn get_bytes(&self, path: &str) -> Result<i64, ConfigError> {
534 let v = self.lookup_node(path).ok_or_else(|| ConfigError {
535 message: format!("path not found: {}", path),
536 path: path.to_string(),
537 })?;
538 match v {
539 HoconValue::Scalar(sv) => {
540 let n: i64 = if sv.value_type == ScalarType::Number {
541 sv.raw.parse::<i64>().map_err(|_| ConfigError {
544 message: format!("expected byte size at {}", path),
545 path: path.to_string(),
546 })?
547 } else {
548 parse_bytes(&sv.raw).ok_or_else(|| ConfigError {
550 message: format!("invalid byte size at {}: {}", path, sv.raw),
551 path: path.to_string(),
552 })?
553 };
554 if n < 0 {
558 return Err(ConfigError {
559 message: format!("negative byte size at {}: {}", path, sv.raw),
560 path: path.to_string(),
561 });
562 }
563 Ok(n)
564 }
565 HoconValue::Placeholder(_) => Err(not_resolved(path)),
566 _ => Err(ConfigError {
567 message: format!("expected byte size at {}", path),
568 path: path.to_string(),
569 }),
570 }
571 }
572
573 pub fn get_bytes_option(&self, path: &str) -> Option<i64> {
575 self.get_bytes(path).ok()
576 }
577
578 pub fn get_period(&self, path: &str) -> Result<Period, ConfigError> {
588 match self.lookup_node(path) {
589 None => Err(missing(path)),
590 Some(HoconValue::Scalar(sv)) => {
591 if let Some((y, mo, d)) = parse_period(&sv.raw) {
592 return Ok(Period::new(y, mo, d));
593 }
594 if sv.value_type == ScalarType::Number {
596 if let Ok(n) = sv.raw.parse::<i32>() {
597 return Ok(Period::new(0, 0, n));
598 }
599 }
600 Err(ConfigError {
601 message: format!("invalid period at {}: {}", path, sv.raw),
602 path: path.to_string(),
603 })
604 }
605 Some(HoconValue::Placeholder(_)) => Err(not_resolved(path)),
606 _ => Err(ConfigError {
607 message: format!("expected period at {}", path),
608 path: path.to_string(),
609 }),
610 }
611 }
612
613 pub fn get_period_option(&self, path: &str) -> Option<Period> {
615 self.get_period(path).ok()
616 }
617
618 pub fn has(&self, path: &str) -> bool {
620 self.lookup_node(path).is_some()
621 }
622
623 pub fn keys(&self) -> Vec<&str> {
625 self.root.keys().map(|s| s.as_str()).collect()
626 }
627
628 pub fn with_fallback(&self, fallback: &Config) -> Config {
650 let recv_obj = match &self.unresolved_tree {
651 Some(t) => t.clone(),
652 None => crate::resolver::hocon_map_to_res_obj(&self.root),
653 };
654 let fb_obj = match &fallback.unresolved_tree {
655 Some(t) => t.clone(),
656 None => crate::resolver::hocon_map_to_res_obj(&fallback.root),
657 };
658 let merged = crate::resolver::merge_unresolved(recv_obj, fb_obj);
659 Config::new_from_res_obj(
660 merged,
661 self.parse_base_dir.clone(),
662 self.origin_description.clone(),
663 )
664 }
665}
666
667fn split_config_path(path: &str) -> Vec<String> {
672 let mut segments = Vec::new();
673 let chars: Vec<char> = path.chars().collect();
674 let mut i = 0;
675 while i < chars.len() {
676 if chars[i] == '"' {
677 i += 1; let mut seg = String::new();
680 let mut closed = false;
681 while i < chars.len() {
682 if chars[i] == '\\' && i + 1 < chars.len() {
683 seg.push(chars[i + 1]);
684 i += 2;
685 continue;
686 }
687 if chars[i] == '"' {
688 closed = true;
689 i += 1;
690 break;
691 }
692 seg.push(chars[i]);
693 i += 1;
694 }
695 if !closed {
696 return vec![path.to_string()]; }
698 segments.push(seg);
699 if i < chars.len() && chars[i] == '.' {
701 i += 1;
702 }
703 } else {
704 let start = i;
707 while i < chars.len() && chars[i] != '.' && chars[i] != '"' {
708 i += 1;
709 }
710 segments.push(chars[start..i].iter().collect());
711 if i < chars.len() && chars[i] == '.' {
713 i += 1;
714 }
715 }
716 }
717 if path.ends_with('.') {
719 segments.push(String::new());
720 }
721 segments
722}
723
724fn lookup_in_map_by_segments<'a>(
725 map: &'a IndexMap<String, HoconValue>,
726 segments: &[String],
727) -> Option<&'a HoconValue> {
728 if segments.is_empty() {
729 return None;
730 }
731 let key = &segments[0];
732 let rest = &segments[1..];
733 let value = map.get(key)?;
734 if rest.is_empty() {
735 Some(value)
736 } else {
737 match value {
738 HoconValue::Object(inner) => lookup_in_map_by_segments(inner, rest),
739 _ => None,
740 }
741 }
742}
743
744#[cfg(feature = "serde")]
745impl Config {
746 pub fn deserialize<T: ::serde::de::DeserializeOwned>(
751 &self,
752 ) -> Result<T, crate::serde::DeserializeError> {
753 let value = HoconValue::Object(self.root.clone());
754 crate::serde::from_value(&value)
755 }
756
757 pub fn get_as<T: ::serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, ConfigError> {
769 let node = self.lookup_node(path).ok_or_else(|| missing(path))?;
770 if value_contains_placeholder(node) {
771 return Err(not_resolved(path));
772 }
773 crate::serde::from_value(node).map_err(|e| ConfigError {
774 message: e.message,
775 path: path.to_string(),
776 })
777 }
778}
779
780fn trim_hocon_ws(s: &str) -> &str {
785 s.trim_matches(is_hocon_whitespace)
786}
787
788fn is_integer_str(s: &str) -> bool {
793 let s = s.strip_prefix(['+', '-']).unwrap_or(s);
794 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
795}
796
797fn parse_duration(s: &str) -> Option<std::time::Duration> {
814 let s = trim_hocon_ws(s);
815 if s.is_empty() {
816 return None;
817 }
818
819 let num_end = s
821 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
822 .unwrap_or(s.len());
823 let num_str = s[..num_end].trim();
824 let unit_str = trim_hocon_ws(&s[num_end..]).to_lowercase();
826
827 if num_str.is_empty() {
828 return None;
829 }
830
831 let nanos_per_unit: f64 = match unit_str.as_str() {
832 "" | "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 1_000_000.0,
834 "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => 1.0,
835 "us" | "micro" | "micros" | "microsecond" | "microseconds" => 1_000.0,
836 "s" | "second" | "seconds" => 1_000_000_000.0,
837 "m" | "minute" | "minutes" => 60_000_000_000.0,
838 "h" | "hour" | "hours" => 3_600_000_000_000.0,
839 "d" | "day" | "days" => 86_400_000_000_000.0,
840 "w" | "week" | "weeks" => 604_800_000_000_000.0,
841 _ => return None,
842 };
843
844 if is_integer_str(num_str) {
846 let n_i128: i128 = num_str.parse().ok()?;
851 if n_i128 < 0 {
852 return None;
853 }
854 let n_u64: u64 = n_i128.try_into().ok()?;
855 let unit_u64 = nanos_per_unit as u64;
860 let nanos = n_u64.checked_mul(unit_u64)?;
861 return Some(std::time::Duration::from_nanos(nanos));
862 }
863
864 let f: f64 = num_str.parse().ok()?;
866 if f < 0.0 || !f.is_finite() {
867 return None;
868 }
869 let product = f * nanos_per_unit;
875 if !product.is_finite() || product >= 2f64.powi(64) {
876 return None;
877 }
878 Some(std::time::Duration::from_nanos(product as u64))
879}
880
881pub(crate) fn parse_period(s: &str) -> Option<(i32, i32, i32)> {
894 let s = trim_hocon_ws(s);
895 if s.is_empty() {
896 return None;
897 }
898
899 let num_end = s
901 .find(|c: char| !c.is_ascii_digit() && c != '-' && c != '+')
902 .unwrap_or(s.len());
903 let num_str = s[..num_end].trim();
904 let unit_str = trim_hocon_ws(&s[num_end..]);
905
906 if num_str.is_empty() {
907 return None;
908 }
909
910 if !is_integer_str(num_str) {
912 return None;
913 }
914
915 let n: i32 = num_str.parse().ok()?;
916
917 match unit_str {
920 "" | "d" | "day" | "days" => Some((0, 0, n)),
922 "w" | "week" | "weeks" => Some((0, 0, n.checked_mul(7)?)),
923 "m" | "mo" | "month" | "months" => Some((0, n, 0)),
924 "y" | "year" | "years" => Some((n, 0, 0)),
925 _ => None,
926 }
927}
928
929fn parse_bytes(s: &str) -> Option<i64> {
941 let s = trim_hocon_ws(s);
942 let num_end = s
943 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
944 .unwrap_or(s.len());
945 let num_str = s[..num_end].trim();
946 let unit_str = trim_hocon_ws(&s[num_end..]);
947
948 if num_str.is_empty() {
949 return None;
950 }
951
952 let multiplier: i64 = match unit_str {
963 "" | "B" | "byte" | "bytes" => 1,
964 "K" | "k" => 1_024,
966 "M" | "m" => 1_048_576,
967 "G" | "g" => 1_073_741_824,
968 "T" | "t" => 1_099_511_627_776,
969 "P" | "p" => 1_125_899_906_842_624,
970 "E" | "e" => 1_152_921_504_606_846_976,
971 "KB" | "kilobyte" | "kilobytes" => 1_000,
973 "KiB" | "Ki" | "kibibyte" | "kibibytes" => 1_024,
974 "MB" | "megabyte" | "megabytes" => 1_000_000,
975 "MiB" | "Mi" | "mebibyte" | "mebibytes" => 1_048_576,
976 "GB" | "gigabyte" | "gigabytes" => 1_000_000_000,
977 "GiB" | "Gi" | "gibibyte" | "gibibytes" => 1_073_741_824,
978 "TB" | "terabyte" | "terabytes" => 1_000_000_000_000,
979 "TiB" | "Ti" | "tebibyte" | "tebibytes" => 1_099_511_627_776,
980 _ => return None,
981 };
982
983 if is_integer_str(num_str) {
985 let n: i64 = num_str.parse().ok()?;
986 return n.checked_mul(multiplier);
987 }
988
989 let f: f64 = num_str.parse().ok()?;
991 if !f.is_finite() || f.abs() * multiplier as f64 >= 2f64.powi(63) {
995 return None;
996 }
997 Some((f * multiplier as f64) as i64)
998}
999
1000fn missing(path: &str) -> ConfigError {
1001 ConfigError {
1002 message: "key not found".to_string(),
1003 path: path.to_string(),
1004 }
1005}
1006
1007fn type_mismatch(path: &str, expected: &str) -> ConfigError {
1008 ConfigError {
1009 message: format!("expected {}", expected),
1010 path: path.to_string(),
1011 }
1012}
1013
1014fn not_resolved(path: &str) -> ConfigError {
1015 ConfigError {
1016 message: "value is not resolved (call Config::resolve() before accessing values)"
1017 .to_string(),
1018 path: path.to_string(),
1019 }
1020}
1021
1022#[cfg(feature = "serde")]
1026fn value_contains_placeholder(value: &HoconValue) -> bool {
1027 match value {
1028 HoconValue::Placeholder(_) => true,
1029 HoconValue::Object(map) => map.values().any(value_contains_placeholder),
1030 HoconValue::Array(items) => items.iter().any(value_contains_placeholder),
1031 HoconValue::Scalar(_) => false,
1032 }
1033}
1034
1035fn filter_hocon_object_by_receiver(
1038 resolved: &mut IndexMap<String, HoconValue>,
1039 receiver_shape: &IndexMap<String, HoconValue>,
1040) {
1041 resolved.retain(|k, v| {
1042 if !receiver_shape.contains_key(k) {
1043 return false;
1044 }
1045 if let (HoconValue::Object(inner_res), Some(HoconValue::Object(inner_recv))) =
1046 (v, receiver_shape.get(k))
1047 {
1048 filter_hocon_object_by_receiver(inner_res, inner_recv);
1049 }
1050 true
1051 });
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056 use super::*;
1057 use crate::value::{HoconValue, ScalarValue};
1058 use indexmap::IndexMap;
1059
1060 fn make_config(entries: Vec<(&str, HoconValue)>) -> Config {
1061 let mut map = IndexMap::new();
1062 for (k, v) in entries {
1063 map.insert(k.to_string(), v);
1064 }
1065 Config::new(map)
1066 }
1067
1068 fn sv(s: &str) -> HoconValue {
1069 HoconValue::Scalar(ScalarValue::string(s.into()))
1070 }
1071 fn iv(n: i64) -> HoconValue {
1072 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1073 }
1074 fn fv(n: f64) -> HoconValue {
1075 HoconValue::Scalar(ScalarValue::number(n.to_string()))
1076 }
1077 fn bv(b: bool) -> HoconValue {
1078 HoconValue::Scalar(ScalarValue::boolean(b))
1079 }
1080
1081 #[test]
1082 fn get_returns_value_at_path() {
1083 let c = make_config(vec![("host", sv("localhost"))]);
1084 assert!(c.get("host").is_some());
1085 }
1086
1087 #[test]
1088 fn get_returns_none_for_missing() {
1089 let c = make_config(vec![]);
1090 assert!(c.get("missing").is_none());
1091 }
1092
1093 #[test]
1094 fn get_string_returns_string() {
1095 let c = make_config(vec![("host", sv("localhost"))]);
1096 assert_eq!(c.get_string("host").unwrap(), "localhost");
1097 }
1098
1099 #[test]
1100 fn get_string_coerces_int() {
1101 let c = make_config(vec![("port", iv(8080))]);
1102 assert_eq!(c.get_string("port").unwrap(), "8080");
1103 }
1104
1105 #[test]
1106 fn get_string_coerces_float() {
1107 let c = make_config(vec![("ratio", fv(2.72))]);
1108 let s = c.get_string("ratio").unwrap();
1110 let v: f64 = s.parse().unwrap();
1111 assert!((v - 2.72).abs() < 1e-10);
1112 }
1113
1114 #[test]
1115 fn get_string_coerces_bool() {
1116 let c = make_config(vec![("flag", bv(true))]);
1117 assert_eq!(c.get_string("flag").unwrap(), "true");
1118 }
1119
1120 #[test]
1121 fn get_string_error_on_null() {
1122 let c = make_config(vec![("v", HoconValue::Scalar(ScalarValue::null()))]);
1124 assert!(c.get_string("v").is_err());
1125 }
1126
1127 #[test]
1128 fn get_string_error_on_object() {
1129 let mut inner = IndexMap::new();
1130 inner.insert("x".into(), iv(1));
1131 let c = make_config(vec![("obj", HoconValue::Object(inner))]);
1132 assert!(c.get_string("obj").is_err());
1133 }
1134
1135 #[test]
1136 fn get_i64_returns_number() {
1137 let c = make_config(vec![("port", iv(8080))]);
1138 assert_eq!(c.get_i64("port").unwrap(), 8080);
1139 }
1140
1141 #[test]
1142 fn get_i64_coerces_numeric_string() {
1143 let c = make_config(vec![("port", sv("9999"))]);
1144 assert_eq!(c.get_i64("port").unwrap(), 9999);
1145 }
1146
1147 #[test]
1148 fn get_i64_error_on_non_numeric() {
1149 let c = make_config(vec![("host", sv("localhost"))]);
1150 assert!(c.get_i64("host").is_err());
1151 }
1152
1153 #[test]
1154 fn get_i64_error_on_overflow() {
1155 let c = make_config(vec![("big", sv("1e20"))]);
1157 assert!(c.get_i64("big").is_err());
1158 }
1159
1160 #[test]
1161 fn get_i64_error_on_i64_max_plus_one() {
1162 let c = make_config(vec![("big", sv("9223372036854775808"))]);
1164 assert!(c.get_i64("big").is_err());
1165 }
1166
1167 #[test]
1168 fn get_f64_returns_float() {
1169 let c = make_config(vec![("rate", fv(2.72))]);
1170 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1171 }
1172
1173 #[test]
1174 fn get_f64_coerces_numeric_string() {
1175 let c = make_config(vec![("rate", sv("2.72"))]);
1176 assert!((c.get_f64("rate").unwrap() - 2.72).abs() < f64::EPSILON);
1177 }
1178
1179 #[test]
1180 fn get_bool_returns_bool() {
1181 let c = make_config(vec![("debug", bv(true))]);
1182 assert!(c.get_bool("debug").unwrap());
1183 }
1184
1185 #[test]
1186 fn get_bool_coerces_string_true() {
1187 let c = make_config(vec![("debug", sv("true"))]);
1188 assert!(c.get_bool("debug").unwrap());
1189 }
1190
1191 #[test]
1192 fn get_bool_coerces_string_false() {
1193 let c = make_config(vec![("debug", sv("false"))]);
1194 assert!(!c.get_bool("debug").unwrap());
1195 }
1196
1197 #[test]
1198 fn get_bool_coerces_yes_no_on_off() {
1199 let c1 = make_config(vec![("v", sv("yes"))]);
1200 assert!(c1.get_bool("v").unwrap());
1201 let c2 = make_config(vec![("v", sv("no"))]);
1202 assert!(!c2.get_bool("v").unwrap());
1203 let c3 = make_config(vec![("v", sv("on"))]);
1204 assert!(c3.get_bool("v").unwrap());
1205 let c4 = make_config(vec![("v", sv("off"))]);
1206 assert!(!c4.get_bool("v").unwrap());
1207 }
1208
1209 #[test]
1210 fn get_bool_is_case_insensitive() {
1211 let c = make_config(vec![("v", sv("TRUE"))]);
1212 assert!(c.get_bool("v").unwrap());
1213 let c2 = make_config(vec![("v", sv("Off"))]);
1214 assert!(!c2.get_bool("v").unwrap());
1215 }
1216
1217 #[test]
1218 fn get_bool_error_on_non_boolean() {
1219 let c = make_config(vec![("v", sv("maybe"))]);
1220 assert!(c.get_bool("v").is_err());
1221 }
1222
1223 #[test]
1224 fn has_returns_true_for_existing() {
1225 let c = make_config(vec![("host", sv("localhost"))]);
1226 assert!(c.has("host"));
1227 }
1228
1229 #[test]
1230 fn has_returns_false_for_missing() {
1231 let c = make_config(vec![]);
1232 assert!(!c.has("missing"));
1233 }
1234
1235 #[test]
1236 fn keys_returns_in_order() {
1237 let c = make_config(vec![("b", iv(2)), ("a", iv(1))]);
1238 assert_eq!(c.keys(), vec!["b", "a"]);
1239 }
1240
1241 #[test]
1242 fn get_nested_dot_path() {
1243 let mut inner = IndexMap::new();
1244 inner.insert("host".into(), sv("localhost"));
1245 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1246 assert_eq!(c.get_string("server.host").unwrap(), "localhost");
1247 }
1248
1249 #[test]
1250 fn get_config_returns_sub_config() {
1251 let mut inner = IndexMap::new();
1252 inner.insert("host".into(), sv("localhost"));
1253 let c = make_config(vec![("server", HoconValue::Object(inner))]);
1254 let sub = c.get_config("server").unwrap();
1255 assert_eq!(sub.get_string("host").unwrap(), "localhost");
1256 }
1257
1258 #[test]
1259 fn get_list_returns_array() {
1260 let items = vec![iv(1), iv(2), iv(3)];
1261 let c = make_config(vec![("list", HoconValue::Array(items))]);
1262 let list = c.get_list("list").unwrap();
1263 assert_eq!(list.len(), 3);
1264 }
1265
1266 #[test]
1267 fn with_fallback_receiver_wins() {
1268 let c1 = make_config(vec![("host", sv("prod"))]);
1269 let c2 = make_config(vec![("host", sv("dev")), ("port", iv(8080))]);
1270 let merged = c1.with_fallback(&c2);
1271 assert_eq!(merged.get_string("host").unwrap(), "prod");
1272 assert_eq!(merged.get_i64("port").unwrap(), 8080);
1273 }
1274
1275 #[test]
1276 fn option_variants_return_none_on_missing() {
1277 let c = make_config(vec![]);
1278 assert!(c.get_string_option("x").is_none());
1279 assert!(c.get_i64_option("x").is_none());
1280 assert!(c.get_f64_option("x").is_none());
1281 assert!(c.get_bool_option("x").is_none());
1282 }
1283
1284 #[test]
1285 fn get_duration_nanoseconds() {
1286 let c = make_config(vec![("t", sv("100 ns"))]);
1287 assert_eq!(
1288 c.get_duration("t").unwrap(),
1289 std::time::Duration::from_nanos(100)
1290 );
1291 }
1292
1293 #[test]
1294 fn get_duration_milliseconds() {
1295 let c = make_config(vec![("t", sv("500 ms"))]);
1296 assert_eq!(
1297 c.get_duration("t").unwrap(),
1298 std::time::Duration::from_millis(500)
1299 );
1300 }
1301
1302 #[test]
1303 fn get_duration_seconds() {
1304 let c = make_config(vec![("t", sv("30 seconds"))]);
1305 assert_eq!(
1306 c.get_duration("t").unwrap(),
1307 std::time::Duration::from_secs(30)
1308 );
1309 }
1310
1311 #[test]
1312 fn get_duration_minutes() {
1313 let c = make_config(vec![("t", sv("5 m"))]);
1314 assert_eq!(
1315 c.get_duration("t").unwrap(),
1316 std::time::Duration::from_secs(300)
1317 );
1318 }
1319
1320 #[test]
1321 fn get_duration_hours() {
1322 let c = make_config(vec![("t", sv("2 hours"))]);
1323 assert_eq!(
1324 c.get_duration("t").unwrap(),
1325 std::time::Duration::from_secs(7200)
1326 );
1327 }
1328
1329 #[test]
1330 fn get_duration_days() {
1331 let c = make_config(vec![("t", sv("1 d"))]);
1332 assert_eq!(
1333 c.get_duration("t").unwrap(),
1334 std::time::Duration::from_secs(86400)
1335 );
1336 }
1337
1338 #[test]
1339 fn get_duration_fractional() {
1340 let c = make_config(vec![("t", sv("1.5 hours"))]);
1341 assert_eq!(
1342 c.get_duration("t").unwrap(),
1343 std::time::Duration::from_secs(5400)
1344 );
1345 }
1346
1347 #[test]
1348 fn get_duration_no_space() {
1349 let c = make_config(vec![("t", sv("100ms"))]);
1350 assert_eq!(
1351 c.get_duration("t").unwrap(),
1352 std::time::Duration::from_millis(100)
1353 );
1354 }
1355
1356 #[test]
1357 fn get_duration_singular_unit() {
1358 let c = make_config(vec![("t", sv("1 second"))]);
1359 assert_eq!(
1360 c.get_duration("t").unwrap(),
1361 std::time::Duration::from_secs(1)
1362 );
1363 }
1364
1365 #[test]
1366 fn get_duration_error_invalid_unit() {
1367 let c = make_config(vec![("t", sv("100 foos"))]);
1368 assert!(c.get_duration("t").is_err());
1369 }
1370
1371 #[test]
1372 fn get_duration_option_missing() {
1373 let c = make_config(vec![]);
1374 assert!(c.get_duration_option("t").is_none());
1375 }
1376
1377 #[test]
1378 fn get_bytes_plain() {
1379 let c = make_config(vec![("s", sv("100 B"))]);
1380 assert_eq!(c.get_bytes("s").unwrap(), 100);
1381 }
1382
1383 #[test]
1384 fn get_bytes_kilobytes() {
1385 let c = make_config(vec![("s", sv("10 KB"))]);
1386 assert_eq!(c.get_bytes("s").unwrap(), 10_000);
1387 }
1388
1389 #[test]
1390 fn get_bytes_kibibytes() {
1391 let c = make_config(vec![("s", sv("1 KiB"))]);
1392 assert_eq!(c.get_bytes("s").unwrap(), 1_024);
1393 }
1394
1395 #[test]
1396 fn get_bytes_megabytes() {
1397 let c = make_config(vec![("s", sv("5 MB"))]);
1398 assert_eq!(c.get_bytes("s").unwrap(), 5_000_000);
1399 }
1400
1401 #[test]
1402 fn get_bytes_mebibytes() {
1403 let c = make_config(vec![("s", sv("1 MiB"))]);
1404 assert_eq!(c.get_bytes("s").unwrap(), 1_048_576);
1405 }
1406
1407 #[test]
1408 fn get_bytes_gigabytes() {
1409 let c = make_config(vec![("s", sv("2 GB"))]);
1410 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000_000);
1411 }
1412
1413 #[test]
1414 fn get_bytes_gibibytes() {
1415 let c = make_config(vec![("s", sv("1 GiB"))]);
1416 assert_eq!(c.get_bytes("s").unwrap(), 1_073_741_824);
1417 }
1418
1419 #[test]
1420 fn get_bytes_terabytes() {
1421 let c = make_config(vec![("s", sv("1 TB"))]);
1422 assert_eq!(c.get_bytes("s").unwrap(), 1_000_000_000_000);
1423 }
1424
1425 #[test]
1426 fn get_bytes_tebibytes() {
1427 let c = make_config(vec![("s", sv("1 TiB"))]);
1428 assert_eq!(c.get_bytes("s").unwrap(), 1_099_511_627_776);
1429 }
1430
1431 #[test]
1432 fn get_bytes_no_space() {
1433 let c = make_config(vec![("s", sv("512MB"))]);
1434 assert_eq!(c.get_bytes("s").unwrap(), 512_000_000);
1435 }
1436
1437 #[test]
1438 fn get_bytes_long_unit() {
1439 let c = make_config(vec![("s", sv("2 megabytes"))]);
1440 assert_eq!(c.get_bytes("s").unwrap(), 2_000_000);
1441 }
1442
1443 #[test]
1444 fn get_bytes_error_invalid_unit() {
1445 let c = make_config(vec![("s", sv("100 XB"))]);
1446 assert!(c.get_bytes("s").is_err());
1447 }
1448
1449 #[test]
1450 fn get_bytes_option_missing() {
1451 let c = make_config(vec![]);
1452 assert!(c.get_bytes_option("s").is_none());
1453 }
1454
1455 #[test]
1456 fn get_bytes_fractional_rounds() {
1457 let c = make_config(vec![("s", sv("1.5 KiB"))]);
1459 assert_eq!(c.get_bytes("s").unwrap(), 1536);
1460 }
1461
1462 #[test]
1467 fn parse_duration_bare_integer_uses_ms_default() {
1468 assert_eq!(
1469 parse_duration("500"),
1470 Some(std::time::Duration::from_millis(500))
1471 );
1472 }
1473 #[test]
1474 fn parse_duration_leading_ws_bare() {
1475 assert_eq!(
1476 parse_duration(" 500"),
1477 Some(std::time::Duration::from_millis(500))
1478 );
1479 }
1480 #[test]
1481 fn parse_duration_trailing_ws_bare() {
1482 assert_eq!(
1483 parse_duration("500 "),
1484 Some(std::time::Duration::from_millis(500))
1485 );
1486 }
1487 #[test]
1488 fn parse_duration_both_ws_bare() {
1489 assert_eq!(
1490 parse_duration(" 500 "),
1491 Some(std::time::Duration::from_millis(500))
1492 );
1493 }
1494 #[test]
1495 fn parse_duration_fractional_bare_uses_nanos() {
1496 let d = parse_duration("500.5").unwrap();
1497 assert_eq!(d.as_nanos(), 500_500_000);
1498 }
1499 #[test]
1500 fn parse_duration_empty_is_none() {
1501 assert!(parse_duration("").is_none());
1502 }
1503 #[test]
1504 fn parse_duration_ws_only_is_none() {
1505 assert!(parse_duration(" ").is_none());
1506 }
1507 #[test]
1508 fn parse_duration_unit_only_is_none() {
1509 assert!(parse_duration("ms").is_none());
1510 }
1511
1512 #[test]
1521 fn parse_duration_integer_overflow_weeks_is_none() {
1522 assert!(parse_duration("9223372036854775807 weeks").is_none());
1524 }
1525
1526 #[test]
1527 fn parse_duration_integer_overflow_days_is_none() {
1528 assert!(parse_duration("9223372036854775807 days").is_none());
1530 }
1531
1532 #[test]
1533 fn parse_duration_integer_max_u64_nanos_succeeds() {
1534 let d = parse_duration("18446744073709551615ns").unwrap();
1536 assert_eq!(d.as_nanos(), u64::MAX as u128);
1537 }
1538
1539 #[test]
1540 fn parse_duration_fractional_overflow_is_none() {
1541 assert!(parse_duration("1e30 d").is_none());
1543 }
1544
1545 #[test]
1546 fn parse_duration_fractional_above_u64_max_is_none() {
1547 assert!(parse_duration("18446744073709551616ns").is_none());
1550 }
1551
1552 #[test]
1553 fn parse_duration_fractional_succeeds_below_boundary() {
1554 let d = parse_duration("1.5w").unwrap();
1556 assert_eq!(d.as_nanos(), 907_200_000_000_000u128);
1557 }
1558
1559 #[test]
1564 fn parse_bytes_leading_trailing_ws_bare() {
1565 assert_eq!(parse_bytes(" 1024 "), Some(1024));
1566 }
1567 #[test]
1568 fn parse_bytes_fractional_truncated() {
1569 assert_eq!(parse_bytes("1024.5"), Some(1024));
1570 }
1571 #[test]
1572 fn get_bytes_negative_accessor_rejects() {
1573 use std::collections::HashMap;
1574 let cfg = crate::parse_with_env(r#"b = "-1""#, &HashMap::new()).unwrap();
1575 assert!(
1576 cfg.get_bytes("b").is_err(),
1577 "ub04: negative byte size must error at accessor (string path)"
1578 );
1579 }
1580 #[test]
1581 fn get_bytes_negative_bare_number_rejects() {
1582 use std::collections::HashMap;
1583 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1585 assert!(
1586 cfg.get_bytes("b").is_err(),
1587 "ub04-bare: bare numeric -1 must error at accessor (both paths must hit guard)"
1588 );
1589 }
1590 #[test]
1591 fn get_bytes_option_negative_bare_number_is_none() {
1592 use std::collections::HashMap;
1593 let cfg = crate::parse_with_env(r#"b = -1"#, &HashMap::new()).unwrap();
1594 assert!(
1595 cfg.get_bytes_option("b").is_none(),
1596 "ub04-bare-option: get_bytes_option must return None for bare numeric -1"
1597 );
1598 }
1599
1600 #[test]
1605 fn parse_period_bare_integer_uses_days_default() {
1606 assert_eq!(parse_period("7"), Some((0, 0, 7)));
1607 }
1608 #[test]
1609 fn parse_period_leading_trailing_ws() {
1610 assert_eq!(parse_period(" 7 "), Some((0, 0, 7)));
1611 }
1612 #[test]
1613 fn parse_period_fractional_rejected() {
1614 assert!(parse_period("7.5").is_none());
1615 }
1616 #[test]
1617 fn parse_period_negative_allowed() {
1618 assert_eq!(parse_period("-7"), Some((0, 0, -7)));
1619 }
1620 #[test]
1621 fn parse_period_weeks_unit() {
1622 assert_eq!(parse_period("7w"), Some((0, 0, 49)));
1623 }
1624 #[test]
1625 fn parse_period_months_unit() {
1626 assert_eq!(parse_period("3m"), Some((0, 3, 0)));
1627 }
1628 #[test]
1629 fn parse_period_years_unit() {
1630 assert_eq!(parse_period("2y"), Some((2, 0, 0)));
1631 }
1632 #[test]
1633 fn parse_period_days_explicit() {
1634 assert_eq!(parse_period("5d"), Some((0, 0, 5)));
1635 }
1636 #[test]
1637 fn parse_period_empty_is_none() {
1638 assert!(parse_period("").is_none());
1639 }
1640
1641 #[test]
1642 fn split_config_path_consecutive_dots_preserve_empty() {
1643 let segs = split_config_path("a..b");
1644 assert_eq!(segs, vec!["a", "", "b"]);
1645 }
1646
1647 #[test]
1648 fn split_config_path_trailing_dot_empty_segment() {
1649 let segs = split_config_path("a.b.");
1650 assert_eq!(segs, vec!["a", "b", ""]);
1651 }
1652
1653 #[test]
1654 fn split_config_path_quoted_escape() {
1655 let segs = split_config_path(r#""a\"b""#);
1657 assert_eq!(segs, vec!["a\"b"]);
1658 }
1659
1660 #[test]
1661 fn split_config_path_quoted_with_dot() {
1662 let segs = split_config_path(r#"server."web.api".port"#);
1663 assert_eq!(segs, vec!["server", "web.api", "port"]);
1664 }
1665}