xidl-parser 0.105.0

A IDL codegen.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

fn unique_temp_dir(name: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let path = std::env::temp_dir().join(format!(
        "xidl-parser-include-{name}-{}-{nanos}",
        std::process::id()
    ));
    fs::create_dir_all(&path).expect("create temp dir");
    path
}

fn write_file(path: &Path, source: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent dir");
    }
    fs::write(path, source).expect("write fixture");
}

fn parse_hir(path: &Path) -> xidl_parser::error::ParserResult<xidl_parser::hir::Specification> {
    let source = fs::read_to_string(path).expect("read fixture");
    let typed = xidl_parser::parser::parser_text_with_resolver(
        &source,
        Some(path.to_str().unwrap()),
        &mut xidl_parser::hir::FsIncludeResolver,
    )?;
    xidl_parser::hir::Specification::from_typed_ast_with_path(typed, path)
}

/// Parses `path` with `mounts` available to dependency `#include` paths.
fn parse_hir_with_mounts(
    path: &Path,
    mounts: std::collections::BTreeMap<String, PathBuf>,
) -> xidl_parser::error::ParserResult<xidl_parser::hir::Specification> {
    let source = fs::read_to_string(path).expect("read fixture");
    let typed = xidl_parser::parser::parser_text_with_resolver(
        &source,
        Some(path.to_str().unwrap()),
        &mut xidl_parser::hir::MountIncludeResolver::new(mounts),
    )?;
    xidl_parser::hir::Specification::from_typed_ast_with_path(typed, path)
}

#[test]
fn test_hir_include_inserts_definitions_in_order() {
    let root = unique_temp_dir("ordered");
    let main = root.join("main.idl");
    let shared = root.join("shared.idl");

    write_file(
        &main,
        r#"
        struct Before {
            int32 value;
        };

        #include "shared.idl"

        struct After {
            int32 value;
        };
        "#,
    );
    write_file(
        &shared,
        r#"
        struct Shared {
            int32 value;
        };
        "#,
    );

    let hir = parse_hir(&main).unwrap();
    insta::assert_debug_snapshot!("hir__include_ordered", hir);
}

#[test]
fn test_hir_nested_include_preserves_depth_first_order() {
    let root = unique_temp_dir("nested");
    let main = root.join("main.idl");
    let middle = root.join("parts/middle.idl");
    let leaf = root.join("parts/leaf.idl");

    write_file(
        &main,
        r#"
        struct Start {
            int32 value;
        };

        #include "parts/middle.idl"

        struct End {
            int32 value;
        };
        "#,
    );
    write_file(
        &middle,
        r#"
        struct Middle {
            int32 value;
        };

        #include "leaf.idl"
        "#,
    );
    write_file(
        &leaf,
        r#"
        struct Leaf {
            int32 value;
        };
        "#,
    );

    let hir = parse_hir(&main).unwrap();
    insta::assert_debug_snapshot!("hir__include_nested", hir);
}

#[test]
fn test_hir_include_missing_file_errors() {
    let root = unique_temp_dir("missing");
    let main = root.join("main.idl");

    write_file(&main, "#include \"missing.idl\"\n");

    let err = parse_hir(&main).expect_err("missing include should fail");
    assert!(err.to_string().contains("does not exist"));
    assert!(err.to_string().contains("missing.idl"));
}

#[test]
fn test_hir_include_invalid_ast_errors() {
    let root = unique_temp_dir("invalid");
    let main = root.join("main.idl");
    let invalid = root.join("invalid.idl");

    write_file(&main, "#include \"invalid.idl\"\n");
    write_file(&invalid, "struct Broken { int32 value \n");

    let err = parse_hir(&main).expect_err("invalid include should fail");
    assert!(err.to_string().contains("failed to parse include"));
    assert!(err.to_string().contains("invalid.idl"));
}

#[test]
fn test_hir_include_cycle_errors() {
    let root = unique_temp_dir("cycle");
    let a = root.join("a.idl");
    let b = root.join("b.idl");

    write_file(&a, "#include \"b.idl\"\n");
    write_file(&b, "#include \"a.idl\"\n");

    let err = parse_hir(&a).expect_err("cyclic include should fail");
    assert!(err.to_string().contains("cyclic include detected"));
    assert!(err.to_string().contains("a.idl"));
    assert!(err.to_string().contains("b.idl"));
}

