xidl-parser 0.97.0

A IDL codegen.
Documentation
use super::*;
use crate::hir::{Annotation, AnnotationParams, ConstExpr, Literal};

fn builtin(name: &str, raw: &str) -> Annotation {
    Annotation::Builtin {
        name: name.to_string(),
        params: Some(AnnotationParams::Raw(raw.to_string())),
    }
}

fn string(value: &str) -> ConstExpr {
    ConstExpr::Literal(Literal::StringLiteral(format!("\"{value}\"")))
}

fn positional(name: &str, values: Vec<ConstExpr>) -> Annotation {
    Annotation::Builtin {
        name: name.to_string(),
        params: Some(AnnotationParams::Positional(values)),
    }
}

#[test]
fn deprecated_info_normalizes_dates_and_validates_ranges() {
    assert_eq!(deprecated_info(&[]).unwrap(), None);

    let info = deprecated_info(&[builtin(
        "deprecated",
        r#"since="2026-01-02", after="2026-01-03""#,
    )])
    .expect("deprecated info")
    .expect("present");
    assert!(info.deprecated);
    assert!(info.since.is_some());
    assert!(info.after.is_some());

    let value_alias = deprecated_info(&[builtin("deprecated", r#""2026-01-05T01:02:03Z""#)])
        .expect("deprecated value alias")
        .expect("present");
    assert_eq!(value_alias.since.as_deref(), Some("2026-01-05T01:02:03Z"));
    assert_eq!(value_alias.after, None);

    let err = deprecated_info(&[builtin(
        "deprecated",
        r#"since="2026-01-04", after="2026-01-03""#,
    )])
    .expect_err("invalid range");
    assert!(err.contains("since <= after"));

    let err = deprecated_info(&[builtin("deprecated", r#"since="not-a-date""#)])
        .expect_err("invalid timestamp");
    assert!(err.contains("invalid @deprecated timestamp literal"));
}

#[test]
fn validate_http_annotations_checks_security_and_media_type() {
    validate_http_annotations(
        "operation 'ok'",
        &[
            positional(
                "cors",
                vec![
                    string("https://app.example.com"),
                    string("https://admin.example.com"),
                ],
            ),
            builtin("request", r#""json""#),
            builtin("response", r#""msgpack""#),
        ],
    )
    .expect("valid annotation set");

    let err = validate_http_annotations("operation 'bad'", &[builtin("response", r#""xml""#)])
        .expect_err("bad media type");
    assert!(err.contains(
        "unsupported @response(\"xml\") format, expected 'json', 'urlencoded', or 'msgpack'"
    ));

    let err = validate_http_annotations(
        "operation 'mime'",
        &[builtin("request", r#""application/json""#)],
    )
    .expect_err("mime string media type");
    assert!(err.contains("unsupported @request(\"application/json\") format"));

    let err = validate_http_annotations(
        "operation 'plain'",
        &[builtin("response", r#""text/plain""#)],
    )
    .expect_err("explicit text/plain");
    assert!(err.contains("unsupported @response(\"text/plain\") format"));

    let err = validate_http_annotations(
        "operation 'secure'",
        &[builtin("no_security", ""), builtin("http_basic", "")],
    )
    .expect_err("bad security");
    assert!(err.contains("operation 'secure': @no_security cannot be combined"));

    let err = validate_http_annotations(
        "operation 'cors'",
        &[builtin("cors", r#"origin="https://app.example.com""#)],
    )
    .expect_err("bad cors");
    assert!(err.contains("@cors only accepts comma-separated string literals"));

    validate_http_annotations(
        "operation 'empty'",
        &[builtin("response", ""), Annotation::Final],
    )
    .expect("empty media annotation should be ignored");
}