concinnity-dev 0.19.119

The Concinnity dev tooling library: world authoring, the in-engine editor, the debug server, docs and packaging
//! Validate a world JSONL without producing blob files.
//!
//! Runs the validation front half of the build pipeline (load, expand, and
//! the semantic checks in `crate::check`) and reports the outcome. Used by
//! `cn test`.

/// Read `world_path`, run validation, and report results. Returns Ok if every
/// asset passes; otherwise an error whose Display contains a human-readable
/// summary of every failure (one per asset).
pub(crate) fn check_at_path(world_path: &str) -> std::io::Result<()> {
    let content = std::fs::read_to_string(world_path)?;
    let source = concinnity_cook::WorldSource::file(&content, std::path::Path::new(world_path));
    check_source(source, world_path)
}

/// Run validation against world text. `label` is the origin used in messages
/// (typically the source path).
fn check_source(source: concinnity_cook::WorldSource<'_>, label: &str) -> std::io::Result<()> {
    match concinnity_cook::prepare_world(source, crate::project::assets_dir().as_deref()) {
        Ok(loaded) => {
            println!("ok: {} asset(s) passed in {}", loaded.assets.len(), label);
            Ok(())
        }
        Err(errors) => Err(report_validation_errors(&errors)),
    }
}

/// Print each validation error in CLI form and collapse them into a single
/// io::Error naming the count, so a failed world surfaces every problem in one
/// pass.
pub(crate) fn report_validation_errors(errors: &[String]) -> std::io::Error {
    for e in errors {
        eprintln!("error:   {}", e);
    }
    eprintln!("\nvalidation failed ({} error(s))", errors.len());
    validation_failed(errors.len())
}

/// The error a world that failed validation with `count` errors returns, once
/// they have been shown.
pub(crate) fn validation_failed(count: usize) -> std::io::Error {
    std::io::Error::new(
        std::io::ErrorKind::InvalidData,
        format!("validation failed with {count} error(s)"),
    )
}

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

    #[test]
    fn report_validation_errors_names_the_count() {
        let errors = vec!["first".to_string(), "second".to_string()];
        let err = report_validation_errors(&errors);
        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
        assert!(err.to_string().contains("2 error(s)"), "got: {err}");
    }

    #[test]
    fn check_source_accepts_a_valid_world() {
        check_source("[\"PhysicsConfig\",{\"$id\":\"phys\"}]\n".into(), "test").unwrap();
    }

    #[test]
    fn check_source_rejects_an_unknown_type() {
        let err =
            check_source("[\"NotARealAssetType\",{\"$id\":\"odd\"}]\n".into(), "test").unwrap_err();
        assert!(!err.to_string().is_empty());
    }

    #[test]
    fn check_at_path_reports_a_missing_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("missing.jsonl");
        assert!(check_at_path(path.to_str().unwrap()).is_err());
    }

    #[test]
    fn check_at_path_accepts_a_valid_world_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("world.jsonl");
        std::fs::write(&path, "[\"PhysicsConfig\",{\"$id\":\"phys\"}]\n").unwrap();
        check_at_path(path.to_str().unwrap()).unwrap();
    }
}