#[test]
fn http_includes_share_compiler_builtins_without_adding_runtime_models() {
    let root = unique_temp_dir("http-builtins");
    let main = root.join("main.idl");
    write_file(&main, "#include \"first.idl\"\n#include \"second.idl\"\n");
    write_file(
        &root.join("first.idl"),
        "@http union First switch(ContentType) { case Json: string value; };",
    );
    write_file(
        &root.join("second.idl"),
        "@http union Second switch(ContentType) { case Text: string value; };",
    );
    let spec = parse_hir(&main).expect("two included HTTP unions");
    assert_eq!(spec.0.len(), 2, "only user declarations are retained");
    let projected = xidl_parser::rest_hir::project(&spec).expect("HTTP projection");
    let media_types = projected
        .document
        .http_unions
        .iter()
        .flat_map(|union| &union.cases)
        .map(|case| case.content_type.as_str())
        .collect::<Vec<_>>();
    assert_eq!(media_types, ["application/json", "text/plain"]);
    fs::remove_dir_all(root).expect("remove include fixtures");
}

#[test]
fn dependency_mount_resolves_nested_include() {
    let root = unique_temp_dir("mount");
    let main = root.join("main.idl");
    let package = root.join("pkgs/timestamp-1.2.3");

    write_file(
        &main,
        r#"
        #include "timestamp/lib.idl"
        "#,
    );
    write_file(
        &package.join("lib.idl"),
        r#"
        struct Stamp {
            int32 value;
        };
        "#,
    );

    let mounts = std::collections::BTreeMap::from([("timestamp".to_string(), package.clone())]);
    let spec = parse_hir_with_mounts(&main, mounts).expect("mounted include resolves");
    assert_eq!(spec.0.len(), 1, "the included declaration is available");
    fs::remove_dir_all(root).expect("remove mount fixtures");
}

#[test]
fn dependency_mount_prefers_the_longest_matching_name() {
    let root = unique_temp_dir("mount-longest");
    let main = root.join("main.idl");
    let outer = root.join("pkgs/xidl");
    let inner = root.join("pkgs/xidl-std");

    write_file(&main, "#include \"xidl/std/lib.idl\"\n");
    write_file(&outer.join("std.idl"), "struct Wrong { int32 value; };\n");
    write_file(
        &outer.join("std/lib.idl"),
        "struct AlsoWrong { int32 value; };\n",
    );
    write_file(&inner.join("lib.idl"), "struct Right { int32 value; };\n");

    let mounts = std::collections::BTreeMap::from([
        ("xidl".to_string(), outer),
        ("xidl/std".to_string(), inner),
    ]);
    let spec = parse_hir_with_mounts(&main, mounts).expect("mounted include resolves");
    let ident = format!("{:?}", spec.0.first());
    assert!(ident.contains("Right"), "expected {ident} to name Right");
    fs::remove_dir_all(root).expect("remove mount fixtures");
}

#[test]
fn dependency_mount_falls_back_to_the_filesystem() {
    let root = unique_temp_dir("mount-fallback");
    let main = root.join("main.idl");

    write_file(&main, "#include \"sibling.idl\"\n");
    write_file(
        &root.join("sibling.idl"),
        "struct Sibling { int32 value; };\n",
    );

    let mounts =
        std::collections::BTreeMap::from([("timestamp".to_string(), root.join("missing-package"))]);
    let spec = parse_hir_with_mounts(&main, mounts).expect("sibling include still resolves");
    assert_eq!(spec.0.len(), 1);
    fs::remove_dir_all(root).expect("remove mount fixtures");
}

#[test]
fn dependency_mount_names_the_mount_when_the_file_is_missing() {
    let root = unique_temp_dir("mount-missing");
    let main = root.join("main.idl");

    write_file(&main, "#include \"timestamp/lib.idl\"\n");

    let mounts = std::collections::BTreeMap::from([(
        "timestamp".to_string(),
        root.join("pkgs/timestamp-1.2.3"),
    )]);
    let err = parse_hir_with_mounts(&main, mounts).expect_err("missing mount target should fail");
    assert!(err.to_string().contains("timestamp/lib.idl"), "{err}");
    assert!(err.to_string().contains("the 'timestamp' mount"), "{err}");
    fs::remove_dir_all(root).expect("remove mount fixtures");
}