use std::io::Write;
use std::time::Instant;
use anyhow::{Context, Result};
use serde::Serialize;
use crate::atomic::{AtomicWriteOptions, atomic_write};
use crate::cli::{DelArgs, GlobalArgs};
use crate::commands::resolve_backup;
use crate::output::NdjsonWriter;
#[derive(Debug, Serialize)]
struct DelResult {
r#type: &'static str,
path: String,
key_path: String,
removed_value: Option<String>,
existed: bool,
format: &'static str,
elapsed_ms: u64,
}
#[tracing::instrument(skip_all, fields(command = "del"))]
pub fn cmd_del(
args: &DelArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
defaults: &crate::config::DefaultsSection,
) -> Result<()> {
let start = Instant::now();
let workspace = global.resolve_workspace()?;
let resolved = resolve_backup(&args.backup_opts, defaults);
let validated = crate::path_safety::validate_path(&args.path, &workspace)?;
if !validated.exists() {
return Err(crate::error::AtomwriteError::NotFound { path: validated }.into());
}
let original =
crate::file_io::read_file_string(&validated, global.effective_max_filesize())?;
let (new_content, removed_value, existed, format) =
match validated.extension().and_then(|s| s.to_str()) {
Some("toml") => {
let mut doc: toml_edit::DocumentMut =
original.parse().with_context(|| "invalid TOML in source")?;
let removed = remove_toml_path(&mut doc, &args.key_path);
let existed = removed.is_some();
(doc.to_string(), removed, existed, "toml")
}
Some("json") => {
let mut v: serde_json::Value =
serde_json::from_str(&original).with_context(|| "invalid JSON in source")?;
if !crate::output::check_json_depth(&v, crate::constants::MAX_JSON_DEPTH) {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: format!(
"JSON nesting depth exceeds maximum of {}",
crate::constants::MAX_JSON_DEPTH
),
}
.into());
}
let pointer = crate::output::dotted_to_json_pointer(&args.key_path);
let removed = v.pointer(&pointer).map(|x| match x {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
});
let existed = removed.is_some();
remove_json_pointer(&mut v, &pointer);
let new_content = serde_json::to_string_pretty(&v)?;
(new_content, removed, existed, "json")
}
other => {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: format!(
"unsupported format for `del` (extension: {other:?}); supported: toml, json"
),
}
.into());
}
};
if !existed && !args.force_missing {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: format!(
"key '{}' not found in {}",
args.key_path,
validated.display()
),
}
.into());
}
if !existed && args.force_missing {
let output = DelResult {
r#type: "del",
path: validated.display().to_string(),
key_path: args.key_path.clone(),
removed_value,
existed: false,
format,
elapsed_ms: start.elapsed().as_millis() as u64,
};
writer.write_event(&output)?;
return Ok(());
}
let opts = AtomicWriteOptions {
backup: resolved.backup,
syntax_check: false,
retention: resolved.retention,
preserve_timestamps: args.preserve_timestamps,
backup_output_dir: None,
strategy: None,
strict_atomic: false,
wal_policy: crate::wal::WalPolicy::Auto,
keep_backup: resolved.keep,
durability: crate::platform::Durability::Auto,
};
let _ = atomic_write(&validated, new_content.as_bytes(), &opts, &workspace)?;
let output = DelResult {
r#type: "del",
path: validated.display().to_string(),
key_path: args.key_path.clone(),
removed_value,
existed: true,
format,
elapsed_ms: start.elapsed().as_millis() as u64,
};
writer.write_event(&output)?;
Ok(())
}
fn remove_json_pointer(root: &mut serde_json::Value, pointer: &str) {
use serde_json::Value;
let segments: Vec<String> = pointer
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.map(|s| s.replace("~1", "/").replace("~0", "~"))
.collect();
if segments.is_empty() {
return;
}
let mut current = root;
for seg in &segments[..segments.len() - 1] {
match current {
Value::Object(map) => match map.get_mut(seg) {
Some(v) => current = v,
None => return,
},
Value::Array(arr) => {
if let Ok(idx) = seg.parse::<usize>() {
if idx < arr.len() {
current = &mut arr[idx];
} else {
return;
}
} else {
return;
}
}
_ => return,
}
}
let last = &segments[segments.len() - 1];
match current {
Value::Object(map) => {
map.remove(last);
}
Value::Array(arr) => {
if let Ok(idx) = last.parse::<usize>() {
if idx < arr.len() {
arr.remove(idx);
}
}
}
_ => {}
}
}
fn remove_toml_path(doc: &mut toml_edit::DocumentMut, key_path: &str) -> Option<String> {
let segments: Vec<&str> = key_path.split('.').collect();
if segments.is_empty() {
return None;
}
let last = match segments.last() {
Some(s) => *s,
None => return None,
};
let mut current: &mut toml_edit::Item = doc.as_item_mut();
for seg in &segments[..segments.len() - 1] {
match current.as_table_mut() {
Some(table) => match table.get_mut(seg) {
Some(item) => current = item,
None => return None,
},
None => return None,
}
}
let table = current.as_table_mut()?;
let removed = table.get(last).map(|item| {
if let Some(v) = item.as_value() {
match v.as_str() {
Some(s) => s.to_owned(),
None => v.to_string().trim().to_owned(),
}
} else {
item.to_string().trim().to_owned()
}
});
table.remove(last);
removed
}