use anyhow::{bail, Context, Result};
enum Frame {
Object { key: Option<String>, key_next: bool },
Array,
}
pub fn replace_string(content: &str, path: &[&str], value: &str) -> Result<Option<String>> {
serde_json::from_str::<serde_json::Value>(content).context("not JSON")?;
let replacement = serde_json::to_string(value)?;
let bytes = content.as_bytes();
let string_end = |start: usize| -> usize {
let mut at = start + 1;
while bytes[at] != b'"' {
at += if bytes[at] == b'\\' { 2 } else { 1 };
}
at + 1
};
let skip_space = |mut at: usize| -> usize {
while bytes[at].is_ascii_whitespace() {
at += 1;
}
at
};
let at_path = |stack: &[Frame]| {
stack.len() == path.len()
&& stack.iter().zip(path).all(|(frame, want)| {
matches!(frame, Frame::Object { key: Some(key), .. } if key == want)
})
};
let mut stack: Vec<Frame> = Vec::new();
let mut at = 0;
while at < bytes.len() {
match bytes[at] {
b'"' => {
let end = string_end(at);
if let Some(Frame::Object { key, key_next }) = stack.last_mut() {
if *key_next {
*key = Some(serde_json::from_str(&content[at..end])?);
*key_next = false;
let value_at = skip_space(skip_space(end) + 1);
if at_path(&stack) && bytes[value_at] != b'"' {
bail!("{} is not a string", path.join("."));
}
at = value_at;
continue;
}
}
if at_path(&stack) {
return Ok(Some(format!("{}{replacement}{}", &content[..at], &content[end..])));
}
at = end;
continue;
}
b'{' => stack.push(Frame::Object { key: None, key_next: true }),
b'[' => stack.push(Frame::Array),
b'}' | b']' => {
stack.pop();
}
b',' => {
if let Some(Frame::Object { key_next, .. }) = stack.last_mut() {
*key_next = true;
}
}
_ => {}
}
at += 1;
}
Ok(None)
}
pub fn set_top_level_string(content: &str, key: &str, value: &str) -> Result<String> {
let parsed: serde_json::Value = serde_json::from_str(content).context("not JSON")?;
if !parsed.is_object() {
bail!("not a JSON object");
}
if let Some(replaced) = replace_string(content, &[key], value)? {
return Ok(replaced);
}
let bytes = content.as_bytes();
let opening = content.find('{').expect("a JSON object has an opening brace");
let mut first = opening + 1;
while bytes[first].is_ascii_whitespace() {
first += 1;
}
let indent = &content[opening + 1..first];
let member = format!("{}: {}", serde_json::to_string(key)?, serde_json::to_string(value)?);
Ok(if bytes[first] == b'}' {
format!("{}{member}{}", &content[..opening + 1], &content[opening + 1..])
} else {
format!("{}{indent}{member},{}", &content[..opening + 1], &content[opening + 1..])
})
}
#[cfg(test)]
#[path = "json_edit_tests.rs"]
mod tests;