xidl-parser 0.105.0

A IDL codegen.
Documentation
use super::*;
use crate::error::ParseError;
use crate::hir::{ConstrTypeDcl, Definition, TypeDcl, TypeSpec};
use crate::versioning::VersioningError;

const VERSIONED: &str = r#"
#pragma xidlc package versioned

@version
enum Versions {
    @rename("1.0") v1,
    @rename("2.0") v2,
    v3,
};

struct Item {
    string id;
};

struct Pet {
    string id;
    ::Item item;
    @added(Versions::v2)
    @removed(Versions::v3)
    int32 age;
    @added(Versions::v3)
    @rename("age")
    int32 age2_;
};
"#;

#[test]
fn projection_selects_members_per_version() {
    let (spec, record) = analyze(VERSIONED);
    let mut signatures = Vec::new();
    for version in project_ids(&record) {
        let projected = record.project(&spec, version).expect("projection");
        let ident = record
            .timeline()
            .get(version)
            .expect("version definition")
            .ident
            .clone();
        signatures.push((ident, struct_members(&projected, "Pet")));
    }

    assert_eq!(
        signatures,
        vec![
            ("v1".to_string(), vec!["id".to_string(), "item".to_string()]),
            (
                "v2".to_string(),
                vec!["id".to_string(), "item".to_string(), "age".to_string()]
            ),
            (
                "v3".to_string(),
                vec!["id".to_string(), "item".to_string(), "age2_".to_string()]
            ),
        ]
    );
}

#[test]
fn projection_wraps_the_version_and_sets_the_document_version() {
    let (spec, record) = analyze(VERSIONED);
    let version = project_ids(&record)[0];
    let projected = record.project(&spec, version).expect("projection");

    assert_eq!(projected_module(&projected).ident, "v1");
    assert_eq!(version_pragma(&projected).as_deref(), Some("1.0"));
}

#[test]
fn projection_removes_the_version_enum_and_lifecycle_annotations() {
    let (spec, record) = analyze(VERSIONED);
    let version = project_ids(&record)[1];
    let projected = record.project(&spec, version).expect("projection");
    let json = serde_json::to_string(&projected).expect("serialize");

    assert!(!has_enum(&projected, "Versions"));
    assert!(!json.contains("\"Added\""));
    assert!(!json.contains("\"Removed\""));
    assert!(!json.contains("\"Version\""));
}

#[test]
fn projection_makes_root_references_relative() {
    let (spec, record) = analyze(VERSIONED);
    let version = project_ids(&record)[0];
    let projected = record.project(&spec, version).expect("projection");

    let item = projected_module(&projected)
        .definition
        .iter()
        .find_map(|definition| match definition {
            Definition::TypeDcl(TypeDcl::ConstrTypeDcl(ConstrTypeDcl::StructDcl(value)))
                if value.ident == "Pet" =>
            {
                value.member.iter().find_map(|member| match &member.ty {
                    TypeSpec::ScopedName(name) if name.name == ["Item"] => Some(name.clone()),
                    _ => None,
                })
            }
            _ => None,
        })
        .expect("Pet.item should reference Item");

    assert!(!item.is_root);
}

#[test]
fn projection_reports_references_to_removed_declarations() {
    let (spec, record) = analyze(
        r#"
@version
enum Versions { v1, v2 };

@removed(Versions::v2)
struct Old {
    string id;
};

struct Pet {
    Old old;
};
"#,
    );
    let versions = project_ids(&record);

    record.project(&spec, versions[0]).expect("v1 keeps Old");
    match record.project(&spec, versions[1]) {
        Err(ParseError::Versioning(VersioningError::DanglingReference {
            decl,
            reference,
            version,
        })) => {
            assert!(decl.contains("Pet"));
            assert_eq!(reference, "Old");
            assert_eq!(version, "v2");
        }
        other => panic!("expected a dangling reference error, got {other:?}"),
    }
}

#[test]
fn projection_prunes_raises_entries_of_removed_exceptions() {
    let (spec, record) = analyze(
        r#"
@version
enum Versions { v1, v2 };

@http(404)
exception NotFound {
    string id;
};

@added(Versions::v2)
@http(409)
exception Conflict {
    string reason;
};

interface Pets {
    @post(path = "/pets/{id}")
    void adopt(@path string id) raises(NotFound, Conflict);
};
"#,
    );
    let versions = project_ids(&record);
    let v1 = record.project(&spec, versions[0]).expect("v1 projection");
    let v2 = record.project(&spec, versions[1]).expect("v2 projection");

    assert_eq!(raised_exceptions(&v1), vec!["NotFound"]);
    assert_eq!(raised_exceptions(&v2), vec!["NotFound", "Conflict"]);
}

#[test]
fn projection_drops_empty_raises_clauses_before_idl_printing() {
    let source = r#"
@version
enum Versions { v1, v2 };

exception Current { string id; };

@added(Versions::v2)
exception Future { string reason; };

interface Api {
    void call() raises(Future);
    readonly attribute string status raises(Future);
    attribute string value getraises(Future) setraises(Current);
    attribute string get_only getraises(Current) setraises(Future);
    attribute string write_only setraises(Future);
};
"#;
    let (spec, record) = analyze(source);
    let version = project_ids(&record)[0];
    let projected = record.project(&spec, version).expect("v1 projection");
    let printed = crate::hir::printer::IdlPrinter::render(&projected);
    let reparsed = parse(&printed);

    assert!(!printed.contains("raises()"), "{printed}");
    assert!(!printed.contains("getraises()"), "{printed}");
    assert!(!printed.contains("setraises()"), "{printed}");
    assert!(printed.contains("attribute string value setraises(Current);"));
    assert!(printed.contains("attribute string get_only getraises(Current);"));
    assert_eq!(
        serde_json::to_value(&reparsed).expect("serialize reparsed"),
        serde_json::to_value(&projected).expect("serialize projected")
    );
}

#[test]
fn merge_keeps_one_module_per_version_in_order() {
    let (spec, record) = analyze(VERSIONED);
    let versions = project_ids(&record);
    let merged = record.merge(&spec, &versions).expect("merge");

    let modules = merged
        .0
        .iter()
        .filter_map(|definition| match definition {
            Definition::ModuleDcl(module) => Some(module.ident.clone()),
            _ => None,
        })
        .collect::<Vec<_>>();
    assert_eq!(modules, vec!["v1", "v2", "v3"]);

    let pragmas = merged
        .0
        .iter()
        .filter(|definition| matches!(definition, Definition::Pragma(_)))
        .count();
    assert_eq!(pragmas, 1);
    assert!(version_pragma(&merged).is_none());
}

#[test]
fn merge_respects_the_selected_subset() {
    let (spec, record) = analyze(VERSIONED);
    let versions = project_ids(&record);
    let merged = record
        .merge(&spec, &[versions[2], versions[0]])
        .expect("merge");

    let modules = merged
        .0
        .iter()
        .filter_map(|definition| match definition {
            Definition::ModuleDcl(module) => Some(module.ident.clone()),
            _ => None,
        })
        .collect::<Vec<_>>();
    assert_eq!(modules, vec!["v3", "v1"]);
}

#[test]
fn projection_snapshot_locks_the_desugaring() {
    let (spec, record) = analyze(VERSIONED);
    let versions = project_ids(&record);
    let projected = record.project(&spec, versions[1]).expect("projection");

    insta::assert_debug_snapshot!("projection_v2", projected);
}