#![warn(clippy::all)]
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, parse_macro_input};
mod asyncapi_attrs;
mod asyncapi_spec_attrs;
mod serde_attrs;
use asyncapi_attrs::extract_asyncapi_meta;
use asyncapi_spec_attrs::extract_asyncapi_spec_meta;
use serde_attrs::{extract_serde_rename, extract_serde_tag};
#[proc_macro_derive(ToAsyncApiMessage, attributes(asyncapi))]
pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let tag_field = extract_serde_tag(&input.attrs);
struct MessageMeta {
name: String,
discriminant: String,
summary: Option<String>,
description: Option<String>,
title: Option<String>,
content_type: Option<String>,
triggers_binary: bool,
}
let messages = match &input.data {
Data::Enum(data_enum) => {
let mut message_metas = Vec::new();
for variant in &data_enum.variants {
let variant_name = &variant.ident;
let variant_ident_str = variant_name.to_string();
let discriminant = extract_serde_rename(&variant.attrs)
.unwrap_or_else(|| variant_ident_str.clone());
let asyncapi_meta = extract_asyncapi_meta(&variant.attrs);
let message_name = asyncapi_meta
.message_name
.clone()
.unwrap_or_else(|| variant_ident_str.clone());
message_metas.push(MessageMeta {
name: message_name,
discriminant,
summary: asyncapi_meta.summary,
description: asyncapi_meta.description,
title: asyncapi_meta.title,
content_type: asyncapi_meta.content_type,
triggers_binary: asyncapi_meta.triggers_binary,
});
}
message_metas
}
Data::Struct(_) => {
let asyncapi_meta = extract_asyncapi_meta(&input.attrs);
let struct_name = name.to_string();
let message_name = asyncapi_meta
.message_name
.clone()
.unwrap_or_else(|| struct_name.clone());
vec![MessageMeta {
name: message_name,
discriminant: struct_name,
summary: asyncapi_meta.summary,
description: asyncapi_meta.description,
title: asyncapi_meta.title,
content_type: asyncapi_meta.content_type,
triggers_binary: asyncapi_meta.triggers_binary,
}]
}
Data::Union(_) => {
return syn::Error::new_spanned(name, "ToAsyncApiMessage cannot be derived for unions")
.to_compile_error()
.into();
}
};
let message_count = messages.len();
let message_literals = messages.iter().map(|m| m.name.as_str());
let message_names_for_gen = messages.iter().map(|m| m.name.as_str());
let message_discriminants = messages.iter().map(|m| m.discriminant.as_str());
let message_titles = messages.iter().map(|m| {
if let Some(ref title) = m.title {
quote! { Some(#title.to_string()) }
} else {
let name = &m.name;
quote! { Some(#name.to_string()) }
}
});
let message_summaries = messages.iter().map(|m| {
if let Some(ref summary) = m.summary {
quote! { Some(#summary.to_string()) }
} else {
quote! { None }
}
});
let message_descriptions = messages.iter().map(|m| {
if let Some(ref desc) = m.description {
quote! { Some(#desc.to_string()) }
} else {
quote! { None }
}
});
let message_content_types = messages.iter().map(|m| {
if let Some(ref ct) = m.content_type {
quote! { Some(#ct.to_string()) }
} else if m.triggers_binary {
quote! { Some("application/octet-stream".to_string()) }
} else {
quote! { Some("application/json".to_string()) }
}
});
let tag_info = if let Some(tag) = tag_field {
quote! {
Some(#tag)
}
} else {
quote! { None }
};
let expanded = quote! {
const _: () = {
fn rewrite_defs_refs(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
if let Some(r) = map.get_mut("$ref") {
if let Some(s) = r.as_str() {
if let Some(name) = s.strip_prefix("#/$defs/") {
*r = serde_json::Value::String(
format!("#/components/schemas/{}", name)
);
}
}
}
for v in map.values_mut() {
rewrite_defs_refs(v);
}
}
serde_json::Value::Array(arr) => {
for v in arr.iter_mut() {
rewrite_defs_refs(v);
}
}
_ => {}
}
}
impl #name {
pub fn asyncapi_message_names() -> Vec<&'static str> {
vec![#(#message_literals),*]
}
pub fn asyncapi_message_count() -> usize {
#message_count
}
pub fn asyncapi_tag_field() -> Option<&'static str> {
#tag_info
}
pub fn asyncapi_schemas() -> std::collections::HashMap<String, asyncapi_rust::Schema>
where
Self: schemars::JsonSchema,
{
use schemars::schema_for;
let schema = schema_for!(Self);
let schema_json = serde_json::to_value(&schema)
.expect("Failed to serialize schema");
let mut result = std::collections::HashMap::new();
if let Some(defs) = schema_json.get("$defs").and_then(|v| v.as_object()) {
for (name, def_schema) in defs {
let mut def = def_schema.clone();
rewrite_defs_refs(&mut def);
if let Ok(s) = serde_json::from_value::<asyncapi_rust::Schema>(def) {
result.insert(name.clone(), s);
}
}
}
result
}
pub fn asyncapi_messages() -> Vec<asyncapi_rust::Message>
where
Self: schemars::JsonSchema,
{
use schemars::schema_for;
let schema = schema_for!(Self);
let schema_json = serde_json::to_value(&schema)
.expect("Failed to serialize schema");
let tag_field = Self::asyncapi_tag_field();
let mut variant_schemas: std::collections::HashMap<String, serde_json::Value> =
std::collections::HashMap::new();
if let Some(tag) = tag_field {
if let Some(variants) = schema_json.get("oneOf").and_then(|v| v.as_array()) {
for variant in variants {
let discriminant = variant
.get("properties")
.and_then(|props| props.get(tag))
.and_then(|tag_prop| {
tag_prop.get("const").or_else(|| {
tag_prop
.get("enum")
.and_then(|e| e.as_array())
.and_then(|a| a.first())
})
})
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(name) = discriminant {
let mut variant_schema = variant.clone();
if let Some(obj) = variant_schema.as_object_mut() {
obj.remove("$defs");
}
rewrite_defs_refs(&mut variant_schema);
variant_schemas.insert(name, variant_schema);
}
}
}
}
let names: &[&str] = &[#(#message_names_for_gen),*];
let discriminants: &[&str] = &[#(#message_discriminants),*];
let titles: &[Option<String>] = &[#(#message_titles),*];
let summaries: &[Option<String>] = &[#(#message_summaries),*];
let descriptions: &[Option<String>] = &[#(#message_descriptions),*];
let content_types: &[Option<String>] = &[#(#message_content_types),*];
let mut messages = Vec::with_capacity(names.len());
for i in 0..names.len() {
let msg_name = names[i];
let discriminant = discriminants[i];
let payload = if let Some(v) = variant_schemas.get(discriminant) {
serde_json::from_value(v.clone()).ok()
} else {
let mut fallback = schema_json.clone();
if let Some(obj) = fallback.as_object_mut() {
obj.remove("$defs");
}
rewrite_defs_refs(&mut fallback);
serde_json::from_value(fallback).ok()
};
messages.push(asyncapi_rust::Message {
name: Some(msg_name.to_string()),
title: titles[i].clone(),
summary: summaries[i].clone(),
description: descriptions[i].clone(),
content_type: content_types[i].clone(),
payload,
});
}
messages
}
}
};
};
TokenStream::from(expanded)
}
#[proc_macro_derive(
AsyncApi,
attributes(
asyncapi,
asyncapi_server,
asyncapi_channel,
asyncapi_operation,
asyncapi_messages
)
)]
pub fn derive_asyncapi(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let spec_meta = extract_asyncapi_spec_meta(&input.attrs);
let title = match spec_meta.title {
Some(t) => t,
None => {
return syn::Error::new_spanned(
name,
"AsyncApi requires a title attribute: #[asyncapi(title = \"...\")]",
)
.to_compile_error()
.into();
}
};
let version = match spec_meta.version {
Some(v) => v,
None => {
return syn::Error::new_spanned(
name,
"AsyncApi requires a version attribute: #[asyncapi(version = \"...\")]",
)
.to_compile_error()
.into();
}
};
let description = if let Some(desc) = spec_meta.description {
quote! { Some(#desc.to_string()) }
} else {
quote! { None }
};
let servers_code = if spec_meta.servers.is_empty() {
quote! { None }
} else {
let server_entries = spec_meta.servers.iter().map(|server| {
let name = &server.name;
let host = &server.host;
let protocol = &server.protocol;
let pathname = if let Some(p) = &server.pathname {
quote! { Some(#p.to_string()) }
} else {
quote! { None }
};
let desc = if let Some(d) = &server.description {
quote! { Some(#d.to_string()) }
} else {
quote! { None }
};
let variables = if server.variables.is_empty() {
quote! { None }
} else {
let var_entries = server.variables.iter().map(|var| {
let var_name = &var.name;
let var_desc = if let Some(d) = &var.description {
quote! { Some(#d.to_string()) }
} else {
quote! { None }
};
let var_default = if let Some(d) = &var.default {
quote! { Some(#d.to_string()) }
} else {
quote! { None }
};
let var_enum = if var.enum_values.is_empty() {
quote! { None }
} else {
let enum_vals = &var.enum_values;
quote! { Some(vec![#(#enum_vals.to_string()),*]) }
};
let var_examples = if var.examples.is_empty() {
quote! { None }
} else {
let examples = &var.examples;
quote! { Some(vec![#(#examples.to_string()),*]) }
};
quote! {
server_variables.insert(
#var_name.to_string(),
asyncapi_rust::ServerVariable {
description: #var_desc,
default: #var_default,
enum_values: #var_enum,
examples: #var_examples,
}
);
}
});
quote! {
{
let mut server_variables = std::collections::HashMap::new();
#(#var_entries)*
Some(server_variables)
}
}
};
quote! {
servers.insert(
#name.to_string(),
asyncapi_rust::Server {
host: #host.to_string(),
protocol: #protocol.to_string(),
pathname: #pathname,
description: #desc,
variables: #variables,
}
);
}
});
quote! {
{
let mut servers = std::collections::HashMap::new();
#(#server_entries)*
Some(servers)
}
}
};
let channels_code = if spec_meta.channels.is_empty() {
quote! { None }
} else {
let channel_entries = spec_meta.channels.iter().map(|channel| {
let name = &channel.name;
let address = if let Some(addr) = &channel.address {
quote! { Some(#addr.to_string()) }
} else {
quote! { None }
};
let parameters = if channel.parameters.is_empty() {
quote! { None }
} else {
let param_entries = channel.parameters.iter().map(|param| {
let param_name = ¶m.name;
let param_desc = if let Some(d) = ¶m.description {
quote! { Some(#d.to_string()) }
} else {
quote! { None }
};
let param_default = if let Some(d) = ¶m.default {
quote! { Some(#d.to_string()) }
} else {
quote! { None }
};
let param_enum = if param.enum_values.is_empty() {
quote! { None }
} else {
let vals = ¶m.enum_values;
quote! { Some(vec![#(#vals.to_string()),*]) }
};
let param_examples = if param.examples.is_empty() {
quote! { None }
} else {
let vals = ¶m.examples;
quote! { Some(vec![#(#vals.to_string()),*]) }
};
let param_location = if let Some(l) = ¶m.location {
quote! { Some(#l.to_string()) }
} else {
quote! { None }
};
quote! {
channel_parameters.insert(
#param_name.to_string(),
asyncapi_rust::Parameter {
description: #param_desc,
default: #param_default,
enum_values: #param_enum,
examples: #param_examples,
location: #param_location,
}
);
}
});
quote! {
{
let mut channel_parameters = std::collections::HashMap::new();
#(#param_entries)*
Some(channel_parameters)
}
}
};
quote! {
channels.insert(
#name.to_string(),
asyncapi_rust::Channel {
address: #address,
messages: None,
parameters: #parameters,
}
);
}
});
quote! {
{
let mut channels = std::collections::HashMap::new();
#(#channel_entries)*
Some(channels)
}
}
};
let operations_code = if spec_meta.operations.is_empty() {
quote! { None }
} else {
let operation_entries = spec_meta.operations.iter().map(|operation| {
let name = &operation.name;
let channel_ref = &operation.channel;
let action = &operation.action;
let action_enum = if action == "send" {
quote! { asyncapi_rust::OperationAction::Send }
} else if action == "receive" {
quote! { asyncapi_rust::OperationAction::Receive }
} else {
return syn::Error::new_spanned(
name,
format!("Invalid action '{}', must be 'send' or 'receive'", action),
)
.to_compile_error();
};
quote! {
operations.insert(
#name.to_string(),
asyncapi_rust::Operation {
action: #action_enum,
channel: asyncapi_rust::ChannelRef {
reference: format!("#/channels/{}", #channel_ref),
},
messages: None,
}
);
}
});
quote! {
{
let mut operations = std::collections::HashMap::new();
#(#operation_entries)*
Some(operations)
}
}
};
let components_code = if spec_meta.message_types.is_empty() {
quote! { None }
} else {
let type_calls = spec_meta.message_types.iter().map(|type_name| {
quote! {
for msg in #type_name::asyncapi_messages() {
if let Some(ref name) = msg.name {
if messages.contains_key(name.as_str()) {
panic!(
"asyncapi-rust: message name collision for '{}' from {}. \
Use #[asyncapi(message_name = \"...\")] on one variant to disambiguate.",
name,
stringify!(#type_name)
);
}
messages.insert(name.clone(), msg.clone());
}
}
for (name, schema) in #type_name::asyncapi_schemas() {
schemas.entry(name).or_insert(schema);
}
}
});
quote! {
{
let mut messages = std::collections::HashMap::new();
let mut schemas = std::collections::HashMap::new();
#(#type_calls)*
Some(asyncapi_rust::Components {
messages: if messages.is_empty() { None } else { Some(messages) },
schemas: if schemas.is_empty() { None } else { Some(schemas) },
})
}
}
};
let expanded = quote! {
impl #name {
pub fn asyncapi_spec() -> asyncapi_rust::AsyncApiSpec {
asyncapi_rust::AsyncApiSpec {
asyncapi: "3.0.0".to_string(),
info: asyncapi_rust::Info {
title: #title.to_string(),
version: #version.to_string(),
description: #description,
},
servers: #servers_code,
channels: #channels_code,
operations: #operations_code,
components: #components_code,
}
}
}
};
TokenStream::from(expanded)
}
#[cfg(test)]
mod tests {
#[test]
fn test_placeholder() {
}
}