use crate::lexer::Segment;
use crate::value::HoconValue;
use indexmap::IndexMap;
use super::types::{ResObj, ResolverValue};
pub(crate) fn segments_text_equal(a: &[Segment], b: &[Segment]) -> bool {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.text == y.text)
}
pub(crate) fn lookup_path<'a>(root: &'a ResObj, segments: &[Segment]) -> Option<&'a ResolverValue> {
if segments.is_empty() {
return None;
}
let head = &segments[0].text;
let tail = &segments[1..];
let val = root.fields.get(head.as_str())?;
if tail.is_empty() {
return Some(val);
}
if let ResolverValue::Obj(inner) = val {
return lookup_path(inner, tail);
}
None
}
pub(crate) fn deep_merge_hocon_objects(
base: IndexMap<String, HoconValue>,
overlay: IndexMap<String, HoconValue>,
) -> HoconValue {
let mut merged = base;
for (k, v) in overlay {
let both_objects = matches!(merged.get(&k), Some(HoconValue::Object(_)))
&& matches!(&v, HoconValue::Object(_));
if both_objects {
let existing_fields = match merged.get_mut(&k).expect("just checked Some via matches!")
{
HoconValue::Object(f) => std::mem::take(f),
_ => unreachable!("just matched HoconValue::Object via matches!"),
};
let new_fields = match v {
HoconValue::Object(f) => f,
_ => unreachable!("just matched HoconValue::Object via matches!"),
};
merged.insert(k, deep_merge_hocon_objects(existing_fields, new_fields));
} else {
merged.insert(k, v);
}
}
HoconValue::Object(merged)
}
fn resolved_obj_to_res_obj(fields: &IndexMap<String, HoconValue>) -> ResObj {
let mut obj = ResObj::new();
for (k, v) in fields {
obj.fields
.insert(k.clone(), ResolverValue::Resolved(v.clone()));
}
obj
}
fn as_res_obj(val: &ResolverValue) -> Option<ResObj> {
match val {
ResolverValue::Obj(o) => Some(o.clone()),
ResolverValue::Resolved(HoconValue::Object(fields)) => {
Some(resolved_obj_to_res_obj(fields))
}
_ => None,
}
}
pub(crate) fn deep_merge_res_obj_into(dst: &mut ResObj, src: ResObj, path_prefix: &[String]) {
let ResObj {
fields: src_fields,
prior_values: src_priors,
reset_keys: src_reset_keys,
} = src;
for (k, src_val) in src_fields {
let dst_is_obj = dst.fields.get(&k).and_then(as_res_obj);
let src_obj = as_res_obj(&src_val);
let mut child_prefix = path_prefix.to_vec();
child_prefix.push(k.clone());
let full_key = string_segments_to_key(child_prefix.iter().map(String::as_str));
if let (Some(mut dst_obj), Some(src_obj)) = (dst_is_obj, src_obj) {
if let Some(old) = dst.fields.get(&k) {
let prior_existing = dst.prior_values.get(&k).cloned();
if let Some(prior) = super::fold_self_ref::fold_or_skip_prior(
old,
&full_key,
prior_existing.as_ref(),
) {
dst.prior_values.insert(k.clone(), prior);
}
}
deep_merge_res_obj_into(&mut dst_obj, src_obj, &child_prefix);
dst.fields.insert(k, ResolverValue::Obj(dst_obj));
continue;
}
if dst.fields.contains_key(&k) {
if src_reset_keys.contains(&k) {
dst.prior_values.shift_remove(&k);
} else {
let dst_old = dst.fields.get(&k).cloned().unwrap();
let dst_prior = dst.prior_values.get(&k).cloned();
if let Some(dst_folded) = super::fold_self_ref::fold_or_skip_prior(
&dst_old,
&full_key,
dst_prior.as_ref(),
) {
if let Some(src_prior) = src_priors.get(&k) {
dst.prior_values.insert(
k.clone(),
super::fold_self_ref::fold_known_absent_self_ref(
src_prior,
&full_key,
&dst_folded,
),
);
} else {
dst.prior_values.insert(k.clone(), dst_folded);
}
}
}
}
dst.fields.insert(k, src_val);
}
for (k, src_prior) in src_priors {
if !dst.prior_values.contains_key(&k) {
dst.prior_values.insert(k, src_prior);
}
}
dst.reset_keys.extend(src_reset_keys);
}
pub(crate) fn relativize_subst_paths(val: &mut ResolverValue, prefix_segments: &[String]) {
match val {
ResolverValue::Subst(s) => {
let prefix: Vec<Segment> = prefix_segments
.iter()
.map(|text| Segment {
text: text.clone(),
line: s.line,
col: s.col,
})
.collect();
let mut new_segments = Vec::with_capacity(prefix.len() + s.segments.len());
new_segments.extend_from_slice(&prefix);
new_segments.extend_from_slice(&s.segments);
s.segments = new_segments;
s.prefix_len += prefix_segments.len();
}
ResolverValue::Concat(c) => {
for node in &mut c.nodes {
relativize_subst_paths(node, prefix_segments);
}
}
ResolverValue::Obj(o) => {
relativize_res_obj(o, prefix_segments);
}
ResolverValue::UnresolvedArray(items) => {
for item in items {
relativize_subst_paths(item, prefix_segments);
}
}
ResolverValue::Resolved(_) => {}
}
}
pub(crate) fn relativize_res_obj(obj: &mut ResObj, prefix_segments: &[String]) {
for val in obj.fields.values_mut() {
relativize_subst_paths(val, prefix_segments);
}
for val in obj.prior_values.values_mut() {
relativize_subst_paths(val, prefix_segments);
}
}
pub(crate) fn segments_to_key(segments: &[Segment]) -> String {
string_segments_to_key(segments.iter().map(|s| s.text.as_str()))
}
pub(crate) fn string_segments_to_key<'a, I>(segments: I) -> String
where
I: IntoIterator<Item = &'a str>,
{
segments
.into_iter()
.map(|s| {
if s.is_empty()
|| s.contains('.')
|| s.contains('"')
|| s.contains('\\')
|| s != s.trim()
|| s.contains(' ')
|| s.contains('\t')
|| s.contains('[')
|| s.contains(']')
{
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
} else {
s.to_string()
}
})
.collect::<Vec<_>>()
.join(".")
}
#[cfg(test)]
mod tests {
use super::*;
fn seg(text: &str) -> Segment {
Segment {
text: text.to_string(),
line: 1,
col: 1,
}
}
#[test]
fn segments_to_key_simple() {
assert_eq!(segments_to_key(&[seg("a"), seg("b"), seg("c")]), "a.b.c");
}
#[test]
fn segments_to_key_quoted_dot() {
assert_eq!(segments_to_key(&[seg("a.b"), seg("c")]), r#""a.b".c"#);
}
#[test]
fn segments_to_key_empty_string() {
assert_eq!(segments_to_key(&[seg(""), seg("foo")]), r#""".foo"#);
}
#[test]
fn segments_to_key_escaped_quotes() {
assert_eq!(segments_to_key(&[seg("a\"b"), seg("c")]), r#""a\"b".c"#);
}
#[test]
fn segments_to_key_escaped_backslash() {
assert_eq!(segments_to_key(&[seg("a\\b"), seg("c")]), r#""a\\b".c"#);
}
#[test]
fn segments_to_key_quotes_whitespace() {
assert_eq!(segments_to_key(&[seg(" a "), seg("b")]), r#"" a ".b"#);
}
fn scalar(s: &str) -> HoconValue {
HoconValue::Scalar(crate::value::ScalarValue::string(s.to_string()))
}
fn obj(pairs: &[(&str, HoconValue)]) -> HoconValue {
let mut m = IndexMap::new();
for (k, v) in pairs {
m.insert((*k).to_string(), v.clone());
}
HoconValue::Object(m)
}
fn as_obj(v: HoconValue) -> IndexMap<String, HoconValue> {
if let HoconValue::Object(m) = v {
m
} else {
panic!("expected Object, got {:?}", v)
}
}
#[test]
fn deep_merge_overlay_wins_on_scalar() {
let base = as_obj(obj(&[("a", scalar("base"))]));
let overlay = as_obj(obj(&[("a", scalar("overlay"))]));
let merged = as_obj(deep_merge_hocon_objects(base, overlay));
assert_eq!(merged.get("a"), Some(&scalar("overlay")));
}
#[test]
fn deep_merge_recurses_when_both_sides_are_objects() {
let base = as_obj(obj(&[(
"a",
obj(&[("x", scalar("from-base")), ("y", scalar("base-only"))]),
)]));
let overlay = as_obj(obj(&[(
"a",
obj(&[("x", scalar("from-overlay")), ("z", scalar("overlay-only"))]),
)]));
let merged = as_obj(deep_merge_hocon_objects(base, overlay));
let a = as_obj(merged.get("a").unwrap().clone());
assert_eq!(a.get("x"), Some(&scalar("from-overlay")));
assert_eq!(a.get("y"), Some(&scalar("base-only")));
assert_eq!(a.get("z"), Some(&scalar("overlay-only")));
}
#[test]
fn deep_merge_preserves_key_position_for_existing_keys() {
let base = as_obj(obj(&[
("a", obj(&[("x", scalar("1"))])),
("b", scalar("2")),
]));
let overlay = as_obj(obj(&[("a", obj(&[("y", scalar("3"))]))]));
let merged = as_obj(deep_merge_hocon_objects(base, overlay));
let keys: Vec<&String> = merged.keys().collect();
assert_eq!(keys, vec!["a", "b"]);
}
#[test]
fn deep_merge_nonobject_then_object_overlays() {
let base = as_obj(obj(&[("a", scalar("scalar-base"))]));
let overlay = as_obj(obj(&[("a", obj(&[("nested", scalar("v"))]))]));
let merged = as_obj(deep_merge_hocon_objects(base, overlay));
let a = as_obj(merged.get("a").unwrap().clone());
assert_eq!(a.get("nested"), Some(&scalar("v")));
}
#[test]
fn deep_merge_empty_overlay_is_noop() {
let base = as_obj(obj(&[("a", scalar("x"))]));
let overlay: IndexMap<String, HoconValue> = IndexMap::new();
let merged = as_obj(deep_merge_hocon_objects(base, overlay));
assert_eq!(merged.get("a"), Some(&scalar("x")));
assert_eq!(merged.len(), 1);
let empty_base: IndexMap<String, HoconValue> = IndexMap::new();
let overlay = as_obj(obj(&[("a", scalar("y"))]));
let merged = as_obj(deep_merge_hocon_objects(empty_base, overlay));
assert_eq!(merged.get("a"), Some(&scalar("y")));
assert_eq!(merged.len(), 1);
}
#[test]
fn deep_merge_handles_deeply_nested_without_quadratic_clones() {
fn build(depth: usize, leaf_label: &str) -> HoconValue {
if depth == 0 {
return scalar(leaf_label);
}
obj(&[("nested", build(depth - 1, leaf_label))])
}
let base = as_obj(build(10, "base-leaf"));
let overlay = as_obj(build(10, "overlay-leaf"));
let merged = deep_merge_hocon_objects(base, overlay);
let mut cur = merged;
for _ in 0..10 {
let m = as_obj(cur);
cur = m.get("nested").cloned().unwrap();
}
assert_eq!(cur, scalar("overlay-leaf"));
}
}