vexil-codegen-rust 0.4.3

Rust code generation backend for the Vexil schema compiler
Documentation
//! Integration tests for multi-file project compilation + codegen.
//!
//! For each corpus project:
//! 1. Load root file, create FilesystemLoader with project dir as include root
//! 2. Compile project via compile_project()
//! 3. Assert no error-level diagnostics
//! 4. Generate code for each schema
//! 5. Verify generated code is non-empty and contains the expected header
//!
//! NOTE: Corpus projects currently avoid transitive type references in fields
//! (e.g., `Graph { left: LeftNode }` where `LeftNode` itself references `Id`
//! from a third schema). This is a known limitation in `clone_types_into` —
//! TypeIds from transitive deps are not remapped, which can cause false
//! "infinite recursion" errors during type-checking. Once transitive type
//! cloning is implemented, the corpus should be updated to exercise that path.

use std::path::PathBuf;
use vexil_lang::diagnostic::Severity;
use vexil_lang::resolve::FilesystemLoader;

fn corpus_dir() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .parent()
        .unwrap()
        .join("corpus")
        .join("projects")
}

fn compile_and_generate(project_name: &str, root_ns: &str, expected_schema_count: usize) {
    let project_dir = corpus_dir().join(project_name);

    // Build root file path from namespace: "simple.main" -> "simple/main.vexil"
    let root_segments: Vec<&str> = root_ns.split('.').collect();
    let mut root_path = project_dir.clone();
    for seg in &root_segments {
        root_path.push(seg);
    }
    root_path.set_extension("vexil");

    let source = std::fs::read_to_string(&root_path)
        .unwrap_or_else(|e| panic!("failed to read {}: {e}", root_path.display()));

    let loader = FilesystemLoader::new(vec![project_dir]);

    let result = vexil_lang::compile_project(&source, &root_path, &loader)
        .unwrap_or_else(|e| panic!("compile_project failed for {project_name}: {e}"));

    let errors: Vec<_> = result
        .diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Error)
        .collect();
    assert!(
        errors.is_empty(),
        "unexpected errors in {project_name}: {errors:?}"
    );

    assert_eq!(
        result.schemas.len(),
        expected_schema_count,
        "{project_name}: expected {expected_schema_count} schemas, got {}",
        result.schemas.len()
    );

    // Generate code for each schema — assert no codegen errors.
    for (ns, compiled) in &result.schemas {
        let code = vexil_codegen_rust::generate(compiled)
            .unwrap_or_else(|e| panic!("codegen failed for {ns}: {e}"));
        assert!(!code.is_empty(), "empty codegen output for {ns}");
        assert!(
            code.contains("// Code generated by vexilc"),
            "missing header in {ns}"
        );
    }

    eprintln!(
        "{project_name}: {} schemas compiled and generated successfully",
        result.schemas.len()
    );
}

/// Simple two-file project: main imports types.
#[test]
fn project_simple() {
    compile_and_generate("simple", "simple.main", 2);
}

/// Diamond dependency: root -> left + right -> base.
/// Verifies diamond dedup (base compiled only once).
#[test]
fn project_diamond() {
    compile_and_generate("diamond", "diamond.root", 4);
}

/// Mixed project with enum + message across three files.
#[test]
fn project_mixed() {
    compile_and_generate("mixed", "mix.app", 3);
}