use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Map, Value};
use crate::ConfigError;
use crate::path::{concrete_paths_overlap, direct_child_array_index};
pub(super) fn record_patch_indexed_array_state(
current_array_lengths: &mut BTreeMap<String, usize>,
indexed_array_base_lengths: &mut BTreeMap<String, usize>,
path: &str,
indexed_array_container_paths: &BTreeSet<String>,
) {
for container_path in indexed_array_container_paths {
let Some(index) = direct_child_array_index(container_path, path) else {
continue;
};
let Some(current_length) = current_array_lengths.get_mut(container_path) else {
continue;
};
indexed_array_base_lengths
.entry(container_path.clone())
.or_insert(*current_length);
if index >= *current_length {
*current_length = index + 1;
}
}
}
pub(super) fn insert_path_with_shape(
current: &mut Value,
shape: Option<&Value>,
segments: &[String],
array_segments: &BTreeSet<usize>,
depth: usize,
value: Value,
) -> Result<(), String> {
let segment = &segments[depth];
if segment.is_empty() {
return Err("configuration path contains an empty segment".to_owned());
}
let is_last = depth + 1 == segments.len();
match current {
Value::Object(map) => {
if is_last {
map.insert(segment.clone(), value);
return Ok(());
}
let shape_child = match shape {
Some(Value::Object(shape_map)) => shape_map.get(segment),
_ => None,
};
let next_is_array = array_segments.contains(&(depth + 1));
let child = map.entry(segment.clone()).or_insert(patch_next_container(
shape_child,
next_is_array,
&segments[depth + 1],
)?);
ensure_patch_container(child, shape_child, next_is_array, segment)?;
insert_path_with_shape(
child,
shape_child,
segments,
array_segments,
depth + 1,
value,
)
}
Value::Array(values) => {
let index = segment.parse::<usize>().map_err(|_| {
format!("path segment {segment} must be an array index at this position")
})?;
if values.len() <= index {
values.resize(index + 1, Value::Null);
}
if is_last {
values[index] = value;
return Ok(());
}
let shape_child = match shape {
Some(Value::Array(shape_values)) => shape_values.get(index),
_ => None,
};
if values[index].is_null() {
let next_is_array = array_segments.contains(&(depth + 1));
values[index] =
patch_next_container(shape_child, next_is_array, &segments[depth + 1])?;
}
let next_is_array = array_segments.contains(&(depth + 1));
ensure_patch_container(&values[index], shape_child, next_is_array, segment)?;
insert_path_with_shape(
&mut values[index],
shape_child,
segments,
array_segments,
depth + 1,
value,
)
}
_ => Err(format!(
"path segment {segment} conflicts with an existing non-container value"
)),
}
}
pub(super) fn claim_patch_path(
layer_name: &str,
path: &str,
claimed_paths: &mut BTreeSet<String>,
) -> Result<(), ConfigError> {
for existing_path in claimed_paths.iter() {
if existing_path == path {
return Err(ConfigError::InvalidPatch {
name: layer_name.to_owned(),
path: path.to_owned(),
message: format!("duplicate patch path `{path}`"),
});
}
if concrete_paths_overlap(existing_path, path) {
return Err(ConfigError::InvalidPatch {
name: layer_name.to_owned(),
path: path.to_owned(),
message: format!("conflicting patch paths `{existing_path}` and `{path}` overlap"),
});
}
}
claimed_paths.insert(path.to_owned());
Ok(())
}
fn patch_next_container(
shape_child: Option<&Value>,
next_is_array: bool,
next_segment: &str,
) -> Result<Value, String> {
match shape_child {
Some(Value::Object(_)) => Ok(Value::Object(Map::new())),
Some(Value::Array(_)) => Ok(Value::Array(Vec::new())),
Some(_) => Err(format!(
"path segment {next_segment} conflicts with an existing non-container value"
)),
None => Ok(if next_is_array {
Value::Array(Vec::new())
} else {
Value::Object(Map::new())
}),
}
}
fn ensure_patch_container(
child: &Value,
shape_child: Option<&Value>,
next_is_array: bool,
segment: &str,
) -> Result<(), String> {
let expected_array =
matches!(shape_child, Some(Value::Array(_))) || (shape_child.is_none() && next_is_array);
let expected_object =
matches!(shape_child, Some(Value::Object(_))) || (shape_child.is_none() && !next_is_array);
match child {
Value::Object(_) if expected_object => Ok(()),
Value::Array(_) if expected_array => Ok(()),
_ => Err(format!(
"path segment {segment} conflicts with an existing non-container value"
)),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::claim_patch_path;
#[test]
fn patch_path_claims_reject_segment_aware_overlaps() {
let mut claimed = BTreeSet::from(["users".to_owned()]);
assert!(claim_patch_path("patch", "users.0.name", &mut claimed).is_err());
assert!(claim_patch_path("patch", "users_profile.0", &mut claimed).is_ok());
}
#[test]
fn patch_path_claims_reject_root_overlap() {
let mut claimed = BTreeSet::from(["users".to_owned()]);
assert!(claim_patch_path("patch", "", &mut claimed).is_err());
}
}