use crate::error::ParserResult;
use crate::hir::{self, Specification};
use std::collections::HashMap;
#[test]
fn runtime_declaration_uses_the_ordinary_idl_parser() {
let definitions = super::HttpBuiltins::load().expect("parse shared runtime declaration");
let [
hir::Definition::TypeDcl(hir::TypeDcl::ConstrTypeDcl(hir::ConstrTypeDcl::EnumDcl(
enumeration,
))),
] = definitions.as_slice()
else {
panic!("runtime declaration must be a single ordinary enum");
};
assert_eq!(enumeration.ident, "ContentType");
assert_eq!(enumeration.member.len(), 3);
for member in &enumeration.member {
let value = xidl_http::ContentType::from_idl_name(&member.ident)
.expect("IDL member exists in runtime");
assert_eq!(
hir::field_rename(&member.annotations).as_deref(),
Some(value.as_str())
);
}
}
fn lower(source: &str) -> ParserResult<Specification> {
Specification::from_typed_ast_with_properties_and_path(
crate::parser::parser_text(source)?,
HashMap::from([("hir_kind".to_string(), "http".into())]),
"builtin-test.idl",
)
}
#[test]
fn builtin_stays_out_of_user_models_and_survives_hir_round_trip() {
let spec = lower(
r#"
module api {
@http union First switch(ContentType) { case Json: string value; };
@http union Second switch(::ContentType) {
case ::ContentType::OctetStream: sequence<octet> bytes;
case ContentType::Text: string text;
};
};
"#,
)
.expect("implicit builtin");
let enums = spec
.0
.iter()
.filter_map(|definition| match definition {
hir::Definition::TypeDcl(hir::TypeDcl::ConstrTypeDcl(hir::ConstrTypeDcl::EnumDcl(
e,
))) => Some(e),
_ => None,
})
.collect::<Vec<_>>();
assert!(
enums.is_empty(),
"compiler declarations are not generated models"
);
assert_eq!(spec.0.len(), 1, "only the user module is retained");
let serialized = serde_json::to_string(&spec).expect("serialize HIR");
let restored: Specification = serde_json::from_str(&serialized).expect("restore HIR");
let document = crate::rest_hir::project(&restored).expect("project restored HIR");
let cases = document
.document
.http_unions
.iter()
.flat_map(|u| &u.cases)
.map(|case| case.content_type.as_str())
.collect::<Vec<_>>();
assert_eq!(
cases,
["application/json", "application/octet-stream", "text/plain"]
);
assert_eq!(serde_json::to_string(&document.spec).unwrap(), serialized);
}
#[test]
fn rejects_unsupported_idl_value_references_to_runtime_builtins() {
for declaration in [
"struct Meta { ::ContentType media; };",
"struct Meta { sequence<ContentType> media; };",
"struct Meta { map<string, ContentType> media; };",
"struct Meta { optional<ContentType> media; };",
"struct Meta { string media[ContentType::Json]; };",
"struct Meta { string<ContentType::Json> media; };",
"struct Meta : ContentType { string value; };",
"typedef ContentType Media;",
"const ContentType media = ContentType::Json;",
"const long media = ContentType::Json;",
"exception Failure { ContentType media; };",
"interface Api { ContentType read(); };",
"interface Api { void write(in ContentType media); };",
"interface Api { readonly attribute ContentType media; };",
"interface Api { attribute ContentType media; };",
"interface Api { void read() raises(ContentType); };",
"interface Api { typedef ContentType Media; };",
"interface Api { exception Failure { ContentType media; }; };",
"union Ordinary switch(ContentType) { case Json: string value; };",
"union Ordinary switch(long) { case ContentType::Json: string value; };",
"@http union Other switch(ContentType) { case Json: ContentType value; };",
] {
let source = format!(
"@http union Payload switch(ContentType) {{ case Json: string value; }}; {declaration}"
);
let error = lower(&source).expect_err("IDL does not support external runtime values");
assert!(
error
.to_string()
.contains("ordinary IDL values cannot reference runtime types"),
"{declaration}: {error}"
);
}
}
#[test]
fn runtime_types_can_shadow_the_builtin_in_modules_and_interfaces() {
let spec = lower(
r#"
@http union Payload switch(ContentType) { case Json: string value; };
module models {
enum ContentType { Custom, };
struct Meta { ContentType media; };
typedef ContentType Media;
const ContentType media = ContentType::Custom;
union Ordinary switch(ContentType) { case Custom: string value; };
};
interface Api {
enum ContentType { Local, };
ContentType read(in ContentType value);
attribute ContentType media;
};
"#,
)
.expect("user runtime types keep their lexical bindings");
assert_eq!(spec.0.len(), 3);
crate::rest_hir::project(&spec).expect("projection retains the same lexical bindings");
}
#[test]
fn interface_expansion_preserves_source_scope_validation() {
let source = r#"
@http union Payload switch(ContentType) { case Json: string value; };
interface Api {
enum ContentType { Custom, };
ContentType read(in ContentType value);
attribute ContentType media;
};
"#;
let typed = crate::parser::parser_text(source).expect("valid source");
Specification::from_typed_ast_with_path(typed, "interface-local.idl")
.expect("source scope is checked before generated wrappers are added");
for expand in [false, true] {
Specification::project_typed_ast_with_properties_and_path(
crate::parser::parser_text(source).expect("valid source"),
HashMap::from([
("hir_kind".to_string(), "http".into()),
("expand_interface".to_string(), expand.into()),
]),
"interface-local.idl",
)
.expect(
"projection collects representations without rechecking generated wrappers as source",
);
}
}
#[test]
fn rejects_http_unions_inside_interfaces_before_transport_projection() {
let error = lower(
"interface Api { @http union Payload switch(ContentType) { case Text: string value; }; Payload read(); };"
).expect_err("interface-local models are not supported by HTTP generators");
assert!(
error
.to_string()
.contains("declare it at module or root scope"),
"{error}"
);
}
#[test]
fn ordinary_unions_do_not_load_or_reserve_content_type() {
let spec = lower("enum ContentType { Custom, }; union Ordinary switch(ContentType) { case Custom: string value; default: long other; };")
.expect("ordinary union rules unchanged");
assert_eq!(spec.0.len(), 2);
let doc = crate::rest_hir::project(&spec).expect("ordinary union");
assert!(doc.document.http_unions.is_empty());
}
#[test]
fn rejects_user_root_content_type_declarations() {
for declaration in [
"enum ContentType { Json, };",
"struct ContentType { string value; };",
"typedef long ContentType;",
"typedef enum ContentType { Json, } Alias;",
"native ContentType;",
"module ContentType { struct Value { string value; }; };",
"const long ContentType = 1;",
"exception ContentType { string value; };",
"interface ContentType {};",
] {
let source = format!(
"{declaration} @http union Payload switch(::ContentType) {{ case Json: string value; }};"
);
let error = lower(&source).expect_err("reserved root declaration");
assert!(
error.to_string().contains("built-in root declaration"),
"{declaration}: {error}"
);
}
}
#[test]
fn local_shadow_requires_explicit_root_discriminator() {
for declaration in [
"enum ContentType { Json, };",
"struct ContentType { string value; };",
"typedef long ContentType;",
"typedef enum ContentType { Json, } Alias;",
"native ContentType;",
"module ContentType { struct Value { string value; }; };",
"const long ContentType = 1;",
"exception ContentType { string value; };",
"interface ContentType {};",
] {
let source = format!(
"module api {{ {declaration} @http union Payload switch(ContentType) {{ case Json: string value; }}; }};"
);
let error = lower(&source).expect_err("local declaration shadows builtin");
assert!(
error
.to_string()
.contains("must resolve to the built-in ::ContentType"),
"{declaration}: {error}"
);
lower(&source.replace("switch(ContentType)", "switch(::ContentType)"))
.expect("explicit root discriminator bypasses local shadow");
}
}
#[test]
fn http_unions_reject_custom_discriminators_and_open_or_ambiguous_cases() {
for (source, expected) in [
(
"enum Mime { Json, }; @http union Payload switch(Mime) { case Json: string value; };",
"must resolve to the built-in ::ContentType",
),
(
"@http union Payload switch(long) { case 0: string value; };",
"must be the built-in ::ContentType",
),
(
"@http union Payload switch(ContentType) { default: string value; };",
"no default",
),
(
"@http union Payload switch(ContentType) { case Json: case Text: string value; };",
"exactly one",
),
(
"@http union Payload switch(ContentType) { case Json: string a; case ::ContentType::Json: string b; };",
"duplicate ContentType case 'Json'",
),
(
"@http union Payload switch(ContentType) { case Missing: string value; };",
"must name a member",
),
(
"enum Other { Json, }; @http union Payload switch(ContentType) { case Other::Json: string value; };",
"must name a member",
),
(
"@http union Payload switch(ContentType) { case 1: string value; };",
"case labels must name enum members",
),
(
"module api { enum ContentType { Json, }; @http union Payload switch(::ContentType) { case ContentType::Json: string value; }; };",
"must name a member",
),
] {
let error = lower(source).expect_err("invalid unused HTTP union declaration");
assert!(error.to_string().contains(expected), "{source}: {error}");
}
}