cobble/commands/
inspect.rs1use 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}