use std::path::Path;
use concinnity_cook::authoring::world::{
WORLD_JSONL, find_entry, known_names, patch_world_jsonl_to,
};
use concinnity_cook::build_only::include::with_includes;
pub(crate) fn rm_at_path(world_path: &str, name: &str) -> std::io::Result<()> {
let mut removed = false;
patch_world_jsonl_to(world_path, world_path, |assets| {
let found = line_index(assets, Path::new(world_path), name)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
if let Some(i) = found {
let asset = assets.remove(i);
tracing::info!(
"Removed '{}' (type: {})",
name,
asset.get("type").and_then(|v| v.as_str()).unwrap_or("?"),
);
removed = true;
}
Ok(())
})?;
if !removed {
let known = known_names(world_path).unwrap_or_default();
if known.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"no asset named '{}' in {} (no assets declared)",
name, WORLD_JSONL
),
));
}
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"no asset named '{}' in {}\nKnown names: {}",
name,
WORLD_JSONL,
known.join(", ")
),
));
}
super::build_world_file(world_path)
}
fn line_index(
lines: &[serde_json::Value],
world_file: &Path,
name: &str,
) -> Result<Option<usize>, String> {
let sourced = with_includes(lines.to_vec(), Some(world_file))?;
let entries: Vec<serde_json::Value> = sourced.iter().map(|s| s.entry.clone()).collect();
let Some(i) = find_entry(&entries, name) else {
return Ok(None);
};
if let Some(file) = &sourced[i].file {
return Err(format!(
"'{name}' is declared in {}, which {} includes; remove it there",
file.display(),
world_file.display()
));
}
Ok(Some(
sourced[..i].iter().filter(|s| s.file.is_none()).count(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rm_of_a_missing_world_file_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing.jsonl");
assert!(rm_at_path(path.to_str().unwrap(), "anything").is_err());
}
#[test]
fn rm_of_an_unknown_name_lists_the_known_names() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("world.jsonl");
std::fs::write(
&path,
concat!(
"[\"Logger\",{\"$id\":\"log\"}]\n",
"[\"Logger\",{\"$id\":\"log2\"}]\n",
),
)
.unwrap();
let err = rm_at_path(path.to_str().unwrap(), "ghost").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
let msg = err.to_string();
assert!(msg.contains("no asset named 'ghost'"), "got: {msg}");
assert!(msg.contains("Known names: log, log2"), "got: {msg}");
let survived = std::fs::read_to_string(&path).unwrap();
assert!(survived.contains("\"log\""));
assert!(survived.contains("\"log2\""));
}
#[test]
fn rm_from_an_empty_world_reports_no_assets() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("world.jsonl");
std::fs::write(&path, "").unwrap();
let err = rm_at_path(path.to_str().unwrap(), "ghost").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("no assets declared"), "got: {err}");
}
#[test]
fn a_label_is_counted_across_includes() {
let dir = tempfile::tempdir().unwrap();
let world = dir.path().join("world.jsonl");
std::fs::write(
dir.path().join("props.jsonl"),
"[\"Prop\",{\"mesh\":\"inc\"}]\n",
)
.unwrap();
let lines = vec![
serde_json::json!({"type": "Prop", "args": {"mesh": "a"}}),
serde_json::json!({"type": "Include", "args": {"path": "props.jsonl"}}),
serde_json::json!({"type": "Prop", "args": {"mesh": "b"}}),
];
assert_eq!(line_index(&lines, &world, "Prop#0"), Ok(Some(0)));
assert_eq!(line_index(&lines, &world, "Prop#2"), Ok(Some(2)));
assert_eq!(line_index(&lines, &world, "Include#0"), Ok(Some(1)));
assert_eq!(line_index(&lines, &world, "Prop#3"), Ok(None));
let err = line_index(&lines, &world, "Prop#1").unwrap_err();
assert!(
err.contains("props.jsonl") && err.contains("remove it there"),
"{err}"
);
}
}