Skip to main content

concinnity_dev/authoring/
rm.rs

1// src/rm.rs
2// Remove an asset from a world JSONL by its unique `name` field and rebuild.
3
4use crate::world::{WORLD_JSONL, known_names, patch_world_jsonl};
5use concinnity_cook::build_from_path;
6
7/// Remove the asset named `name` from `world_path` and rebuild.
8///
9/// Errors if `name` is not present. When it isn't, the error message includes
10/// the known asset names from the world so the caller can suggest a fix.
11pub fn rm_at_path(world_path: &str, name: &str) -> std::io::Result<()> {
12    let mut removed = false;
13
14    patch_world_jsonl(world_path, |assets| {
15        if let Some(i) = assets
16            .iter()
17            .position(|a| a.get("name").and_then(|v| v.as_str()) == Some(name))
18        {
19            let asset = assets.remove(i);
20            tracing::info!(
21                "Removed '{}' (type: {})",
22                name,
23                asset.get("type").and_then(|v| v.as_str()).unwrap_or("?"),
24            );
25            removed = true;
26        }
27    })?;
28
29    if !removed {
30        let known = known_names(world_path).unwrap_or_default();
31        if known.is_empty() {
32            return Err(std::io::Error::new(
33                std::io::ErrorKind::InvalidInput,
34                format!(
35                    "no asset named '{}' in {} (no assets declared)",
36                    name, WORLD_JSONL
37                ),
38            ));
39        }
40        return Err(std::io::Error::new(
41            std::io::ErrorKind::InvalidInput,
42            format!(
43                "no asset named '{}' in {}\nKnown names: {}",
44                name,
45                WORLD_JSONL,
46                known.join(", ")
47            ),
48        ));
49    }
50
51    build_from_path(
52        &crate::project::require()?,
53        world_path,
54        crate::cook_platform(),
55    )
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    // Removal failures surface before any rebuild runs, so these tests never
63    // touch the compile pipeline.
64
65    #[test]
66    fn rm_of_a_missing_world_file_errors() {
67        let dir = tempfile::tempdir().unwrap();
68        let path = dir.path().join("missing.jsonl");
69        assert!(rm_at_path(path.to_str().unwrap(), "anything").is_err());
70    }
71
72    #[test]
73    fn rm_of_an_unknown_name_lists_the_known_names() {
74        let dir = tempfile::tempdir().unwrap();
75        let path = dir.path().join("world.jsonl");
76        std::fs::write(
77            &path,
78            concat!(
79                "{\"name\":\"log\",\"type\":\"Logger\",\"args\":{}}\n",
80                "{\"name\":\"log2\",\"type\":\"Logger\",\"args\":{}}\n",
81            ),
82        )
83        .unwrap();
84
85        let err = rm_at_path(path.to_str().unwrap(), "ghost").unwrap_err();
86        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
87        let msg = err.to_string();
88        assert!(msg.contains("no asset named 'ghost'"), "got: {msg}");
89        assert!(msg.contains("Known names: log, log2"), "got: {msg}");
90        // The world file itself is left intact.
91        let survived = std::fs::read_to_string(&path).unwrap();
92        assert!(survived.contains("\"log\""));
93        assert!(survived.contains("\"log2\""));
94    }
95
96    #[test]
97    fn rm_from_an_empty_world_reports_no_assets() {
98        let dir = tempfile::tempdir().unwrap();
99        let path = dir.path().join("world.jsonl");
100        std::fs::write(&path, "").unwrap();
101
102        let err = rm_at_path(path.to_str().unwrap(), "ghost").unwrap_err();
103        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
104        assert!(err.to_string().contains("no assets declared"), "got: {err}");
105    }
106}