xidl-parser 0.72.0

A IDL codegen.
Documentation
use super::context::{ConstContext, TemplateContext};
use super::render::render_template;
use super::types::{
    idl_type_spec, qualified_exception_name, render_const_expr, scoped_name_to_idl,
};
use super::*;

fn scoped(parts: &[&str], is_root: bool) -> ScopedName {
    ScopedName {
        name: parts.iter().map(|part| (*part).to_string()).collect(),
        is_root,
    }
}

fn int(value: &str) -> ConstExpr {
    ConstExpr::Literal(Literal::IntegerLiteral(IntegerLiteral(value.to_string())))
}

fn scoped_expr(parts: &[&str], is_root: bool) -> ConstExpr {
    ConstExpr::ScopedName(scoped(parts, is_root))
}

fn wrap_unary(op: UnaryOperator, value: ConstExpr) -> ConstExpr {
    ConstExpr::UnaryExpr(op, Box::new(value))
}

fn literal(value: Literal) -> ConstExpr {
    ConstExpr::Literal(value)
}

fn nested(value: ConstExpr) -> ConstExpr {
    ConstExpr::UnaryExpr(UnaryOperator::Add, Box::new(value))
}

fn binary(op: BinaryOperator, left: ConstExpr, right: ConstExpr) -> ConstExpr {
    ConstExpr::BinaryExpr(op, Box::new(left), Box::new(right))
}

fn parse_interface(source: &str) -> InterfaceDcl {
    let typed = crate::parser::parser_text(source).expect("parse should succeed");
    let spec = crate::hir::Specification::from_typed_ast_with_properties(
        typed,
        [("expand_interface".to_string(), serde_json::json!(false))]
            .into_iter()
            .collect(),
    );
    match spec.0.into_iter().next().expect("interface") {
        Definition::InterfaceDcl(value) => value,
        other => panic!("expected interface, got {other:?}"),
    }
}

#[test]
fn renders_idl_types_and_expressions() {
    let rendered = [
        render_const_expr(&binary(BinaryOperator::Or, int("1"), int("2"))),
        render_const_expr(&binary(BinaryOperator::Xor, int("1"), int("2"))),
        render_const_expr(&binary(BinaryOperator::And, int("1"), int("2"))),
        render_const_expr(&binary(BinaryOperator::LeftShift, int("1"), int("2"))),
        render_const_expr(&binary(BinaryOperator::RightShift, int("4"), int("1"))),
        render_const_expr(&binary(BinaryOperator::Add, int("1"), int("2"))),
        render_const_expr(&binary(BinaryOperator::Sub, int("3"), int("1"))),
        render_const_expr(&binary(BinaryOperator::Mult, int("2"), int("3"))),
        render_const_expr(&binary(BinaryOperator::Div, int("6"), int("2"))),
        render_const_expr(&binary(BinaryOperator::Mod, int("7"), int("3"))),
        render_const_expr(&wrap_unary(UnaryOperator::Not, int("1"))),
        render_const_expr(&scoped_expr(&["demo", "VALUE"], true)),
        render_const_expr(&nested(int("8"))),
        render_const_expr(&literal(Literal::FloatingPtLiteral(FloatingPtLiteral {
            sign: Some(IntegerSign::Minus),
            integer: DecNumber("1".to_string()),
            fraction: DecNumber("5".to_string()),
        }))),
        render_const_expr(&literal(Literal::CharLiteral("'a'".to_string()))),
        render_const_expr(&literal(Literal::WideCharacterLiteral("L'a'".to_string()))),
        render_const_expr(&literal(Literal::StringLiteral("\"hi\"".to_string()))),
        render_const_expr(&literal(Literal::WideStringLiteral("L\"hi\"".to_string()))),
        render_const_expr(&literal(Literal::BooleanLiteral(true))),
    ];

    for token in ["|", "^", "&", "<<", ">>", "+", "-", "*", "/", "%", "~"] {
        assert!(
            rendered.iter().any(|value| value.contains(token)),
            "{token}"
        );
    }
    for value in [
        "'a'",
        "L'a'",
        "\"hi\"",
        "L\"hi\"",
        "true",
        "-1.5",
        "::demo::VALUE",
    ] {
        assert!(rendered.iter().any(|item| item == value), "{value}");
    }

    let rendered_types = [
        TypeSpec::IntegerType(IntegerType::Char),
        TypeSpec::IntegerType(IntegerType::UChar),
        TypeSpec::FloatingPtType,
        TypeSpec::CharType,
        TypeSpec::WideCharType,
        TypeSpec::Boolean,
        TypeSpec::AnyType,
        TypeSpec::ObjectType,
        TypeSpec::ValueBaseType,
        TypeSpec::ScopedName(scoped(&["demo", "Thing"], true)),
        TypeSpec::WideStringType(WideStringType { bound: None }),
        TypeSpec::MapType(MapType {
            key: Box::new(TypeSpec::CharType),
            value: Box::new(TypeSpec::Boolean),
            len: None,
        }),
        TypeSpec::TemplateType(TemplateType {
            ident: "Box".to_string(),
            args: vec![TypeSpec::IntegerType(IntegerType::I32), TypeSpec::CharType],
        }),
    ]
    .iter()
    .map(idl_type_spec)
    .collect::<Vec<_>>();

    for value in [
        "double",
        "int8",
        "uint8",
        "char",
        "wchar",
        "boolean",
        "any",
        "Object",
        "ValueBase",
        "::demo::Thing",
        "wstring",
        "map<char, boolean>",
        "Box<long, char>",
    ] {
        assert!(rendered_types.iter().any(|item| item == value), "{value}");
    }
    assert_eq!(scoped_name_to_idl(&scoped(&["Problem"], false)), "Problem");
    assert_eq!(
        qualified_exception_name(&scoped(&["Problem"], false), &["demo".to_string()]),
        "demo::Problem"
    );
    assert_eq!(
        qualified_exception_name(&scoped(&["Root", "Problem"], true), &["demo".to_string()]),
        "Root::Problem"
    );
}

