cobble-lang 0.6.3

A modern, Python-like language for creating Minecraft Data Packs
Documentation
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};

pub struct InspectOptions {
    pub input: PathBuf,
    pub json: bool,
}

pub fn inspect(options: InspectOptions) -> Result<(), String> {
    let summary = read_inspection(&options.input)?;

    if options.json {
        let output = serde_json::json!({
            "manifest": summary.manifest,
            "source_map_entries": summary.source_map_entries,
        });
        println!(
            "{}",
            serde_json::to_string_pretty(&output)
                .map_err(|error| format!("Failed to format inspection JSON: {error}"))?
        );
        return Ok(());
    }

    print_inspection(&options.input, &summary);
    Ok(())
}

#[derive(Debug)]
struct InspectionSummary {
    manifest: Value,
    source_map_entries: Option<usize>,
}

fn read_inspection(input: &Path) -> Result<InspectionSummary, String> {
    let manifest_path = input.join(".cobble").join("build_manifest.json");
    let manifest_content = fs::read_to_string(&manifest_path).map_err(|error| {
        format!(
            "No Cobble build manifest found at {}: {error}\n\
            Run `cobble build` on a data pack directory first. ZIP archives do not include .cobble metadata.",
            manifest_path.display()
        )
    })?;
    let manifest = serde_json::from_str::<Value>(&manifest_content)
        .map_err(|error| format!("Failed to parse {}: {error}", manifest_path.display()))?;

    Ok(InspectionSummary {
        source_map_entries: read_source_map_entry_count(input)?,
        manifest,
    })
}

fn read_source_map_entry_count(input: &Path) -> Result<Option<usize>, String> {
    let source_map_path = input.join(".cobble").join("source_map.json");
    if !source_map_path.exists() {
        return Ok(None);
    }

    let content = fs::read_to_string(&source_map_path)
        .map_err(|error| format!("Failed to read {}: {error}", source_map_path.display()))?;
    let source_map = serde_json::from_str::<Value>(&content)
        .map_err(|error| format!("Failed to parse {}: {error}", source_map_path.display()))?;
    Ok(source_map
        .get("entries")
        .and_then(Value::as_array)
        .map(Vec::len))
}

fn print_inspection(input: &Path, summary: &InspectionSummary) {
    let manifest = &summary.manifest;
    println!("Cobble inspect: {}", input.display());
    println!(
        "  Cobble version: {}",
        string_value(manifest, &["cobble_version"]).unwrap_or("unknown")
    );
    println!(
        "  Minecraft target: Java Edition {}",
        string_value(manifest, &["minecraft_version"]).unwrap_or("unknown")
    );
    println!(
        "  Pack format: {}",
        string_value(manifest, &["pack_format_text"]).unwrap_or("unknown")
    );
    println!(
        "  Namespace: {}",
        string_value(manifest, &["namespace"]).unwrap_or("unknown")
    );

    if let Some(input) = manifest.get("input").and_then(Value::as_object) {
        if let Some(source) = input.get("source").and_then(Value::as_str) {
            println!("  Source: {source}");
        }
        if let Some(compiled_files) = input.get("compiled_files").and_then(Value::as_array) {
            println!("  Source files: {}", compiled_files.len());
        }
    }

    println!("Generated:");
    print_usize(manifest, &["generated", "functions"], "  Functions");
    print_usize(manifest, &["generated", "commands"], "  Commands");
    print_usize(
        manifest,
        &["generated", "total_json_resources"],
        "  JSON resources",
    );
    print_usize(manifest, &["generated", "function_tags"], "  Function tags");

    match summary.source_map_entries {
        Some(entries) => println!("  Source map entries: {entries}"),
        None => println!("  Source map entries: none"),
    }

    if let Some(validation) = manifest.get("validation").filter(|value| !value.is_null()) {
        println!("Validation:");
        print_usize(validation, &["commands_checked"], "  Commands checked");
        print_usize(validation, &["errors"], "  Command errors");
        print_usize(validation, &["source_map_errors"], "  Source map errors");
    } else {
        println!("Validation: not recorded");
    }

    if let Some(resources) = manifest.get("resources").and_then(Value::as_array) {
        println!("Resources: {}", resources.len());
        for resource in resources.iter().take(12) {
            let kind = resource
                .get("kind")
                .and_then(Value::as_str)
                .unwrap_or("resource");
            let namespace = resource
                .get("namespace")
                .and_then(Value::as_str)
                .unwrap_or("unknown");
            let path = resource.get("path").and_then(Value::as_str).unwrap_or("");
            println!("  {kind}: {namespace}:{path}");
        }
        if resources.len() > 12 {
            println!("  ... {} more", resources.len() - 12);
        }
    }
}

fn string_value<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> {
    let mut current = value;
    for segment in path {
        current = current.get(*segment)?;
    }
    current.as_str()
}

fn print_usize(value: &Value, path: &[&str], label: &str) {
    let mut current = value;
    for segment in path {
        let Some(next) = current.get(*segment) else {
            return;
        };
        current = next;
    }
    if let Some(number) = current.as_u64() {
        println!("{label}: {number}");
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn read_inspection_loads_manifest_and_source_map_count() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let cobble_dir = temp_dir.path().join(".cobble");
        fs::create_dir_all(&cobble_dir).unwrap();
        fs::write(
            cobble_dir.join("build_manifest.json"),
            r#"{
                "version": 1,
                "cobble_version": "0.0.0-test",
                "minecraft_version": "26.1.2",
                "pack_format_text": "101.1",
                "namespace": "inspect",
                "generated": {"functions": 1, "commands": 1, "total_json_resources": 0, "function_tags": 0},
                "resources": []
            }"#,
        )
        .unwrap();
        fs::write(
            cobble_dir.join("source_map.json"),
            r#"{"version":1,"entries":[{"generated_path":"data/inspect/function/main.mcfunction","generated_line":1,"command":"say hi","source":null,"kind":"UserCommand"}]}"#,
        )
        .unwrap();

        let summary = read_inspection(temp_dir.path()).unwrap();

        assert_eq!(summary.manifest["namespace"], "inspect");
        assert_eq!(summary.source_map_entries, Some(1));
    }

    #[test]
    fn read_inspection_reports_missing_manifest() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let error = read_inspection(temp_dir.path()).unwrap_err();
        assert!(error.contains("No Cobble build manifest found"));
    }
}