use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use darling::FromMeta;
#[derive(Debug, Clone)]
pub struct FnArg {
pub pos: usize,
pub name: String,
pub ty: Option<syn::Type>,
pub description: Option<String>,
}
impl FnArg {
pub fn type_name(&self) -> Option<String> {
if let Some(ty) = &self.ty {
Some(type_to_string(ty))
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct FnReturn {
pub ty: Option<syn::Type>,
pub status: Option<u16>,
pub description: Option<String>,
}
impl FnReturn {
pub fn type_name(&self) -> Option<String> {
if let Some(ty) = &self.ty {
Some(type_to_string(ty))
} else {
None
}
}
}
#[derive(Debug)]
pub struct FnSpec {
pub name: String,
pub args: Vec<FnArg>,
pub returns: Vec<FnReturn>,
pub description: Option<String>,
pub method: bool,
pub receiver: Option<ReceiverSpec>,
}
#[derive(Debug, Clone)]
pub enum ReceiverSpec {
Value { mut_self: bool },
Ref { mut_ref: bool },
Typed { ty: String },
}
fn extract_receiver(rcv: &syn::Receiver) -> ReceiverSpec {
if rcv.colon_token.is_some() {
let ty = &rcv.ty;
return ReceiverSpec::Typed {
ty: quote!(#ty).to_string(),
};
}
let mut_self = rcv.mutability.is_some();
if rcv.reference.is_some() {
ReceiverSpec::Ref { mut_ref: mut_self }
} else {
ReceiverSpec::Value { mut_self }
}
}
fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
let docs: Vec<String> = attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.filter_map(|attr| {
if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(expr_lit) = &meta.value {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
let line = lit_str.value();
let line = line.trim_end();
let line = line.strip_prefix(' ').unwrap_or(line);
return Some(line.to_string());
}
}
}
None
})
.collect();
if docs.is_empty() {
None
} else {
Some(docs.join("\n"))
}
}
fn type_to_string(ty: &syn::Type) -> String {
match ty {
syn::Type::Path(type_path) => {
type_path.path.segments.last().map_or_else(
|| "_".to_string(),
|segment| {
let type_ident = &segment.ident;
let type_args = &segment.arguments;
if type_args.is_empty() {
type_ident.to_string()
} else {
format!("{}{}", type_ident, quote! {#type_args})
}
},
)
}
_ => quote! {#ty}.to_string(),
}
}
fn extract_args(sig: &syn::Signature) -> Vec<FnArg> {
sig.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => None, syn::FnArg::Typed(pat_type) => Some(extract_param_name(&pat_type.pat)),
})
.enumerate()
.map(|(pos, name)| FnArg {
pos,
name,
ty: None, description: None,
})
.collect()
}
fn extract_returns(sig: &syn::Signature) -> Vec<FnReturn> {
vec![] }
fn build_spec_from_signature(sig: &syn::Signature, description: Option<String>) -> FnSpec {
let receiver = sig.inputs.first().and_then(|arg| match arg {
syn::FnArg::Receiver(rcv) => Some(extract_receiver(rcv)),
_ => None,
});
FnSpec {
name: sig.ident.to_string(),
args: extract_args(sig),
returns: extract_returns(sig),
description,
method: false,
receiver,
}
}
fn extract_param_name(pat: &syn::Pat) -> String {
match pat {
syn::Pat::Ident(ident) => ident.ident.to_string(),
syn::Pat::Type(pat_type) => extract_param_name(&pat_type.pat),
syn::Pat::Reference(pat_ref) => extract_param_name(&pat_ref.pat),
syn::Pat::Paren(paren) => extract_param_name(&paren.pat),
syn::Pat::TupleStruct(ts) => ts.elems.first().map_or("_".to_string(), extract_param_name),
syn::Pat::Tuple(tuple) => tuple
.elems
.first()
.map_or("_".to_string(), extract_param_name),
syn::Pat::Wild(_) => "_".to_string(),
_ => "_".to_string(),
}
}
pub(crate) fn extract_func_spec(item: &proc_macro2::TokenStream, attr_name: &str) -> Result<FnSpec, syn::Error> {
let item = item.clone();
let (sig, attrs) = if let Ok(func) = syn::parse2::<syn::ItemFn>(item.clone()) {
(func.sig, func.attrs)
} else if let Ok(method) = syn::parse2::<syn::ImplItemFn>(item.clone()) {
(method.sig, method.attrs)
} else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!("#[{attr_name}] can only be applied to functions or methods"),
));
};
let docs = extract_docs(&attrs);
let mut fn_spec = build_spec_from_signature(&sig, docs);
fn_spec.method = fn_spec.receiver.is_some();
Ok(fn_spec)
}
#[derive(Debug, Clone, FromMeta)]
struct ArgOverride {
#[darling(default)]
pos: Option<usize>,
name: String,
#[darling(default)]
ty: Option<syn::Type>,
#[darling(default)]
description: Option<String>,
}
#[derive(Debug, Clone, FromMeta)]
struct ReturnOverride {
ty: Option<syn::Type>,
#[darling(default)]
status: Option<u16>,
#[darling(default)]
description: Option<String>,
}
pub(crate) fn parse_and_apply_overrides<T: FromMeta + Default>(
items: &[darling::ast::NestedMeta],
spec: &mut FnSpec,
) -> darling::Result<T> {
let mut conf_items = Vec::with_capacity(items.len());
let mut arg_overrides = Vec::new();
let mut return_overrides = Vec::new();
let mut description = None;
for item in items {
match item {
darling::ast::NestedMeta::Meta(syn::Meta::List(list)) if list.path.is_ident("arg") => {
let nested = darling::ast::NestedMeta::parse_meta_list(list.tokens.clone())
.map_err(|e| darling::Error::custom(format!("Failed to parse arg: {}", e)))?;
let arg_ovr = ArgOverride::from_list(&nested)
.map_err(|e| darling::Error::custom(format!("Invalid arg syntax: {}", e)))?;
arg_overrides.push(arg_ovr);
}
darling::ast::NestedMeta::Meta(syn::Meta::List(list)) if list.path.is_ident("returns") => {
let nested = darling::ast::NestedMeta::parse_meta_list(list.tokens.clone())
.map_err(|e| darling::Error::custom(format!("Failed to parse returns: {}", e)))?;
let ret_ovr = ReturnOverride::from_list(&nested)
.map_err(|e| darling::Error::custom(format!("Invalid returns syntax: {}", e)))?;
return_overrides.push(ret_ovr);
}
darling::ast::NestedMeta::Meta(syn::Meta::NameValue(nv)) if nv.path.is_ident("description") => {
if let syn::Expr::Lit(expr_lit) = &nv.value {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
description = Some(lit_str.value());
}
}
}
_ => conf_items.push(item.clone()),
}
}
apply_arg_overrides(spec, &arg_overrides)?;
apply_return_overrides(spec, &return_overrides)?;
if let Some(desc) = description {
spec.description = Some(desc);
}
let conf = if conf_items.is_empty() {
T::default()
} else {
T::from_list(&conf_items)?
};
Ok(conf)
}
fn apply_arg_overrides(spec: &mut FnSpec, overrides: &[ArgOverride]) -> darling::Result<()> {
for ovr in overrides {
let arg = if let Some(pos) = ovr.pos {
spec.args.get_mut(pos)
.ok_or_else(|| darling::Error::custom(format!("Argument position {} out of range", pos)))?
} else {
spec.args.iter_mut()
.find(|a| a.name == ovr.name)
.ok_or_else(|| darling::Error::custom(format!("Argument '{}' not found", ovr.name)))?
};
if ovr.pos.is_some() && arg.name != ovr.name {
return Err(darling::Error::custom(
format!("Position and name mismatch: expected '{}', got '{}'", arg.name, ovr.name)
));
}
if let Some(ty) = &ovr.ty {
arg.ty = Some(ty.clone());
}
if let Some(ref desc) = ovr.description {
arg.description = Some(desc.clone());
}
}
Ok(())
}
fn apply_return_overrides(spec: &mut FnSpec, overrides: &[ReturnOverride]) -> darling::Result<()> {
let mut main_override_seen = false;
let mut status_codes = std::collections::HashSet::new();
for ovr in overrides {
if ovr.status.is_some() && ovr.ty.is_none() {
return Err(darling::Error::custom(
format!("returns() with status = {} must include ty field", ovr.status.unwrap())
));
}
if ovr.ty.is_none() {
if main_override_seen {
return Err(darling::Error::custom(
"Multiple returns() without ty field - only one can modify the signature return"
));
}
main_override_seen = true;
spec.returns.push(FnReturn {
ty: None, status: ovr.status,
description: ovr.description.clone(),
});
} else {
if let Some(status) = ovr.status {
if !status_codes.insert(status) {
return Err(darling::Error::custom(
format!("Duplicate status code {} - each status can only have one return type", status)
));
}
}
spec.returns.push(FnReturn {
ty: ovr.ty.clone(),
status: ovr.status,
description: ovr.description.clone(),
});
}
}
Ok(())
}
pub fn generate_bundle_part<T: darling::FromMeta + Default>(
attr: TokenStream,
item: TokenStream,
attr_name: &str,
conf_builder: fn(&T, &FnSpec) -> Result<proc_macro2::TokenStream, syn::Error>,
) -> TokenStream {
let attr2: proc_macro2::TokenStream = attr.into();
let item2: proc_macro2::TokenStream = item.into();
match generate_impl(attr2, item2, attr_name, conf_builder) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
pub(crate) fn generate_impl<T: darling::FromMeta + Default>(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
attr_name: &str,
conf_builder: fn(&T, &FnSpec) -> Result<proc_macro2::TokenStream, syn::Error>,
) -> Result<proc_macro2::TokenStream, syn::Error> {
let mut spec = extract_func_spec(&item, attr_name)?;
let conf = parse_and_apply_metadata::<T>(attr, &mut spec)?;
let conf_tokens = conf_builder(&conf, &spec)?;
let patch_chain = build_patch_chain(&spec);
Ok(emit_registration(&spec, attr_name, &item, &conf_tokens, &patch_chain))
}
fn parse_and_apply_metadata<T: darling::FromMeta + Default>(
attr: proc_macro2::TokenStream,
spec: &mut FnSpec,
) -> Result<T, syn::Error> {
let nested_meta = darling::ast::NestedMeta::parse_meta_list(attr)
.map_err(|e| syn::Error::new(e.span(), e))?;
if nested_meta.is_empty() {
return Ok(T::default());
}
parse_and_apply_overrides::<T>(&nested_meta, spec)
.map_err(|e| syn::Error::new(Span::call_site(), format!("{}", e)))
}
fn emit_registration(
spec: &FnSpec,
attr_name: &str,
item: &proc_macro2::TokenStream,
conf_tokens: &proc_macro2::TokenStream,
patch_chain: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let handler_fn = if spec.method {
let method_name = format!("Self::{}", spec.name);
syn::Ident::new(&method_name, Span::call_site())
} else {
syn::Ident::new(&spec.name, Span::call_site())
};
let wrapper_fn = syn::Ident::new(&format!("__bundle_part_{}", spec.name), Span::call_site());
let factory = syn::Ident::new(attr_name, Span::call_site());
quote! {
#item
#[allow(non_snake_case)]
#[doc(hidden)]
fn #wrapper_fn() -> ::uxar::bundles::BundlePart {
::uxar::bundles::#factory(#handler_fn, #conf_tokens) #patch_chain
}
}
}
pub(crate) fn build_patch_chain(spec: &FnSpec) -> proc_macro2::TokenStream {
let arg_patches = spec.args.iter()
.map(build_arg_patch);
let return_patches = spec.returns.iter()
.map(build_return_patch);
let desc_patch = if let Some(desc) = &spec.description {
quote! { .description(#desc) }
} else {
quote! {}
};
quote! {
.patch(::uxar::callables::PatchOp::new()
#desc_patch
#(#arg_patches)*
#(#return_patches)*
)
}
}
pub(crate) fn build_arg_patch(arg: &FnArg) -> proc_macro2::TokenStream {
let pos = arg.pos;
let name = &arg.name;
let idx_lit = syn::LitInt::new(&pos.to_string(), Span::call_site());
let mut patch = quote! { .arg(#idx_lit).name(#name) };
if let Some(ty) = &arg.ty {
patch = quote! { #patch.typed::<#ty>() };
}
if let Some(desc) = &arg.description {
patch = quote! { #patch.doc(#desc) };
}
quote! { #patch.done() }
}
pub(crate) fn build_return_patch(ret: &FnReturn) -> proc_macro2::TokenStream {
let mut patch = if let Some(ty) = &ret.ty {
quote! { .append().typed::<#ty>() }
} else {
quote! { .ret() }
};
if let Some(status) = ret.status {
let status_lit = syn::LitInt::new(&status.to_string(), Span::call_site());
patch = quote! { #patch.status(#status_lit) };
}
if let Some(desc) = &ret.description {
patch = quote! { #patch.doc(#desc) };
}
quote! { #patch.done() }
}
#[cfg(test)]
#[path = "bundlepart_tests.rs"]
mod bundlepart_tests;