use proc_macro2::TokenStream;
use quote::quote;
use syn::{Error, ImplItemFn, Result, parse2};
use crate::attrs::ResourceAttrs;
pub fn expand_resource(attr: TokenStream, item: TokenStream) -> Result<TokenStream> {
let attrs =
ResourceAttrs::parse(attr).map_err(|e| Error::new(proc_macro2::Span::call_site(), e))?;
let method: ImplItemFn = parse2(item)?;
validate_resource_method(&method)?;
let uri_pattern = &attrs.uri_pattern;
let resource_name = attrs.name.unwrap_or_else(|| method.sig.ident.to_string());
let description = attrs.description.unwrap_or_default();
let mime_type = attrs.mime_type.unwrap_or_else(|| "text/plain".to_string());
let marker_name = syn::Ident::new(
&format!("__MCP_RESOURCE_{}", method.sig.ident),
method.sig.ident.span(),
);
Ok(quote! {
#[doc(hidden)]
#[allow(non_upper_case_globals)]
const #marker_name: (&str, &str, &str, &str) = (#uri_pattern, #resource_name, #description, #mime_type);
#[doc = #description]
#[allow(dead_code)]
#method
})
}
fn validate_resource_method(method: &ImplItemFn) -> Result<()> {
if method.sig.receiver().is_none() {
return Err(Error::new_spanned(
&method.sig,
"resource methods must take &self",
));
}
if let Some(receiver) = method.sig.receiver() {
if receiver.mutability.is_some() {
return Err(Error::new_spanned(
receiver,
"resource methods should take &self, not &mut self\n\
help: use interior mutability (e.g., Mutex, RwLock) if you need to modify state",
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_validate_resource_method() {
let method: ImplItemFn = parse_quote! {
async fn get_config(&self, key: String) -> ResourceContents {
ResourceContents::text("value")
}
};
assert!(validate_resource_method(&method).is_ok());
let method: ImplItemFn = parse_quote! {
async fn get_config(key: String) -> ResourceContents {
ResourceContents::text("value")
}
};
assert!(validate_resource_method(&method).is_err());
}
}