xidl-parser 0.105.0

A IDL codegen.
Documentation
use crate::{hir, parser::parser_text, rest_hir::semantics::*};

fn annotations(source: &str) -> Vec<hir::Annotation> {
    let spec = hir::Specification::from_typed_ast_with_properties_and_path(
        parser_text(&format!("{source} interface Api {{}};")).unwrap(),
        [("expand_interface".into(), false.into())].into(),
        "security.idl",
    )
    .unwrap();
    let hir::Definition::InterfaceDcl(interface) = &spec.0[0] else {
        panic!("interface")
    };
    interface.annotations.clone()
}

#[test]
fn explicit_security_overrides_and_inheritance_agree_for_both_public_views() {
    for (interface, method, expected, origin) in [
        ("", "", None, None),
        (
            "@http_basic",
            "",
            Some(vec![HttpSecurityRequirement::HttpBasic]),
            Some(HttpSecurityOrigin::Interface),
        ),
        (
            "@http_basic",
            "@no_security",
            Some(vec![]),
            Some(HttpSecurityOrigin::Method),
        ),
        (
            "@no_security",
            "",
            Some(vec![]),
            Some(HttpSecurityOrigin::Interface),
        ),
        (
            "@http_basic",
            "@http_bearer",
            Some(vec![HttpSecurityRequirement::HttpBearer]),
            Some(HttpSecurityOrigin::Method),
        ),
    ] {
        let interface = annotations(interface);
        let method = annotations(method);
        assert_eq!(effective_security(&interface, &method).unwrap(), expected);
        let located = effective_security_with_origin(&interface, &method).unwrap();
        assert_eq!(located.as_ref().map(|profile| profile.origin), origin);
        assert_eq!(located.map(|profile| profile.requirements), expected);
    }
    for invalid in [
        "@api_key(in=\"body\", name=\"token\")",
        "@http_basic @no_security",
    ] {
        let error = effective_security(&annotations(invalid), &[]).unwrap_err();
        assert!(
            error.to_string().contains(if invalid.starts_with("@api") {
                "header|query|cookie"
            } else {
                "cannot be combined"
            }),
            "{error}"
        );
    }
}

#[test]
fn websocket_options_accept_real_codecs_and_reject_invalid_handshake_contracts() {
    for (codec, expected) in [
        ("json", WebSocketCodec::Json),
        ("msgpack", WebSocketCodec::Msgpack),
        ("bytes", WebSocketCodec::Bytes),
    ] {
        let values = annotations(&format!(
            r#"@upgrade(protocol="websocket",codec="{codec}",subprotocol="events.v1",heartbeat="20s",max_message=4096)"#
        ));
        assert_eq!(
            parse_upgrade_protocol(&values).unwrap().as_deref(),
            Some("websocket")
        );
        let config = parse_websocket_config(&values, UpgradeMode::WebSocket)
            .unwrap()
            .unwrap();
        assert_eq!(config.codec, expected);
        assert_eq!(config.subprotocol.as_deref(), Some("events.v1"));
        assert_eq!(config.heartbeat_ms, Some(20_000));
        assert_eq!(config.max_message_bytes, Some(4096));
    }
    for (argument, fragment) in [
        ("codec=\"unknown\"", "unsupported"),
        ("subprotocol=\"bad token\"", "RFC 6455"),
        ("heartbeat=\"\"", "cannot be empty"),
        ("max_message=\"many\"", "positive integer"),
        ("max_message=0", ">= 1"),
        ("unknown=1", "unknown parameter"),
    ] {
        let values = annotations(&format!("@upgrade(protocol=\"websocket\",{argument})"));
        let error = parse_websocket_config(&values, UpgradeMode::WebSocket).unwrap_err();
        assert!(error.contains(fragment), "{argument}: {error}");
    }
    for (argument, fragment) in [
        ("codec=\"json\"", "only valid"),
        ("unknown=1", "unknown parameter"),
    ] {
        let values = annotations(&format!("@upgrade(protocol=\"fastnet\",{argument})"));
        assert!(
            parse_websocket_config(&values, UpgradeMode::Raw)
                .unwrap_err()
                .contains(fragment)
        );
    }
    assert!(
        parse_websocket_config(
            &annotations("@upgrade(protocol=\"fastnet\")"),
            UpgradeMode::Raw
        )
        .unwrap()
        .is_none()
    );
    assert!(
        parse_websocket_config(&[], UpgradeMode::WebSocket)
            .unwrap()
            .is_none()
    );
    assert!(
        parse_upgrade_protocol(&annotations("@upgrade(codec=\"json\")"))
            .unwrap_err()
            .contains("requires a protocol")
    );
}