#[test]
fn collects_operations_and_expands_interface() {
    let interface = parse_interface(
        r#"
        interface Service {
            readonly attribute boolean ready;
            readonly attribute boolean check raises(ReadError);
            attribute long value getraises(GetError) setraises(SetError);
            attribute long return_ setraises(SetOnly);
            void ping();
            long compute(in long input, out long return_) raises(Failure, ::RootFailure);
        };
        "#,
    );

    let InterfaceDclInner::InterfaceDef(def) = &interface.decl else {
        panic!("expected interface def");
    };
    let body = def.interface_body.as_ref().expect("body");
    let ops = collect_operations(body);
    assert!(ops.iter().any(|op| op.name == "get_attribute_ready"));
    assert!(ops.iter().any(|op| op.name == "set_attribute_value"));
    assert!(ops.iter().any(|op| op.name == "compute"));

    let compute = ops.iter().find(|op| op.name == "compute").expect("compute");
    let ctx = compute.to_context();
    assert!(
        ctx.out_members
            .iter()
            .any(|member| member.name == "return_1")
    );
    assert!(
        ctx.result_exceptions
            .iter()
            .any(|value| value.ty == "::RootFailure")
    );

    let expanded = expand_interface(&interface, &["demo".to_string()]).expect("expand");
    assert!(!expanded.is_empty());
}

#[test]
fn renders_templates_and_reports_missing_files() {
    let ctx = TemplateContext {
        modules: vec!["demo".to_string()],
        interface_name: "Service".to_string(),
        consts: vec![ConstContext {
            name: "SERVICE_PING_HASH".to_string(),
            value: "1".to_string(),
        }],
        operations: Vec::new(),
    };

    let rendered = render_template("interface.idl.j2", &ctx).expect("template");
    assert!(rendered.contains("module demo"));

    let err = render_template("missing.idl.j2", &ctx).expect_err("missing template must fail");
    assert!(err.to_string().contains("missing template"));
}