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>,
pub wire_raw: bool,
pub streaming: bool,
pub stream_item_type: Option<Type>,
pub client_stream_item: Option<Type>,
}
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 parse_wire_attr(attrs: &[Attribute]) -> syn::Result<bool> {
for attr in attrs {
if !attr.path().is_ident("wire") {
continue;
}
let mut raw = false;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("raw") {
raw = true;
Ok(())
} else {
Err(meta.error("expected `raw` — only `#[wire(raw)]` is supported"))
}
})?;
return Ok(raw);
}
Ok(false)
}
fn is_vec_u8(ty: &Type) -> bool {
let Type::Path(type_path) = ty else {
return false;
};
let Some(last) = type_path.path.segments.last() else {
return false;
};
if last.ident != "Vec" {
return false;
}
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return false;
};
if args.args.len() != 1 {
return false;
}
let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
return false;
};
let Type::Path(inner_path) = inner else {
return false;
};
inner_path
.path
.get_ident()
.map(|id| id == "u8")
.unwrap_or(false)
}
fn result_ok_type(ty: &Type) -> Option<&Type> {
let Type::Path(type_path) = ty else {
return None;
};
let last = type_path.path.segments.last()?;
if last.ident != "Result" {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
let first = args.args.first()?;
match first {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
}
}
fn validate_raw_method_signature(
method: &TraitItemFn,
arg_types: &[Type],
return_type: Option<&Type>,
) -> syn::Result<()> {
let span = method.sig.ident.span();
if arg_types.len() != 1 {
return Err(syn::Error::new(
span,
"#[wire(raw)] methods must take exactly one argument of type `Vec<u8>` (excluding `&self`)",
));
}
if !is_vec_u8(&arg_types[0]) {
return Err(syn::Error::new(
arg_types[0].span(),
"#[wire(raw)] argument must be `Vec<u8>`",
));
}
let Some(ret) = return_type else {
return Err(syn::Error::new(
span,
"#[wire(raw)] methods must return `Vec<u8>` or `Result<Vec<u8>, E>`",
));
};
if is_vec_u8(ret) {
return Ok(());
}
if let Some(ok) = result_ok_type(ret) {
if is_vec_u8(ok) {
return Ok(());
}
}
Err(syn::Error::new(
ret.span(),
"#[wire(raw)] methods must return `Vec<u8>` or `Result<Vec<u8>, E>`",
))
}
fn stream_item_type(ty: &Type) -> Option<Type> {
let Type::Path(type_path) = ty else {
return None;
};
let last = type_path.path.segments.last()?;
if last.ident != "Stream" {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
if args.args.len() != 1 {
return None;
}
match args.args.first()? {
syn::GenericArgument::Type(t) => Some(t.clone()),
_ => None,
}
}
fn build_signature_string(
method: &TraitItemFn,
wire_raw: bool,
stream_item: Option<&Type>,
client_streaming: bool,
) -> 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(match stream_item_type(&pat_type.ty) {
Some(item) => item.to_token_stream().to_string(),
None => pat_type.ty.to_token_stream().to_string(),
}),
})
.collect();
let ret = match stream_item {
Some(item) => item.to_token_stream().to_string(),
None => match &method.sig.output {
ReturnType::Default => String::new(),
ReturnType::Type(_, ty) => ty.to_token_stream().to_string(),
},
};
fidius_core::hash::signature_string(
&name,
&arg_types,
&ret,
wire_raw,
stream_item.is_some(),
client_streaming,
)
}
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 wire_raw = parse_wire_attr(&method.attrs)?;
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")?;
let stream_item = return_type.as_ref().and_then(stream_item_type);
let streaming = stream_item.is_some();
let client_stream_item: Option<Type> = arg_types.iter().find_map(stream_item_type);
if wire_raw {
if is_async {
return Err(syn::Error::new(
method.sig.ident.span(),
"#[wire(raw)] is not supported on async methods in this release",
));
}
validate_raw_method_signature(method, &arg_types, return_type.as_ref())?;
}
let signature_string = build_signature_string(
method,
wire_raw,
stream_item.as_ref(),
client_stream_item.is_some(),
);
methods.push(MethodIR {
name: method.sig.ident.clone(),
arg_types,
arg_names,
return_type,
is_async,
optional_since,
signature_string,
method_metas,
wire_raw,
streaming,
stream_item_type: stream_item,
client_stream_item,
});
}
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"]);
}
#[test]
fn detects_server_streaming_return() {
let ir = parse_test_trait(quote! {
pub trait Source: Send + Sync {
fn read(&self, cfg: String) -> fidius::Stream<u64>;
fn ping(&self) -> bool;
}
});
assert!(ir.methods[0].streaming);
let item = ir.methods[0].stream_item_type.as_ref().unwrap();
assert_eq!(quote!(#item).to_string(), "u64");
assert!(ir.methods[0].signature_string.ends_with("!stream"));
assert!(ir.methods[0]
.signature_string
.starts_with("read:String->u64"));
assert!(!ir.methods[1].streaming);
assert!(ir.methods[1].stream_item_type.is_none());
assert!(!ir.methods[1].signature_string.contains("!stream"));
}
#[test]
fn streaming_and_unary_hash_differently() {
let streaming = parse_test_trait(quote! {
pub trait A: Send + Sync { fn read(&self) -> fidius::Stream<Row>; }
});
let unary = parse_test_trait(quote! {
pub trait A: Send + Sync { fn read(&self) -> Row; }
});
let hs =
fidius_core::hash::interface_hash(&[streaming.methods[0].signature_string.as_str()]);
let hu = fidius_core::hash::interface_hash(&[unary.methods[0].signature_string.as_str()]);
assert_ne!(
hs, hu,
"a streaming method must hash differently from a unary one of the same name/args"
);
}
#[test]
fn bare_stream_marker_is_detected() {
let ir = parse_test_trait(quote! {
pub trait S: Send + Sync { fn read(&self) -> Stream<String>; }
});
assert!(ir.methods[0].streaming);
}
#[test]
fn client_streaming_is_recognized_in_the_ir() {
let item: ItemTrait = syn::parse2(quote! {
pub trait Sink: Send + Sync {
fn load(&self, items: fidius::Stream<Row>) -> u32;
}
})
.unwrap();
let attrs = InterfaceAttrs {
version: 1,
buffer_strategy: BufferStrategyAttr::PluginAllocated,
crate_path: syn::parse_str("fidius").unwrap(),
};
let ir = parse_interface(attrs, &item).unwrap();
let m = &ir.methods[0];
assert!(
m.client_stream_item.is_some(),
"a `Stream<T>` arg sets client_stream_item"
);
assert!(
m.signature_string.contains("<stream"),
"the hash carries the `<stream` marker: {}",
m.signature_string
);
}
#[test]
fn is_required_reflects_optional_attr() {
let ir = parse_test_trait(quote! {
pub trait Api: Send + Sync {
fn core(&self, x: u32) -> u32;
#[optional(since = 2)]
fn added_later(&self, y: u32) -> u32;
}
});
let core = ir.methods.iter().find(|m| m.name == "core").unwrap();
let opt = ir.methods.iter().find(|m| m.name == "added_later").unwrap();
assert!(core.is_required(), "a plain method is required");
assert!(!opt.is_required(), "an #[optional] method is not required");
}
#[test]
fn is_vec_u8_distinguishes_types() {
assert!(is_vec_u8(&syn::parse_str::<Type>("Vec<u8>").unwrap()));
assert!(!is_vec_u8(&syn::parse_str::<Type>("String").unwrap()));
assert!(!is_vec_u8(&syn::parse_str::<Type>("Vec<u16>").unwrap()));
}
#[test]
fn extract_arg_names_returns_each_non_self_arg() {
let m: TraitItemFn = syn::parse_str("fn f(&self, a: u32, b: String);").unwrap();
let names = extract_arg_names(&m);
assert_eq!(names.len(), 2);
assert_eq!(names[0].to_string(), "a");
assert_eq!(names[1].to_string(), "b");
}
#[test]
fn validate_raw_method_signature_rejects_bad_shapes() {
let ret: Type = syn::parse_str("Vec<u8>").unwrap();
let bad: TraitItemFn = syn::parse_str("fn f(&self, a: String) -> Vec<u8>;").unwrap();
let bad_args = vec![syn::parse_str::<Type>("String").unwrap()];
assert!(validate_raw_method_signature(&bad, &bad_args, Some(&ret)).is_err());
let good: TraitItemFn = syn::parse_str("fn g(&self, a: Vec<u8>) -> Vec<u8>;").unwrap();
let good_args = vec![syn::parse_str::<Type>("Vec<u8>").unwrap()];
assert!(validate_raw_method_signature(&good, &good_args, Some(&ret)).is_ok());
}
}