Skip to main content

cobble/commands/
inspect.rs

1use serde_json::Value;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5pub struct InspectOptions {
6    pub input: PathBuf,
7    pub json: bool,
8}
9
10pub fn inspect(options: InspectOptions) -> Result<(), String> {
11    let summary = read_inspection(&options.input)?;
12
13    if options.json {
14        let output = serde_json::json!({
15            "manifest": summary.manifest,
16            "source_map_entries": summary.source_map_entries,
17        });
18        println!(
19            "{}",
20            serde_json::to_string_pretty(&output)
21                .map_err(|error| format!("Failed to format inspection JSON: {error}"))?
22        );
23        return Ok(());
24    }
25
26    print_inspection(&options.input, &summary);
27    Ok(())
28}
29
30#[derive(Debug)]
31struct InspectionSummary {
32    manifest: Value,
33    source_map_entries: Option<usize>,
34}
35
36fn read_inspection(input: &Path) -> Result<InspectionSummary, String> {
37    let manifest_path = input.join(".cobble").join("build_manifest.json");
38    let manifest_content = fs::read_to_string(&manifest_path).map_err(|error| {
39        format!(
40            "No Cobble build manifest found at {}: {error}\n\
41            Run `cobble build` on a data pack directory first. ZIP archives do not include .cobble metadata.",
42            manifest_path.display()
43        )
44    })?;
45    let manifest = serde_json::from_str::<Value>(&manifest_content)
46        .map_err(|error| format!("Failed to parse {}: {error}", manifest_path.display()))?;
47
48    Ok(InspectionSummary {
49        source_map_entries: read_source_map_entry_count(input)?,
50        manifest,
51    })
52}
53
54fn read_source_map_entry_count(input: &Path) -> Result<Option<usize>, String> {
55    let source_map_path = input.join(".cobble").join("source_map.json");
56    if !source_map_path.exists() {
57        return Ok(None);
58    }
59
60    let content = fs::read_to_string(&source_map_path)
61        .map_err(|error| format!("Failed to read {}: {error}", source_map_path.display()))?;
62    let source_map = serde_json::from_str::<Value>(&content)
63        .map_err(|error| format!("Failed to parse {}: {error}", source_map_path.display()))?;
64    Ok(source_map
65        .get("entries")
66        .and_then(Value::as_array)
67        .map(Vec::len))
68}
69
70fn print_inspection(input: &Path, summary: &InspectionSummary) {
71    let manifest = &summary.manifest;
72    println!("Cobble inspect: {}", input.display());
73    println!(
74        "  Cobble version: {}",
75        string_value(manifest, &["cobble_version"]).unwrap_or("unknown")
76    );
77    println!(
78        "  Minecraft target: Java Edition {}",
79        string_value(manifest, &["minecraft_version"]).unwrap_or("unknown")
80    );
81    println!(
82        "  Pack format: {}",
83        string_value(manifest, &["pack_format_text"]).unwrap_or("unknown")
84    );
85    println!(
86        "  Namespace: {}",
87        string_value(manifest, &["namespace"]).unwrap_or("unknown")
88    );
89
90    if let Some(input) = manifest.get("input").and_then(Value::as_object) {
91        if let Some(source) = input.get("source").and_then(Value::as_str) {
92            println!("  Source: {source}");
93        }
94        if let Some(compiled_files) = input.get("compiled_files").and_then(Value::as_array) {
95            println!("  Source files: {}", compiled_files.len());
96        }
97    }
98
99    println!("Generated:");
100    print_usize(manifest, &["generated", "functions"], "  Functions");
101    print_usize(manifest, &["generated", "commands"], "  Commands");
102    print_usize(
103        manifest,
104        &["generated", "total_json_resources"],
105        "  JSON resources",
106    );
107    print_usize(manifest, &["generated", "function_tags"], "  Function tags");
108
109    match summary.source_map_entries {
110        Some(entries) => println!("  Source map entries: {entries}"),
111        None => println!("  Source map entries: none"),
112    }
113
114    if let Some(validation) = manifest.get("validation").filter(|value| !value.is_null()) {
115        println!("Validation:");
116        print_usize(validation, &["commands_checked"], "  Commands checked");
117        print_usize(validation, &["errors"], "  Command errors");
118        print_usize(validation, &["source_map_errors"], "  Source map errors");
119    } else {
120        println!("Validation: not recorded");
121    }
122
123    if let Some(resources) = manifest.get("resources").and_then(Value::as_array) {
124        println!("Resources: {}", resources.len());
125        for resource in resources.iter().take(12) {
126            let kind = resource
127                .get("kind")
128                .and_then(Value::as_str)
129                .unwrap_or("resource");
130            let namespace = resource
131                .get("namespace")
132                .and_then(Value::as_str)
133                .unwrap_or("unknown");
134            let path = resource.get("path").and_then(Value::as_str).unwrap_or("");
135            println!("  {kind}: {namespace}:{path}");
136        }
137        if resources.len() > 12 {
138            println!("  ... {} more", resources.len() - 12);
139        }
140    }
141}
142
143fn string_value<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> {
144    let mut current = value;
145    for segment in path {
146        current = current.get(*segment)?;
147    }
148    current.as_str()
149}
150
151fn print_usize(value: &Value, path: &[&str], label: &str) {
152    let mut current = value;
153    for segment in path {
154        let Some(next) = current.get(*segment) else {
155            return;
156        };
157        current = next;
158    }
159    if let Some(number) = current.as_u64() {
160        println!("{label}: {number}");
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn read_inspection_loads_manifest_and_source_map_count() {
170        let temp_dir = tempfile::TempDir::new().unwrap();
171        let cobble_dir = temp_dir.path().join(".cobble");
172        fs::create_dir_all(&cobble_dir).unwrap();
173        fs::write(
174            cobble_dir.join("build_manifest.json"),
175            r#"{
176                "version": 1,
177                "cobble_version": "0.0.0-test",
178                "minecraft_version": "26.1.2",
179                "pack_format_text": "101.1",
180                "namespace": "inspect",
181                "generated": {"functions": 1, "commands": 1, "total_json_resources": 0, "function_tags": 0},
182                "resources": []
183            }"#,
184        )
185        .unwrap();
186        fs::write(
187            cobble_dir.join("source_map.json"),
188            r#"{"version":1,"entries":[{"generated_path":"data/inspect/function/main.mcfunction","generated_line":1,"command":"say hi","source":null,"kind":"UserCommand"}]}"#,
189        )
190        .unwrap();
191
192        let summary = read_inspection(temp_dir.path()).unwrap();
193
194        assert_eq!(summary.manifest["namespace"], "inspect");
195        assert_eq!(summary.source_map_entries, Some(1));
196    }
197
198    #[test]
199    fn read_inspection_reports_missing_manifest() {
200        let temp_dir = tempfile::TempDir::new().unwrap();
201        let error = read_inspection(temp_dir.path()).unwrap_err();
202        assert!(error.contains("No Cobble build manifest found"));
203    }
204}