use oag_core::ir::{IrParameterLocation, IrReturnType, IrSchema, IrType};
use oag_core::parse;
use oag_core::transform;
const SSE_CHAT: &str = include_str!("fixtures/sse-chat.yaml");
const PETSTORE: &str = include_str!("fixtures/petstore-3.2.yaml");
const MIXED: &str = include_str!("fixtures/mixed-endpoints.yaml");
const ANTHROPIC: &str = include_str!("fixtures/anthropic-messages.yaml");
const PETSTORE_POLY: &str = include_str!("fixtures/petstore-polymorphic.yaml");
const INTEGER_DISC: &str = include_str!("fixtures/integer-discriminator.yaml");
const LITERAL_DEFAULT: &str = include_str!("fixtures/literal-default.yaml");
#[test]
fn transform_sse_chat() {
let spec = parse::from_yaml(SSE_CHAT).unwrap();
let ir = transform::transform(&spec).unwrap();
assert_eq!(ir.info.title, "AI Chat API");
assert!(!ir.schemas.is_empty());
assert!(!ir.operations.is_empty());
let stream_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "createChatCompletionStream")
.expect("should have createChatCompletionStream");
match &stream_op.return_type {
IrReturnType::Sse(sse) => {
assert!(sse.event_type_name.is_some(), "should have event type name");
assert_eq!(sse.variants.len(), 2, "should have 2 SSE event variants");
assert!(
!sse.also_has_json,
"stream-only endpoint should not have JSON"
);
}
_ => panic!("expected SSE return type"),
}
let dual_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "createChatCompletion")
.expect("should have createChatCompletion");
match &dual_op.return_type {
IrReturnType::Sse(sse) => {
assert!(sse.also_has_json, "dual endpoint should have JSON");
assert!(sse.json_response.is_some());
}
_ => panic!("expected SSE return type for dual endpoint"),
}
}
#[test]
fn transform_petstore() {
let spec = parse::from_yaml(PETSTORE).unwrap();
let ir = transform::transform(&spec).unwrap();
assert_eq!(ir.info.title, "Petstore");
let pet = ir.schemas.iter().find(|s| s.name().pascal_case == "Pet");
assert!(pet.is_some(), "should have Pet schema");
match pet.unwrap() {
IrSchema::Object(obj) => {
assert!(obj.fields.len() >= 3);
}
_ => panic!("Pet should be an object schema"),
}
let status = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "PetStatus");
assert!(status.is_some(), "should have PetStatus schema");
match status.unwrap() {
IrSchema::Enum(e) => {
assert_eq!(e.variants.len(), 3);
}
_ => panic!("PetStatus should be an enum"),
}
assert!(!ir.operations.is_empty());
let list_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "listPets")
.expect("should have listPets");
assert_eq!(list_op.parameters.len(), 2);
let delete_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "deletePet")
.expect("should have deletePet");
match &delete_op.return_type {
IrReturnType::Void => {}
_ => panic!("deletePet should return void"),
}
}
#[test]
fn transform_mixed() {
let spec = parse::from_yaml(MIXED).unwrap();
let ir = transform::transform(&spec).unwrap();
let stream_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "streamEvents")
.expect("should have streamEvents");
match &stream_op.return_type {
IrReturnType::Sse(sse) => {
assert!(!sse.also_has_json);
assert!(
sse.event_type_name.is_none(),
"no itemSchema oneOf, so no event type name"
);
}
_ => panic!("expected SSE return type"),
}
let list_items = ir
.operations
.iter()
.find(|op| op.name.camel_case == "listItems")
.expect("should have listItems");
let tags_param = list_items
.parameters
.iter()
.find(|p| p.original_name == "tags")
.unwrap();
assert_eq!(tags_param.style, Some("form".to_string()));
assert_eq!(tags_param.explode, Some(false));
let filter_param = list_items
.parameters
.iter()
.find(|p| p.original_name == "filter")
.unwrap();
assert_eq!(filter_param.style, Some("deepObject".to_string()));
assert_eq!(filter_param.explode, Some(true));
let page_param = list_items
.parameters
.iter()
.find(|p| p.original_name == "page")
.unwrap();
assert_eq!(page_param.style, None);
assert_eq!(page_param.explode, None);
}
#[test]
fn transform_202_with_body_is_mapped() {
let spec = parse::from_yaml(MIXED).unwrap();
let ir = transform::transform(&spec).unwrap();
let enqueue = ir
.operations
.iter()
.find(|op| op.name.camel_case == "enqueueJob")
.expect("should have enqueueJob");
match &enqueue.return_type {
IrReturnType::Standard(resp) => {
assert!(
matches!(&resp.response_type, IrType::Ref(name) if name == "Job"),
"enqueueJob (202) should return the Job body, got {:?}",
resp.response_type
);
assert_eq!(
resp.status_code, 202,
"the 202 status code must be retained so server stubs stay compliant"
);
}
other => panic!("enqueueJob should map its 202 body, got {other:?}"),
}
let delete = ir
.operations
.iter()
.find(|op| op.name.camel_case == "deleteItem")
.expect("should have deleteItem");
assert!(
matches!(delete.return_type, IrReturnType::Void),
"deleteItem (204, no body) should remain void"
);
}
fn return_type_for(responses_yaml: &str) -> IrReturnType {
let spec = format!(
r#"
openapi: "3.2.0"
info:
title: T
version: "1.0"
paths:
/thing:
get:
operationId: getThing
responses:
{responses_yaml}
components:
schemas:
Thing:
type: object
properties:
id:
type: string
"#
);
let parsed = parse::from_yaml(&spec).unwrap();
let ir = transform::transform(&parsed).unwrap();
ir.operations
.into_iter()
.find(|op| op.name.camel_case == "getThing")
.expect("should have getThing")
.return_type
}
#[test]
fn transform_200_body_is_retained() {
let rt = return_type_for(
r##" "200":
description: ok
content:
application/json:
schema:
$ref: "#/components/schemas/Thing""##,
);
assert!(
matches!(&rt, IrReturnType::Standard(r) if matches!(&r.response_type, IrType::Ref(n) if n == "Thing")),
"200 with a body should return the body type, got {rt:?}"
);
}
#[test]
fn transform_body_bearing_2xx_wins_over_empty_200() {
let rt = return_type_for(
r##" "200":
description: accepted, no content
"202":
description: queued
content:
application/json:
schema:
$ref: "#/components/schemas/Thing""##,
);
assert!(
matches!(&rt, IrReturnType::Standard(r) if matches!(&r.response_type, IrType::Ref(n) if n == "Thing")),
"a bodyless 200 must not hide a 202 body, got {rt:?}"
);
}
#[test]
fn transform_all_bodyless_2xx_is_void() {
let rt = return_type_for(
r##" "200":
description: ok
"204":
description: no content"##,
);
assert!(
matches!(rt, IrReturnType::Void),
"bodyless success responses should be void, got {rt:?}"
);
}
#[test]
fn transform_modules_grouping() {
let spec = parse::from_yaml(SSE_CHAT).unwrap();
let ir = transform::transform(&spec).unwrap();
assert!(
ir.modules.len() >= 2,
"should have at least chat and models modules"
);
let chat_module = ir
.modules
.iter()
.find(|m| m.name.original == "chat")
.expect("should have chat module");
assert!(!chat_module.operations.is_empty());
let models_module = ir
.modules
.iter()
.find(|m| m.name.original == "models")
.expect("should have models module");
assert!(!models_module.operations.is_empty());
}
#[test]
fn transform_request_body() {
let spec = parse::from_yaml(SSE_CHAT).unwrap();
let ir = transform::transform(&spec).unwrap();
let create_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "createChatCompletion")
.expect("should have createChatCompletion");
let body = create_op
.request_body
.as_ref()
.expect("should have request body");
assert!(body.required);
assert_eq!(body.content_type, "application/json");
}
#[test]
fn transform_void_response() {
let spec = parse::from_yaml(SSE_CHAT).unwrap();
let ir = transform::transform(&spec).unwrap();
let feedback_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "submitFeedback")
.expect("should have submitFeedback");
match &feedback_op.return_type {
IrReturnType::Void => {}
_ => panic!("submitFeedback should return void"),
}
}
#[test]
fn transform_anthropic_messages() {
let spec = parse::from_yaml(ANTHROPIC).unwrap();
let ir = transform::transform(&spec).unwrap();
assert_eq!(ir.info.title, "Anthropic Messages API");
let content_block = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "ContentBlock")
.expect("should have ContentBlock schema");
match content_block {
IrSchema::Union(u) => {
assert_eq!(u.variants.len(), 4, "ContentBlock should have 4 variants");
assert!(
matches!(&u.variants[0], IrType::Ref(name) if name == "TextBlock"),
"first variant should be Ref(TextBlock)"
);
assert!(
matches!(&u.variants[1], IrType::Ref(name) if name == "ImageBlock"),
"second variant should be Ref(ImageBlock)"
);
let disc = u.discriminator.as_ref().expect("should have discriminator");
assert_eq!(disc.property_name, "type");
assert_eq!(disc.mapping.len(), 4);
assert_eq!(
disc.mapping.iter().find(|(k, _)| k == "text").unwrap().1,
"TextBlock"
);
assert_eq!(
disc.mapping.iter().find(|(k, _)| k == "image").unwrap().1,
"ImageBlock"
);
}
_ => panic!("ContentBlock should be a Union"),
}
let stream_delta = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "StreamDelta")
.expect("should have StreamDelta schema");
match stream_delta {
IrSchema::Union(u) => {
assert_eq!(u.variants.len(), 2, "StreamDelta should have 2 variants");
assert!(
matches!(&u.variants[0], IrType::Ref(name) if name == "TextDelta"),
"first variant should be Ref(TextDelta)"
);
assert!(
matches!(&u.variants[1], IrType::Ref(name) if name == "InputJsonDelta"),
"second variant should be Ref(InputJsonDelta)"
);
let disc = u.discriminator.as_ref().expect("should have discriminator");
assert_eq!(disc.property_name, "type");
}
_ => panic!("StreamDelta should be a Union"),
}
let tool_result_content = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "ToolResultContent")
.expect("should have ToolResultContent schema");
match tool_result_content {
IrSchema::Union(u) => {
assert_eq!(
u.variants.len(),
2,
"ToolResultContent should have 2 variants"
);
assert!(
matches!(&u.variants[0], IrType::Ref(name) if name == "TextBlock"),
"first variant should be Ref(TextBlock)"
);
assert!(
matches!(&u.variants[1], IrType::Ref(name) if name == "ImageBlock"),
"second variant should be Ref(ImageBlock)"
);
assert!(
u.discriminator.is_none(),
"anyOf should have no discriminator"
);
}
_ => panic!("ToolResultContent should be a Union"),
}
let msg_start = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "MessageStartEvent")
.expect("should have MessageStartEvent schema");
match msg_start {
IrSchema::Object(obj) => {
assert!(
obj.fields.iter().any(|f| f.original_name == "type"),
"should have type field"
);
let message_field = obj
.fields
.iter()
.find(|f| f.original_name == "message")
.expect("should have message field");
assert!(
matches!(&message_field.field_type, IrType::Ref(name) if name == "MessageResponse"),
"message field should reference MessageResponse"
);
}
_ => panic!("MessageStartEvent should be an Object (merged allOf)"),
}
let text_block = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "TextBlock")
.expect("should have TextBlock schema");
match text_block {
IrSchema::Object(obj) => {
let type_field = obj
.fields
.iter()
.find(|f| f.original_name == "type")
.expect("TextBlock should have type field");
assert_eq!(
type_field.field_type,
IrType::StringLiteral("text".to_string()),
"TextBlock.type should be StringLiteral(\"text\")"
);
}
_ => panic!("TextBlock should be an Object"),
}
let msg_resp = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "MessageResponse")
.expect("should have MessageResponse schema");
match msg_resp {
IrSchema::Object(obj) => {
let id_field = obj
.fields
.iter()
.find(|f| f.original_name == "id")
.expect("MessageResponse should have id field");
assert!(id_field.read_only, "MessageResponse.id should be readOnly");
}
_ => panic!("MessageResponse should be an Object"),
}
let create_msg = ir
.operations
.iter()
.find(|op| op.name.camel_case == "createMessage")
.expect("should have createMessage operation");
let header_param = create_msg
.parameters
.iter()
.find(|p| p.original_name == "anthropic-version")
.expect("should have anthropic-version parameter");
assert_eq!(header_param.location, IrParameterLocation::Header);
match &create_msg.return_type {
IrReturnType::Sse(sse) => {
assert_eq!(sse.variants.len(), 8, "should have 8 SSE event variants");
assert!(sse.also_has_json, "dual endpoint should have JSON");
assert!(sse.json_response.is_some());
}
_ => panic!("createMessage should have SSE return type"),
}
let cancel_batch = ir
.operations
.iter()
.find(|op| op.name.camel_case == "cancelBatch")
.expect("should have cancelBatch operation");
match &cancel_batch.return_type {
IrReturnType::Void => {}
_ => panic!("cancelBatch should return Void"),
}
let stop_reason = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "StopReason")
.expect("should have StopReason schema");
match stop_reason {
IrSchema::Enum(e) => {
assert_eq!(e.variants.len(), 4, "StopReason should have 4 variants");
}
_ => panic!("StopReason should be an Enum"),
}
assert!(
ir.modules.len() >= 3,
"should have at least 3 modules (messages, models, and tokens or batches), got {}",
ir.modules.len()
);
}
#[test]
fn transform_petstore_polymorphic() {
let spec = parse::from_yaml(PETSTORE_POLY).unwrap();
let ir = transform::transform(&spec).unwrap();
assert_eq!(ir.info.title, "Petstore (Polymorphic)");
let pet = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "Pet")
.expect("should have Pet schema");
match pet {
IrSchema::Union(u) => {
assert_eq!(u.variants.len(), 2, "Pet should have 2 variants");
assert!(
matches!(&u.variants[0], IrType::Ref(name) if name == "Cat"),
"first variant should be Ref(Cat)"
);
assert!(
matches!(&u.variants[1], IrType::Ref(name) if name == "Dog"),
"second variant should be Ref(Dog)"
);
let disc = u.discriminator.as_ref().expect("should have discriminator");
assert_eq!(disc.property_name, "petType");
assert_eq!(disc.mapping.len(), 2);
assert_eq!(
disc.mapping.iter().find(|(k, _)| k == "cat").unwrap().1,
"Cat"
);
assert_eq!(
disc.mapping.iter().find(|(k, _)| k == "dog").unwrap().1,
"Dog"
);
}
_ => panic!("Pet should be a Union"),
}
let cat = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "Cat")
.expect("should have Cat schema");
match cat {
IrSchema::Object(obj) => {
let pet_type_field = obj
.fields
.iter()
.find(|f| f.original_name == "petType")
.expect("Cat should have petType field");
assert_eq!(
pet_type_field.field_type,
IrType::StringLiteral("cat".to_string()),
"Cat.petType should be StringLiteral(\"cat\")"
);
assert!(
obj.fields.iter().any(|f| f.original_name == "huntingSkill"),
"Cat should have huntingSkill field"
);
}
_ => panic!("Cat should be an Object"),
}
let dog = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "Dog")
.expect("should have Dog schema");
match dog {
IrSchema::Object(obj) => {
let pet_type_field = obj
.fields
.iter()
.find(|f| f.original_name == "petType")
.expect("Dog should have petType field");
assert_eq!(
pet_type_field.field_type,
IrType::StringLiteral("dog".to_string()),
"Dog.petType should be StringLiteral(\"dog\")"
);
}
_ => panic!("Dog should be an Object"),
}
let ext_err = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "ExtendedErrorModel")
.expect("should have ExtendedErrorModel schema");
match ext_err {
IrSchema::Alias(alias) => match &alias.target {
IrType::Intersection(parts) => {
assert_eq!(parts.len(), 2, "should have 2 intersection parts");
assert!(
matches!(&parts[0], IrType::Ref(name) if name == "ErrorModel"),
"first part should be Ref(ErrorModel)"
);
match &parts[1] {
IrType::Object(fields) => {
assert!(
fields.iter().any(|(name, _, _)| name == "rootCause"),
"should have rootCause field from extension"
);
}
_ => panic!("second part should be Object"),
}
}
_ => panic!("ExtendedErrorModel target should be Intersection"),
},
_ => panic!("ExtendedErrorModel should be an Alias (allOf with $ref)"),
}
assert!(!ir.operations.is_empty());
let list_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "listPets")
.expect("should have listPets");
assert_eq!(list_op.parameters.len(), 1);
let create_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "createPet")
.expect("should have createPet");
assert!(create_op.request_body.is_some());
let get_op = ir
.operations
.iter()
.find(|op| op.name.camel_case == "getPet")
.expect("should have getPet");
assert_eq!(get_op.parameters.len(), 1); }
#[test]
fn integer_const_produces_integer_literal() {
let spec = parse::from_yaml(INTEGER_DISC).unwrap();
let ir = transform::transform(&spec).unwrap();
let circle = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "Circle")
.expect("should have Circle schema");
match circle {
IrSchema::Object(obj) => {
let shape_type = obj
.fields
.iter()
.find(|f| f.original_name == "shapeType")
.expect("Circle should have shapeType field");
assert_eq!(
shape_type.field_type,
IrType::IntegerLiteral(0),
"Circle.shapeType should be IntegerLiteral(0)"
);
}
_ => panic!("Circle should be an Object"),
}
let square = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "Square")
.expect("should have Square schema");
match square {
IrSchema::Object(obj) => {
let shape_type = obj
.fields
.iter()
.find(|f| f.original_name == "shapeType")
.expect("Square should have shapeType field");
assert_eq!(
shape_type.field_type,
IrType::IntegerLiteral(1),
"Square.shapeType should be IntegerLiteral(1)"
);
}
_ => panic!("Square should be an Object"),
}
}
#[test]
fn literal_default_keeps_literals_present_and_preserves_default() {
let spec = parse::from_yaml(LITERAL_DEFAULT).unwrap();
let ir = transform::transform(&spec).unwrap();
let message = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "Message")
.expect("should have Message schema");
let IrSchema::Object(obj) = message else {
panic!("Message should be an Object");
};
let field = |name: &str| {
obj.fields
.iter()
.find(|f| f.original_name == name)
.unwrap_or_else(|| panic!("Message should have {name} field"))
};
let type_field = field("type");
assert_eq!(
type_field.field_type,
IrType::StringLiteral("message".to_string())
);
assert!(!type_field.required, "type is not in `required`");
assert_eq!(
type_field.default_repr,
Some(IrType::StringLiteral("message".to_string())),
"literal default value should be preserved on the field"
);
let version = field("version");
assert_eq!(version.field_type, IrType::IntegerLiteral(1));
assert_eq!(version.default_repr, Some(IrType::IntegerLiteral(1)));
let object_type = field("objectType");
assert_eq!(
object_type.field_type,
IrType::StringLiteral("list".to_string())
);
assert_eq!(
object_type.default_repr,
Some(IrType::StringLiteral("list".to_string()))
);
let content = field("content");
assert!(content.required, "content is the only required field");
assert_eq!(content.field_type, IrType::String);
assert_eq!(content.default_repr, None);
let max_tokens = field("maxTokens");
assert_eq!(max_tokens.field_type, IrType::Integer);
assert!(!max_tokens.required);
let temperature = field("temperature");
assert_eq!(temperature.field_type, IrType::Number);
assert!(!temperature.required);
assert_eq!(
temperature.default_repr, None,
"a float default is not a string/integer literal"
);
let role = field("role");
assert!(
matches!(role.field_type, IrType::Union(_)),
"multi-value enum should be a Union, not a single literal"
);
assert!(!role.required);
let text_block = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "TextBlock")
.expect("should have TextBlock schema");
let IrSchema::Object(tb) = text_block else {
panic!("TextBlock should be an Object");
};
let tb_type = tb
.fields
.iter()
.find(|f| f.original_name == "type")
.expect("TextBlock should have type field");
assert_eq!(
tb_type.field_type,
IrType::StringLiteral("text".to_string())
);
assert!(tb_type.required, "TextBlock.type is in `required`");
assert_eq!(
tb_type.default_repr,
Some(IrType::StringLiteral("text".to_string()))
);
}
#[test]
fn integer_discriminator_mapping_keys_preserved() {
let spec = parse::from_yaml(INTEGER_DISC).unwrap();
let ir = transform::transform(&spec).unwrap();
let shape = ir
.schemas
.iter()
.find(|s| s.name().pascal_case == "Shape")
.expect("should have Shape schema");
match shape {
IrSchema::Union(u) => {
let disc = u.discriminator.as_ref().expect("should have discriminator");
assert_eq!(disc.property_name, "shapeType");
assert_eq!(disc.mapping.len(), 2);
assert_eq!(
disc.mapping.iter().find(|(k, _)| k == "0").unwrap().1,
"Circle"
);
assert_eq!(
disc.mapping.iter().find(|(k, _)| k == "1").unwrap().1,
"Square"
);
}
_ => panic!("Shape should be a Union"),
}
}