use std::path::Path;
use serde_json::{Map, Value};
use crate::error::AgentConfigError;
use crate::util::fs_atomic;
pub(crate) const TAG_KEY: &str = "_agent_config_tag";
pub(crate) fn read_or_empty(path: &Path) -> Result<Value, AgentConfigError> {
let bytes = fs_atomic::read_capped_or_empty(path)?;
if bytes.is_empty() || bytes.iter().all(|b| b.is_ascii_whitespace()) {
return Ok(Value::Object(Map::new()));
}
serde_json::from_slice(&bytes).map_err(|e| AgentConfigError::json(path, e))
}
pub(crate) fn to_pretty(value: &Value) -> Vec<u8> {
let mut buf = serde_json::to_vec_pretty(value).expect("valid JSON serializes");
buf.push(b'\n');
buf
}
pub(crate) fn ensure_object<'a>(
root: &'a mut Value,
path: &[&str],
) -> Result<&'a mut Map<String, Value>, AgentConfigError> {
if !root.is_object() {
*root = Value::Object(Map::new());
}
let mut cur = root.as_object_mut().unwrap();
for key in path {
if !cur.contains_key(*key) {
cur.insert((*key).to_string(), Value::Object(Map::new()));
} else if !cur[*key].is_object() {
return Err(AgentConfigError::Other(anyhow::anyhow!(
"expected object at JSON path segment {:?}, found {:?}",
key,
cur[*key]
)));
}
cur = cur.get_mut(*key).unwrap().as_object_mut().unwrap();
}
Ok(cur)
}
pub(crate) fn ensure_array<'a>(
root: &'a mut Value,
path: &[&str],
) -> Result<&'a mut Vec<Value>, AgentConfigError> {
if path.is_empty() {
return Err(AgentConfigError::Other(anyhow::anyhow!(
"ensure_array requires a non-empty path"
)));
}
let (last, parents) = path.split_last().unwrap();
let parent = ensure_object(root, parents)?;
let entry = parent
.entry((*last).to_string())
.or_insert_with(|| Value::Array(Vec::new()));
match entry {
Value::Array(a) => Ok(a),
other => Err(AgentConfigError::Other(anyhow::anyhow!(
"expected array at JSON path segment {:?}, found {:?}",
last,
other
))),
}
}
pub(super) fn find_tagged_index(arr: &[Value], tag: &str) -> Option<usize> {
arr.iter().position(|v| matches_tag(v, tag))
}
pub(super) fn matches_tag(v: &Value, tag: &str) -> bool {
v.as_object()
.and_then(|o| o.get(TAG_KEY))
.and_then(Value::as_str)
== Some(tag)
}
pub(super) fn traverse_array<'a>(root: &'a Value, path: &[&str]) -> Option<&'a Vec<Value>> {
let mut cur = root;
for key in path {
cur = cur.get(*key)?;
}
cur.as_array()
}
pub(super) fn traverse_object<'a>(
root: &'a Value,
path: &[&str],
) -> Option<&'a Map<String, Value>> {
let mut cur = root;
for key in path {
cur = cur.get(*key)?;
}
cur.as_object()
}
pub(super) fn traverse_array_mut<'a>(
root: &'a mut Value,
path: &[&str],
) -> Option<&'a mut Vec<Value>> {
let mut cur = root;
for key in path {
cur = cur.get_mut(*key)?;
}
cur.as_array_mut()
}
pub(super) fn prune_empty_path(root: &mut Value, path: &[&str]) {
for depth in (1..=path.len()).rev() {
let (parent_path, key) = path[..depth].split_at(depth - 1);
let key = key[0];
let Some(parent) = traverse_object_mut(root, parent_path) else {
return;
};
let should_remove = match parent.get(key) {
Some(Value::Array(a)) => a.is_empty(),
Some(Value::Object(o)) => o.is_empty(),
_ => return,
};
if !should_remove {
return;
}
parent.remove(key);
}
}
pub(super) fn traverse_object_mut<'a>(
root: &'a mut Value,
path: &[&str],
) -> Option<&'a mut Map<String, Value>> {
let mut cur = root;
for key in path {
cur = cur.get_mut(*key)?;
}
cur.as_object_mut()
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn read_missing_file_returns_empty_object() {
let dir = tempfile::tempdir().unwrap();
let v = read_or_empty(&dir.path().join("absent.json")).unwrap();
assert_eq!(v, json!({}));
}
#[test]
fn read_invalid_json_errors() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("bad.json");
std::fs::write(&p, b"{not valid").unwrap();
let err = read_or_empty(&p).unwrap_err();
assert!(matches!(err, AgentConfigError::JsonInvalid { .. }));
}
#[test]
fn ensure_object_rejects_non_object_collision() {
let mut root = json!({ "hooks": "oops a string" });
let err = ensure_object(&mut root, &["hooks"]).unwrap_err();
assert!(matches!(err, AgentConfigError::Other(_)));
}
#[test]
fn pretty_emits_trailing_newline() {
let bytes = to_pretty(&json!({ "a": 1 }));
assert!(bytes.ends_with(b"\n"));
}
}