Skip to main content

concinnity_dev/command/
explain.rs

1// src/cli/explain.rs
2// Print one asset's effective entry from the expanded world: the full JSONL
3// line as the build sees it, pasteable into world.jsonl verbatim. This is the
4// override path for injected defaults and expanded assets, which have no line
5// in the authored file to copy from.
6
7use crate::command::{provenance, resolve_world_path};
8
9/// Print one asset's effective entry from the expanded world, with where
10/// each value came from.
11pub fn explain(name: &str, json_path: Option<&str>) -> std::io::Result<()> {
12    let json_path = resolve_world_path(json_path)?;
13    let content = std::fs::read_to_string(&json_path)?;
14
15    let loaded = concinnity_cook::prepare_world(
16        &content,
17        crate::project::assets_dir().as_deref(),
18        crate::cook_platform(),
19    )
20    .map_err(|errs| concinnity_cook::check::report_validation_errors(&errs))?;
21
22    let Some(asset) = loaded.assets.iter().find(|a| a.name == name) else {
23        let mut close: Vec<&str> = loaded
24            .assets
25            .iter()
26            .map(|a| a.name.as_str())
27            .filter(|n| n.contains(name))
28            .take(5)
29            .collect();
30        close.sort_unstable();
31        let hint = if close.is_empty() {
32            String::new()
33        } else {
34            format!("; close matches: {}", close.join(", "))
35        };
36        return Err(std::io::Error::new(
37            std::io::ErrorKind::NotFound,
38            format!("no asset named '{}' in the expanded world{}", name, hint),
39        ));
40    };
41
42    let line = serde_json::json!({
43        "name": asset.name,
44        "type": asset.asset_type,
45        "args": asset.args,
46    });
47
48    println!("// {}", provenance(&loaded, &asset.name));
49    println!("{}", serde_json::to_string(&line)?);
50    Ok(())
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    fn write_world(content: &str) -> (tempfile::TempDir, String) {
58        let dir = tempfile::tempdir().unwrap();
59        let path = dir.path().join("world.jsonl");
60        std::fs::write(&path, content).unwrap();
61        (dir, path.to_string_lossy().into_owned())
62    }
63
64    #[test]
65    fn explain_prints_a_known_asset() {
66        let (_dir, path) =
67            write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
68        explain("gfx", Some(&path)).unwrap();
69    }
70
71    #[test]
72    fn explain_of_an_unknown_name_offers_close_matches() {
73        let (_dir, path) =
74            write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
75        let err = explain("gf", Some(&path)).unwrap_err();
76        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
77        let msg = err.to_string();
78        assert!(msg.contains("close matches"), "got: {msg}");
79        assert!(msg.contains("gfx"), "got: {msg}");
80    }
81
82    #[test]
83    fn explain_of_an_unknown_name_without_matches_has_no_hint() {
84        let (_dir, path) =
85            write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
86        let err = explain("zzz_nothing", Some(&path)).unwrap_err();
87        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
88        assert!(!err.to_string().contains("close matches"), "got: {err}");
89    }
90
91    #[test]
92    fn explain_surfaces_validation_failures() {
93        let (_dir, path) =
94            write_world("{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n");
95        assert!(explain("odd", Some(&path)).is_err());
96    }
97}