use asyncapi_rust::{AsyncApi, ToAsyncApiMessage, schemars::JsonSchema};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
pub enum BasicMessage {
Ping,
Pong,
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
pub enum TaggedMessage {
Echo { text: String },
Broadcast { room: String, text: String },
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "message")]
pub enum RenamedMessage {
#[serde(rename = "user.join")]
UserJoin { username: String },
#[serde(rename = "user.leave")]
UserLeave { username: String },
#[serde(rename = "chat.message")]
ChatMessage { username: String, text: String },
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
pub struct SimpleMessage {
pub id: u64,
pub text: String,
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
pub enum DocumentedMessage {
#[asyncapi(
summary = "User joins a chat room",
description = "Sent when a user enters a room"
)]
Join { username: String, room: String },
#[asyncapi(
summary = "User leaves a chat room",
description = "Sent when a user exits a room",
title = "Leave Room"
)]
Leave { username: String, room: String },
#[asyncapi(content_type = "application/octet-stream")]
File { filename: String, data: Vec<u8> },
#[asyncapi(triggers_binary)]
Binary { data: Vec<u8> },
}
#[test]
fn test_basic_enum_messages() {
let names = BasicMessage::asyncapi_message_names();
assert_eq!(names, vec!["Ping", "Pong"]);
assert_eq!(BasicMessage::asyncapi_message_count(), 2);
assert_eq!(BasicMessage::asyncapi_tag_field(), None);
}
#[test]
fn test_tagged_enum() {
let names = TaggedMessage::asyncapi_message_names();
assert_eq!(names, vec!["Echo", "Broadcast"]);
assert_eq!(TaggedMessage::asyncapi_message_count(), 2);
assert_eq!(TaggedMessage::asyncapi_tag_field(), Some("type"));
}
#[test]
fn test_renamed_enum() {
let names = RenamedMessage::asyncapi_message_names();
assert_eq!(names, vec!["UserJoin", "UserLeave", "ChatMessage"]);
assert_eq!(RenamedMessage::asyncapi_message_count(), 3);
assert_eq!(RenamedMessage::asyncapi_tag_field(), Some("message"));
}
#[test]
fn test_struct_message() {
let names = SimpleMessage::asyncapi_message_names();
assert_eq!(names, vec!["SimpleMessage"]);
assert_eq!(SimpleMessage::asyncapi_message_count(), 1);
assert_eq!(SimpleMessage::asyncapi_tag_field(), None);
}
#[test]
fn test_schema_generation() {
let messages = SimpleMessage::asyncapi_messages();
assert_eq!(messages.len(), 1);
let message = &messages[0];
assert_eq!(message.name, Some("SimpleMessage".to_string()));
assert_eq!(message.content_type, Some("application/json".to_string()));
assert!(message.payload.is_some());
if let Some(schema) = &message.payload {
assert!(matches!(schema, asyncapi_rust::Schema::Object(_)));
}
}
#[test]
fn test_enum_schema_generation() {
let messages = TaggedMessage::asyncapi_messages();
assert_eq!(messages.len(), 2);
let echo = messages
.iter()
.find(|m| m.name.as_deref() == Some("Echo"))
.expect("Echo message should exist");
let broadcast = messages
.iter()
.find(|m| m.name.as_deref() == Some("Broadcast"))
.expect("Broadcast message should exist");
let echo_json = serde_json::to_value(&echo.payload).unwrap();
let broadcast_json = serde_json::to_value(&broadcast.payload).unwrap();
assert!(
echo_json.get("oneOf").is_none(),
"Echo payload must not be the whole-enum oneOf"
);
assert!(
broadcast_json.get("oneOf").is_none(),
"Broadcast payload must not be the whole-enum oneOf"
);
let echo_type_const = echo_json
.pointer("/properties/type/const")
.and_then(|v| v.as_str());
assert_eq!(echo_type_const, Some("Echo"));
let broadcast_type_const = broadcast_json
.pointer("/properties/type/const")
.and_then(|v| v.as_str());
assert_eq!(broadcast_type_const, Some("Broadcast"));
}
#[test]
fn test_per_variant_schema_non_type_tag() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "message")]
pub enum ChannelEvent {
#[serde(rename = "channel-list")]
ChannelList { channels: Vec<String> },
#[serde(rename = "data-changed")]
DataChanged { project_id: i64 },
}
let messages = ChannelEvent::asyncapi_messages();
assert_eq!(messages.len(), 2);
let channel_list = messages
.iter()
.find(|m| m.name.as_deref() == Some("ChannelList"))
.expect("ChannelList should exist");
let data_changed = messages
.iter()
.find(|m| m.name.as_deref() == Some("DataChanged"))
.expect("DataChanged should exist");
let cl_json = serde_json::to_value(&channel_list.payload).unwrap();
let dc_json = serde_json::to_value(&data_changed.payload).unwrap();
assert!(cl_json.get("oneOf").is_none());
assert!(dc_json.get("oneOf").is_none());
assert_eq!(
cl_json
.pointer("/properties/message/const")
.and_then(|v| v.as_str()),
Some("channel-list")
);
assert_eq!(
dc_json
.pointer("/properties/message/const")
.and_then(|v| v.as_str()),
Some("data-changed")
);
assert!(cl_json.pointer("/properties/channels").is_some());
assert!(cl_json.pointer("/properties/project_id").is_none());
}
#[test]
fn test_asyncapi_attributes() {
let messages = DocumentedMessage::asyncapi_messages();
assert_eq!(messages.len(), 4);
let join = &messages[0];
assert_eq!(join.name, Some("Join".to_string()));
assert_eq!(join.summary, Some("User joins a chat room".to_string()));
assert_eq!(
join.description,
Some("Sent when a user enters a room".to_string())
);
assert_eq!(join.content_type, Some("application/json".to_string()));
let leave = &messages[1];
assert_eq!(leave.name, Some("Leave".to_string()));
assert_eq!(leave.title, Some("Leave Room".to_string()));
assert_eq!(leave.summary, Some("User leaves a chat room".to_string()));
assert_eq!(
leave.description,
Some("Sent when a user exits a room".to_string())
);
let file = &messages[2];
assert_eq!(file.name, Some("File".to_string()));
assert_eq!(
file.content_type,
Some("application/octet-stream".to_string())
);
let binary = &messages[3];
assert_eq!(binary.name, Some("Binary".to_string()));
assert_eq!(
binary.content_type,
Some("application/octet-stream".to_string())
);
}
#[derive(AsyncApi)]
#[asyncapi(
title = "Test API",
version = "1.0.0",
description = "A test API specification"
)]
struct TestApi;
#[test]
fn test_asyncapi_derive() {
let spec = TestApi::asyncapi_spec();
assert_eq!(spec.asyncapi, "3.0.0");
assert_eq!(spec.info.title, "Test API");
assert_eq!(spec.info.version, "1.0.0");
assert_eq!(
spec.info.description,
Some("A test API specification".to_string())
);
assert!(spec.servers.is_none());
assert!(spec.channels.is_none());
assert!(spec.operations.is_none());
assert!(spec.components.is_none());
}
#[derive(AsyncApi)]
#[asyncapi(title = "Minimal API", version = "0.1.0")]
struct MinimalApi;
#[test]
fn test_asyncapi_minimal() {
let spec = MinimalApi::asyncapi_spec();
assert_eq!(spec.asyncapi, "3.0.0");
assert_eq!(spec.info.title, "Minimal API");
assert_eq!(spec.info.version, "0.1.0");
assert_eq!(spec.info.description, None);
}
#[allow(clippy::duplicated_attributes)] #[derive(AsyncApi)]
#[asyncapi(
title = "Full API",
version = "1.0.0",
description = "Complete API spec"
)]
#[asyncapi_server(
name = "production",
host = "api.example.com",
protocol = "wss",
description = "Production server"
)]
#[asyncapi_server(name = "development", host = "localhost:8080", protocol = "ws")]
#[asyncapi_channel(name = "chat", address = "/ws/chat")]
#[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat")]
#[asyncapi_operation(name = "receiveMessage", action = "receive", channel = "chat")]
struct FullApi;
#[test]
fn test_asyncapi_full() {
let spec = FullApi::asyncapi_spec();
assert_eq!(spec.info.title, "Full API");
assert_eq!(spec.info.version, "1.0.0");
assert_eq!(spec.info.description, Some("Complete API spec".to_string()));
let servers = spec.servers.expect("Should have servers");
assert_eq!(servers.len(), 2);
let prod_server = servers
.get("production")
.expect("Should have production server");
assert_eq!(prod_server.host, "api.example.com");
assert_eq!(prod_server.protocol, "wss");
assert_eq!(
prod_server.description,
Some("Production server".to_string())
);
let dev_server = servers
.get("development")
.expect("Should have development server");
assert_eq!(dev_server.host, "localhost:8080");
assert_eq!(dev_server.protocol, "ws");
assert_eq!(dev_server.description, None);
let channels = spec.channels.expect("Should have channels");
assert_eq!(channels.len(), 1);
let chat_channel = channels.get("chat").expect("Should have chat channel");
assert_eq!(chat_channel.address, Some("/ws/chat".to_string()));
let operations = spec.operations.expect("Should have operations");
assert_eq!(operations.len(), 2);
let send_op = operations
.get("sendMessage")
.expect("Should have sendMessage operation");
assert!(matches!(
send_op.action,
asyncapi_rust::OperationAction::Send
));
assert_eq!(send_op.channel.reference, "#/channels/chat");
let receive_op = operations
.get("receiveMessage")
.expect("Should have receiveMessage operation");
assert!(matches!(
receive_op.action,
asyncapi_rust::OperationAction::Receive
));
assert_eq!(receive_op.channel.reference, "#/channels/chat");
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
enum ApiMessage {
#[serde(rename = "user.join")]
#[asyncapi(summary = "User joins", description = "User enters a room")]
UserJoin { username: String, room: String },
#[serde(rename = "user.leave")]
#[asyncapi(summary = "User leaves")]
UserLeave { username: String, room: String },
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
enum SystemMessage {
#[serde(rename = "system.status")]
#[asyncapi(summary = "System status")]
SystemStatus { status: String },
}
#[derive(AsyncApi)]
#[asyncapi(title = "Message Integration API", version = "1.0.0")]
#[asyncapi_messages(ApiMessage, SystemMessage)]
struct MessageIntegrationApi;
#[test]
fn test_asyncapi_with_messages() {
let spec = MessageIntegrationApi::asyncapi_spec();
assert_eq!(spec.info.title, "Message Integration API");
assert_eq!(spec.info.version, "1.0.0");
let components = spec.components.expect("Should have components");
let messages = components
.messages
.expect("Should have messages in components");
assert_eq!(messages.len(), 3);
let user_join = messages
.get("UserJoin")
.expect("Should have UserJoin message");
assert_eq!(user_join.name, Some("UserJoin".to_string()));
assert_eq!(user_join.summary, Some("User joins".to_string()));
assert_eq!(
user_join.description,
Some("User enters a room".to_string())
);
assert!(user_join.payload.is_some());
let user_leave = messages
.get("UserLeave")
.expect("Should have UserLeave message");
assert_eq!(user_leave.name, Some("UserLeave".to_string()));
assert_eq!(user_leave.summary, Some("User leaves".to_string()));
let system_status = messages
.get("SystemStatus")
.expect("Should have SystemStatus message");
assert_eq!(system_status.name, Some("SystemStatus".to_string()));
assert_eq!(system_status.summary, Some("System status".to_string()));
}
#[test]
fn test_enum_with_json_value_fields() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
pub enum MsgWithValue {
#[serde(rename = "hello")]
Hello { version: String },
#[serde(rename = "result")]
Result {
ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
payload: Option<serde_json::Value>,
},
}
let messages = MsgWithValue::asyncapi_messages();
assert_eq!(messages.len(), 2);
let hello = messages
.iter()
.find(|m| m.name.as_deref() == Some("Hello"))
.expect("Hello message should exist");
assert!(hello.payload.is_some());
let result = messages
.iter()
.find(|m| m.name.as_deref() == Some("Result"))
.expect("Result message should exist");
assert!(result.payload.is_some());
}
#[test]
fn test_shared_defs_hoisted_to_components_schemas() {
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct SharedInfo {
pub id: i64,
pub label: String,
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
pub enum EventMsg {
#[serde(rename = "event-a")]
EventA { info: SharedInfo },
#[serde(rename = "event-b")]
EventB { info: SharedInfo, extra: String },
}
#[derive(AsyncApi)]
#[asyncapi(title = "Hoisting Test", version = "1.0.0")]
#[asyncapi_messages(EventMsg)]
struct HoistApi;
let schemas = EventMsg::asyncapi_schemas();
assert!(
schemas.contains_key("SharedInfo"),
"SharedInfo must appear in asyncapi_schemas()"
);
let messages = EventMsg::asyncapi_messages();
for msg in &messages {
let payload_json = serde_json::to_value(&msg.payload).unwrap();
assert!(
payload_json.get("$defs").is_none(),
"payload for '{}' must not contain $defs",
msg.name.as_deref().unwrap_or("?")
);
}
fn find_bad_refs(v: &serde_json::Value) -> Vec<String> {
let mut bad = Vec::new();
match v {
serde_json::Value::Object(map) => {
if let Some(r) = map.get("$ref").and_then(|r| r.as_str()) {
if r.starts_with("#/$defs/") {
bad.push(r.to_string());
}
}
for val in map.values() {
bad.extend(find_bad_refs(val));
}
}
serde_json::Value::Array(arr) => {
for val in arr {
bad.extend(find_bad_refs(val));
}
}
_ => {}
}
bad
}
for msg in &messages {
let payload_json = serde_json::to_value(&msg.payload).unwrap();
let bad = find_bad_refs(&payload_json);
assert!(
bad.is_empty(),
"payload for '{}' still has #/$defs/ refs: {:?}",
msg.name.as_deref().unwrap_or("?"),
bad
);
}
let spec = HoistApi::asyncapi_spec();
let comp_schemas = spec
.components
.as_ref()
.and_then(|c| c.schemas.as_ref())
.expect("components.schemas must be populated");
assert!(
comp_schemas.contains_key("SharedInfo"),
"components.schemas must contain SharedInfo"
);
let comp_messages = spec
.components
.as_ref()
.and_then(|c| c.messages.as_ref())
.expect("components.messages must be populated");
for (name, msg) in comp_messages {
let payload_json = serde_json::to_value(&msg.payload).unwrap();
assert!(
payload_json.get("$defs").is_none(),
"components.messages.{name}.payload must not contain $defs"
);
let bad = find_bad_refs(&payload_json);
assert!(
bad.is_empty(),
"components.messages.{name}.payload has unrewritten refs: {bad:?}"
);
}
}
#[test]
fn test_empty_serde_rename_fallback() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "message")]
pub enum ResponseMsg {
#[serde(rename = "ok")]
Ok { value: String },
#[serde(rename = "")]
Empty,
}
let names = ResponseMsg::asyncapi_message_names();
assert!(
names.contains(&"Empty"),
"empty serde rename must fall back to variant ident; got: {names:?}"
);
assert!(
!names.contains(&""),
"empty string must not appear as a message name; got: {names:?}"
);
let messages = ResponseMsg::asyncapi_messages();
assert!(
messages.iter().any(|m| m.name.as_deref() == Some("Empty")),
"Empty message must be findable by variant ident"
);
assert!(
!messages.iter().any(|m| m.name.as_deref() == Some("")),
"empty-string message name must not appear"
);
}
#[test]
fn test_cross_enum_collision_resolved_with_message_name() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "message")]
pub enum Operation {
#[serde(rename = "get-info")]
GetInfo { project_id: i64 },
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "message")]
pub enum OperationResponse {
#[serde(rename = "get-info")]
#[asyncapi(message_name = "GetInfoResponse")]
GetInfo { id: i64, label: String },
}
#[derive(AsyncApi)]
#[asyncapi(title = "Collision Test", version = "1.0.0")]
#[asyncapi_messages(Operation, OperationResponse)]
struct CollisionApi;
let spec = CollisionApi::asyncapi_spec();
let messages = spec
.components
.as_ref()
.and_then(|c| c.messages.as_ref())
.expect("components.messages must be present");
assert!(
messages.contains_key("GetInfo"),
"GetInfo (request) must be in components.messages"
);
assert!(
messages.contains_key("GetInfoResponse"),
"GetInfoResponse must be in components.messages"
);
assert_eq!(
messages.len(),
2,
"both messages must survive — no silent overwrite"
);
let req_json = serde_json::to_value(&messages["GetInfo"].payload).unwrap();
let res_json = serde_json::to_value(&messages["GetInfoResponse"].payload).unwrap();
assert!(
req_json.pointer("/properties/project_id").is_some(),
"GetInfo payload must have project_id"
);
assert!(
res_json.pointer("/properties/label").is_some(),
"GetInfoResponse payload must have label"
);
assert_eq!(
req_json
.pointer("/properties/message/const")
.and_then(|v| v.as_str()),
Some("get-info"),
"GetInfo discriminant must be 'get-info'"
);
assert_eq!(
res_json
.pointer("/properties/message/const")
.and_then(|v| v.as_str()),
Some("get-info"),
"GetInfoResponse discriminant must be 'get-info'"
);
}
#[test]
fn test_message_name_override_attribute() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
pub enum Event {
#[serde(rename = "editor-update")]
#[asyncapi(message_name = "EditorUpdate", summary = "Editor state changed")]
Update { content: String },
}
let names = Event::asyncapi_message_names();
assert_eq!(names, vec!["EditorUpdate"]);
let messages = Event::asyncapi_messages();
let msg = &messages[0];
assert_eq!(msg.name.as_deref(), Some("EditorUpdate"));
assert_eq!(msg.summary.as_deref(), Some("Editor state changed"));
let payload_json = serde_json::to_value(&msg.payload).unwrap();
assert_eq!(
payload_json
.pointer("/properties/type/const")
.and_then(|v| v.as_str()),
Some("editor-update")
);
}
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
enum RoundtripMsg {
#[serde(rename = "ping")]
#[asyncapi(summary = "Ping")]
Ping { seq: u32 },
#[serde(rename = "pong")]
#[asyncapi(summary = "Pong")]
Pong { seq: u32, latency_ms: u32 },
}
#[derive(AsyncApi)]
#[asyncapi(
title = "Roundtrip API",
version = "2.0.0",
description = "Used for roundtrip tests"
)]
#[asyncapi_server(name = "prod", host = "ws.example.com", protocol = "wss")]
#[asyncapi_channel(name = "ping", address = "/ws/ping")]
#[allow(clippy::duplicated_attributes)]
#[asyncapi_operation(name = "sendPing", action = "send", channel = "ping")]
#[asyncapi_operation(name = "receivePong", action = "receive", channel = "ping")]
#[asyncapi_messages(RoundtripMsg)]
struct RoundtripApi;
#[test]
fn test_full_spec_roundtrip() {
use asyncapi_rust::AsyncApiSpec;
let original = RoundtripApi::asyncapi_spec();
let json = serde_json::to_string_pretty(&original).expect("serialization failed");
let restored: AsyncApiSpec =
serde_json::from_str(&json).expect("deserialization failed — JSON was:\n{json}");
assert_eq!(restored.info.title, "Roundtrip API");
assert_eq!(restored.info.version, "2.0.0");
assert_eq!(
restored.info.description,
Some("Used for roundtrip tests".to_string())
);
let servers = restored
.servers
.as_ref()
.expect("servers must survive roundtrip");
assert!(servers.contains_key("prod"), "prod server must survive");
assert_eq!(servers["prod"].protocol, "wss");
let channels = restored
.channels
.as_ref()
.expect("channels must survive roundtrip");
assert!(channels.contains_key("ping"));
assert_eq!(channels["ping"].address, Some("/ws/ping".to_string()));
let ops = restored
.operations
.as_ref()
.expect("operations must survive roundtrip");
assert_eq!(ops.len(), 2);
assert!(matches!(
ops["sendPing"].action,
asyncapi_rust::OperationAction::Send
));
assert!(matches!(
ops["receivePong"].action,
asyncapi_rust::OperationAction::Receive
));
let msgs = restored
.components
.as_ref()
.and_then(|c| c.messages.as_ref())
.expect("components.messages must survive roundtrip");
assert!(msgs.contains_key("Ping"), "Ping message must survive");
assert!(msgs.contains_key("Pong"), "Pong message must survive");
assert!(msgs["Ping"].payload.is_some(), "Ping payload must survive");
assert!(msgs["Pong"].payload.is_some(), "Pong payload must survive");
assert_eq!(msgs["Ping"].summary.as_deref(), Some("Ping"));
let rejson = serde_json::to_string(&restored).expect("re-serialization failed");
assert!(rejson.contains("Roundtrip API"));
assert!(rejson.contains("wss"));
}
#[test]
fn test_schema_any_roundtrip() {
use asyncapi_rust::Schema;
let true_schema: Schema =
serde_json::from_value(serde_json::json!(true)).expect("true should deserialize");
assert!(
matches!(true_schema, Schema::Any(_)),
"boolean true must deserialize as Schema::Any"
);
let false_schema: Schema =
serde_json::from_value(serde_json::json!(false)).expect("false should deserialize");
assert!(
matches!(false_schema, Schema::Any(_)),
"boolean false must deserialize as Schema::Any"
);
let json_val = serde_json::to_value(&true_schema).unwrap();
assert_eq!(json_val, serde_json::json!(true));
let num_schema: Schema =
serde_json::from_value(serde_json::json!(42)).expect("number should deserialize");
assert!(
matches!(num_schema, Schema::Any(_)),
"number must deserialize as Schema::Any"
);
let any = Schema::Any(serde_json::json!({"x-custom": "extension-only schema"}));
let serialized = serde_json::to_value(&any).unwrap();
assert_eq!(serialized["x-custom"], "extension-only schema");
}
#[test]
fn test_schema_any_in_generated_messages() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "type")]
pub enum MsgWithAny {
#[serde(rename = "typed")]
Typed { id: u32 },
#[serde(rename = "open")]
Open {
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
},
}
let messages = MsgWithAny::asyncapi_messages();
assert_eq!(messages.len(), 2);
let open_msg = messages
.iter()
.find(|m| m.name.as_deref() == Some("Open"))
.expect("Open message must exist");
assert!(open_msg.payload.is_some(), "Open payload must be present");
let payload_json = serde_json::to_value(&open_msg.payload).unwrap();
let data_schema = payload_json.pointer("/properties/data");
assert!(
data_schema.is_some(),
"data property must appear in payload schema"
);
if let Some(ds) = data_schema {
let schema: asyncapi_rust::Schema = serde_json::from_value(ds.clone())
.expect("data property schema must deserialize without panic");
assert!(
matches!(schema, asyncapi_rust::Schema::Any(_)),
"serde_json::Value field must produce Schema::Any; got: {ds}"
);
}
}
#[test]
fn test_untagged_enum() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(untagged)]
pub enum UntaggedMsg {
Number(i64),
Text { content: String },
Object { id: u32, label: String },
}
let names = UntaggedMsg::asyncapi_message_names();
assert_eq!(names, vec!["Number", "Text", "Object"]);
assert_eq!(UntaggedMsg::asyncapi_tag_field(), None);
let messages = UntaggedMsg::asyncapi_messages();
assert_eq!(messages.len(), 3);
for msg in &messages {
assert!(
msg.payload.is_some(),
"untagged variant '{}' must have a payload",
msg.name.as_deref().unwrap_or("?")
);
}
let number_msg = messages
.iter()
.find(|m| m.name.as_deref() == Some("Number"))
.expect("Number message must exist");
let payload_json = serde_json::to_value(&number_msg.payload).unwrap();
let has_union = payload_json.get("oneOf").is_some() || payload_json.get("anyOf").is_some();
assert!(
has_union,
"untagged enum payload should be a union schema; got: {payload_json}"
);
}
#[test]
fn test_adjacently_tagged_enum() {
#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
#[serde(tag = "kind", content = "payload")]
pub enum AdjacentMsg {
#[serde(rename = "ping")]
Ping,
#[serde(rename = "data")]
Data { value: String, count: u32 },
}
let names = AdjacentMsg::asyncapi_message_names();
assert_eq!(names, vec!["Ping", "Data"]);
assert_eq!(AdjacentMsg::asyncapi_tag_field(), Some("kind"));
let messages = AdjacentMsg::asyncapi_messages();
assert_eq!(messages.len(), 2);
for msg in &messages {
assert!(
msg.payload.is_some(),
"adjacently-tagged variant '{}' must have a payload",
msg.name.as_deref().unwrap_or("?")
);
}
let ping = messages
.iter()
.find(|m| m.name.as_deref() == Some("Ping"))
.expect("Ping must exist");
let data = messages
.iter()
.find(|m| m.name.as_deref() == Some("Data"))
.expect("Data must exist");
let ping_json = serde_json::to_value(&ping.payload).unwrap();
let data_json = serde_json::to_value(&data.payload).unwrap();
assert!(ping_json.is_object() || ping_json.is_boolean());
assert!(data_json.is_object() || data_json.is_boolean());
let ping_str = serde_json::to_string(&ping_json).unwrap();
let data_str = serde_json::to_string(&data_json).unwrap();
assert!(
ping_str.contains("ping"),
"discriminant 'ping' must appear in Ping payload schema"
);
assert!(
data_str.contains("data"),
"discriminant 'data' must appear in Data payload schema"
);
}