use proc_macro2::Span;
use quote::ToTokens;
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
Attribute, FnArg, Ident, ItemTrait, LitInt, LitStr, Pat, Path, ReturnType, Token, TraitItem,
TraitItemFn, Type,
};
#[derive(Debug, Clone)]
pub struct InterfaceAttrs {
pub version: u32,
pub buffer_strategy: BufferStrategyAttr,
pub crate_path: Path,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufferStrategyAttr {
PluginAllocated = 1,
Arena = 2,
}
impl Parse for InterfaceAttrs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut version = None;
let mut buffer = None;
let mut crate_path = None;
while !input.is_empty() {
let key_str = if input.peek(Token![crate]) {
let _kw: Token![crate] = input.parse()?;
"crate".to_string()
} else {
let ident: Ident = input.parse()?;
ident.to_string()
};
let _eq: Token![=] = input.parse()?;
match key_str.as_str() {
"version" => {
let lit: LitInt = input.parse()?;
version = Some(lit.base10_parse::<u32>()?);
}
"buffer" => {
let strategy: Ident = input.parse()?;
buffer = Some(match strategy.to_string().as_str() {
"PluginAllocated" => BufferStrategyAttr::PluginAllocated,
"Arena" => BufferStrategyAttr::Arena,
"CallerAllocated" => {
return Err(syn::Error::new(
strategy.span(),
"`CallerAllocated` buffer strategy was removed in fidius 0.1.0; use `PluginAllocated` or `Arena`",
))
}
_ => {
return Err(syn::Error::new(
strategy.span(),
"expected PluginAllocated or Arena",
))
}
});
}
"crate" => {
let lit: LitStr = input.parse()?;
let path: Path = lit.parse()?;
crate_path = Some(path);
}
other => {
return Err(syn::Error::new(
Span::call_site(),
format!(
"unknown attribute `{other}`, expected `version`, `buffer`, or `crate`"
),
))
}
}
if !input.is_empty() {
let _comma: Token![,] = input.parse()?;
}
}
let crate_path = crate_path.unwrap_or_else(|| syn::parse_str::<Path>("fidius").unwrap());
Ok(InterfaceAttrs {
version: version
.ok_or_else(|| syn::Error::new(Span::call_site(), "missing `version` attribute"))?,
buffer_strategy: buffer
.ok_or_else(|| syn::Error::new(Span::call_site(), "missing `buffer` attribute"))?,
crate_path,
})
}
}
#[derive(Debug, Clone)]
pub struct MetaKvAttr {
pub key: String,
pub value: String,
}
#[derive(Debug)]
pub struct InterfaceIR {
pub trait_name: Ident,
pub attrs: InterfaceAttrs,
pub methods: Vec<MethodIR>,
pub trait_metas: Vec<MetaKvAttr>,
pub original_trait: ItemTrait,
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct MethodIR {
pub name: Ident,
pub arg_types: Vec<Type>,
pub arg_names: Vec<Ident>,
pub return_type: Option<Type>,
pub is_async: bool,
pub optional_since: Option<u32>,
pub signature_string: String,
pub method_metas: Vec<MetaKvAttr>,
}
impl MethodIR {
pub fn is_required(&self) -> bool {
self.optional_since.is_none()
}
}
fn parse_meta_attrs(attrs: &[Attribute], ident: &str) -> syn::Result<Vec<MetaKvAttr>> {
let mut out = Vec::new();
for attr in attrs {
if !attr.path().is_ident(ident) {
continue;
}
let (key_lit, value_lit): (LitStr, LitStr) =
attr.parse_args_with(|input: syn::parse::ParseStream| {
let k: LitStr = input.parse()?;
let _: Token![,] = input.parse()?;
let v: LitStr = input.parse()?;
Ok((k, v))
})?;
let key = key_lit.value();
let value = value_lit.value();
if key.is_empty() {
return Err(syn::Error::new(
key_lit.span(),
format!("#[{ident}(key, value)] key must not be empty"),
));
}
if key.trim() != key {
return Err(syn::Error::new(
key_lit.span(),
format!("#[{ident}(key, value)] key must not have leading or trailing whitespace"),
));
}
if key.starts_with("fidius.") {
return Err(syn::Error::new(
key_lit.span(),
format!("the `fidius.*` key namespace is reserved for framework use; got `{key}`"),
));
}
if out.iter().any(|existing: &MetaKvAttr| existing.key == key) {
return Err(syn::Error::new(
key_lit.span(),
format!("duplicate #[{ident}] key `{key}`"),
));
}
out.push(MetaKvAttr { key, value });
}
Ok(out)
}
fn parse_optional_attr(attrs: &[Attribute]) -> syn::Result<Option<u32>> {
for attr in attrs {
if attr.path().is_ident("optional") {
let mut since = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("since") {
let _eq: Token![=] = meta.input.parse()?;
let lit: LitInt = meta.input.parse()?;
since = Some(lit.base10_parse::<u32>()?);
Ok(())
} else {
Err(meta.error("expected `since`"))
}
})?;
return Ok(since);
}
}
Ok(None)
}
fn build_signature_string(method: &TraitItemFn) -> String {
let name = method.sig.ident.to_string();
let arg_types: Vec<String> = method
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(pat_type) => Some(pat_type.ty.to_token_stream().to_string()),
})
.collect();
let ret = match &method.sig.output {
ReturnType::Default => String::new(),
ReturnType::Type(_, ty) => ty.to_token_stream().to_string(),
};
format!("{}:{}->{}", name, arg_types.join(","), ret)
}
fn extract_arg_names(method: &TraitItemFn) -> Vec<Ident> {
method
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(pat_type) => {
if let Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
Some(pat_ident.ident.clone())
} else {
Some(Ident::new("_arg", pat_type.span()))
}
}
})
.collect()
}
fn extract_arg_types(method: &TraitItemFn) -> Vec<Type> {
method
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(pat_type) => Some((*pat_type.ty).clone()),
})
.collect()
}
fn extract_return_type(method: &TraitItemFn) -> Option<Type> {
match &method.sig.output {
ReturnType::Default => None,
ReturnType::Type(_, ty) => Some((**ty).clone()),
}
}
pub fn parse_interface(attrs: InterfaceAttrs, item: &ItemTrait) -> syn::Result<InterfaceIR> {
let trait_name = item.ident.clone();
let mut methods = Vec::new();
let mut optional_count = 0u32;
for trait_item in &item.items {
let TraitItem::Fn(method) = trait_item else {
continue;
};
if let Some(FnArg::Receiver(receiver)) = method.sig.inputs.first() {
if receiver.mutability.is_some() {
return Err(syn::Error::new(
receiver.span(),
"fidius plugins are stateless: methods must take `&self`, not `&mut self`",
));
}
}
let optional_since = parse_optional_attr(&method.attrs)?;
if optional_since.is_some() {
optional_count += 1;
if optional_count > 64 {
return Err(syn::Error::new(
method.sig.ident.span(),
"fidius supports at most 64 optional methods per interface (u64 capability bitfield)",
));
}
}
let is_async = method.sig.asyncness.is_some();
let signature_string = build_signature_string(method);
let arg_types = extract_arg_types(method);
let arg_names = extract_arg_names(method);
let return_type = extract_return_type(method);
let method_metas = parse_meta_attrs(&method.attrs, "method_meta")?;
methods.push(MethodIR {
name: method.sig.ident.clone(),
arg_types,
arg_names,
return_type,
is_async,
optional_since,
signature_string,
method_metas,
});
}
let trait_metas = parse_meta_attrs(&item.attrs, "trait_meta")?;
Ok(InterfaceIR {
trait_name,
attrs,
methods,
trait_metas,
original_trait: item.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
fn parse_test_trait(tokens: proc_macro2::TokenStream) -> InterfaceIR {
let item: ItemTrait = syn::parse2(tokens).expect("failed to parse trait");
let attrs = InterfaceAttrs {
version: 1,
buffer_strategy: BufferStrategyAttr::PluginAllocated,
crate_path: syn::parse_str("fidius").unwrap(),
};
parse_interface(attrs, &item).expect("failed to parse interface")
}
#[test]
fn basic_trait_parsing() {
let ir = parse_test_trait(quote! {
pub trait Greeter: Send + Sync {
fn greet(&self, name: String) -> String;
}
});
assert_eq!(ir.trait_name, "Greeter");
assert_eq!(ir.methods.len(), 1);
let m = &ir.methods[0];
assert_eq!(m.name, "greet");
assert!(!m.is_async);
assert!(m.is_required());
assert_eq!(m.arg_types.len(), 1);
assert!(m.return_type.is_some());
assert!(m.signature_string.starts_with("greet:"));
}
#[test]
fn optional_method_parsing() {
let ir = parse_test_trait(quote! {
pub trait Filter: Send + Sync {
fn process(&self, data: Vec<u8>) -> Vec<u8>;
#[optional(since = 2)]
fn process_v2(&self, data: Vec<u8>, opts: String) -> Vec<u8>;
}
});
assert_eq!(ir.methods.len(), 2);
assert!(ir.methods[0].is_required());
assert_eq!(ir.methods[1].optional_since, Some(2));
}
#[test]
fn async_method_detection() {
let ir = parse_test_trait(quote! {
pub trait AsyncProcessor: Send + Sync {
async fn process(&self, input: String) -> String;
fn sync_method(&self) -> u32;
}
});
assert!(ir.methods[0].is_async);
assert!(!ir.methods[1].is_async);
}
#[test]
fn rejects_mut_self() {
let item: ItemTrait = syn::parse2(quote! {
pub trait Bad: Send + Sync {
fn mutate(&mut self);
}
})
.unwrap();
let attrs = InterfaceAttrs {
version: 1,
buffer_strategy: BufferStrategyAttr::PluginAllocated,
crate_path: syn::parse_str("fidius").unwrap(),
};
let result = parse_interface(attrs, &item);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("stateless"));
}
#[test]
fn signature_string_format() {
let ir = parse_test_trait(quote! {
pub trait Example: Send + Sync {
fn foo(&self, a: u32, b: String) -> bool;
}
});
let sig = &ir.methods[0].signature_string;
assert!(sig.starts_with("foo:"));
assert!(sig.contains("->"));
}
#[test]
fn interface_attrs_parsing() {
let attrs: InterfaceAttrs = syn::parse_str("version = 3, buffer = Arena").unwrap();
assert_eq!(attrs.version, 3);
assert_eq!(attrs.buffer_strategy, BufferStrategyAttr::Arena);
assert_eq!(attrs.crate_path.segments.last().unwrap().ident, "fidius");
}
#[test]
fn interface_attrs_with_crate_path() {
let attrs: InterfaceAttrs =
syn::parse_str(r#"version = 1, buffer = PluginAllocated, crate = "my_crate::fidius""#)
.unwrap();
assert_eq!(attrs.version, 1);
assert_eq!(attrs.buffer_strategy, BufferStrategyAttr::PluginAllocated);
let segments: Vec<String> = attrs
.crate_path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
assert_eq!(segments, vec!["my_crate", "fidius"]);
}
}