use std::collections::{BTreeMap, BTreeSet};
use serde_json::Value;
use crate::ConfigError;
use crate::path::{join_path, normalize_path, path_is_at_or_below};
pub(super) use crate::path::direct_child_array_index;
pub(super) fn ensure_path_safe_keys(value: &Value, current_path: &str) -> Result<(), ConfigError> {
match value {
Value::Object(map) => {
for (key, child) in map {
validate_path_key(current_path, key)?;
let next = join_path(current_path, key);
ensure_path_safe_keys(child, &next)?;
}
Ok(())
}
Value::Array(values) => {
for (index, child) in values.iter().enumerate() {
let next = join_path(current_path, &index.to_string());
ensure_path_safe_keys(child, &next)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn validate_path_key(current_path: &str, key: &str) -> Result<(), ConfigError> {
let message = invalid_path_key_message(key);
if let Some(message) = message {
Err(ConfigError::InvalidPathKey {
path: current_path.to_owned(),
key: key.to_owned(),
message,
})
} else {
Ok(())
}
}
pub(super) fn invalid_path_key_message(key: &str) -> Option<String> {
if key.is_empty() {
Some("empty object keys are not supported".to_owned())
} else if key == "*" {
Some("`*` is reserved for wildcard metadata paths".to_owned())
} else if key.contains('.') {
Some("`.` is reserved as the configuration path separator".to_owned())
} else if key.contains('[') || key.contains(']') {
Some("`[` and `]` are reserved for external array path syntax".to_owned())
} else {
None
}
}
pub(super) fn invalid_concrete_path_segment(path: &str) -> Option<(&str, String)> {
path.split('.')
.find_map(|segment| invalid_path_key_message(segment).map(|message| (segment, message)))
}
pub(crate) fn indexed_array_container_paths(segments: &[&str]) -> BTreeSet<String> {
let mut paths = BTreeSet::new();
for index in 0..segments.len() {
if segments[index].parse::<usize>().is_ok() && index > 0 {
paths.insert(segments[..index].join("."));
}
}
paths
}
pub(crate) fn record_indexed_array_state(
current_array_lengths: &mut BTreeMap<String, usize>,
indexed_array_base_lengths: &mut BTreeMap<String, usize>,
path: &str,
segments: &[&str],
) {
for container_path in indexed_array_container_paths(segments) {
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(crate) fn record_direct_array_state(
current_array_lengths: &mut BTreeMap<String, usize>,
indexed_array_base_lengths: &mut BTreeMap<String, usize>,
path: &str,
value: &Value,
) {
clear_array_state(current_array_lengths, path);
clear_array_state(indexed_array_base_lengths, path);
collect_array_lengths(value, path, current_array_lengths);
}
fn clear_array_state<T>(state: &mut BTreeMap<String, T>, path: &str) {
state.retain(|candidate, _| !path_is_at_or_below(candidate, path));
}
fn collect_array_lengths(value: &Value, path: &str, lengths: &mut BTreeMap<String, usize>) {
match value {
Value::Object(map) => {
for (key, child) in map {
let next = join_path(path, key);
collect_array_lengths(child, &next, lengths);
}
}
Value::Array(values) => {
lengths.insert(path.to_owned(), values.len());
for (index, child) in values.iter().enumerate() {
let next = join_path(path, &index.to_string());
collect_array_lengths(child, &next, lengths);
}
}
_ => {}
}
}
pub(crate) fn normalize_external_path(path: &str) -> String {
try_normalize_external_path(path).unwrap_or_else(|_| normalize_path(path))
}
pub(crate) fn try_normalize_external_path(path: &str) -> Result<String, String> {
crate::path::normalize_external_path(path)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::clear_array_state;
#[test]
fn clear_array_state_removes_replaced_path_and_descendants_only() {
let mut state = BTreeMap::from([
("users".to_owned(), 2),
("users.0.roles".to_owned(), 1),
("users_profile".to_owned(), 1),
]);
clear_array_state(&mut state, "users");
assert_eq!(state, BTreeMap::from([("users_profile".to_owned(), 1)]));
}
#[test]
fn clear_array_state_removes_everything_for_root_replacement() {
let mut state = BTreeMap::from([
("".to_owned(), 1),
("0.roles".to_owned(), 2),
("service.users".to_owned(), 3),
]);
clear_array_state(&mut state, "");
assert!(state.is_empty());
}
}