Skip to main content

protovalidate_buffa_macros/
lib.rs

1//! `#[connect_impl]` — inserts `req.validate()?` at the top of every Connect
2//! service handler method in an `impl` block whose request parameter is an
3//! `OwnedView<_>` or `connectrpc::ServiceRequest<'_, _>`. Single-site safety
4//! net: add it once to the service impl and every present-and-future handler is
5//! validated on entry.
6//!
7//! Non-handler `async fn`s inside the same `impl` block are left alone
8//! (they lack a recognized request parameter, so the macro skips them).
9
10use proc_macro::TokenStream;
11use proc_macro2::TokenStream as TokenStream2;
12use quote::quote;
13use syn::{Error, FnArg, ImplItem, ItemImpl, PatType, Type, TypePath, parse_macro_input};
14
15#[proc_macro_attribute]
16pub fn connect_impl(attr: TokenStream, input: TokenStream) -> TokenStream {
17    if !attr.is_empty() {
18        return Error::new_spanned(
19            TokenStream2::from(attr),
20            "protovalidate_buffa::connect_impl takes no arguments",
21        )
22        .to_compile_error()
23        .into();
24    }
25
26    let mut item = parse_macro_input!(input as ItemImpl);
27
28    for impl_item in &mut item.items {
29        if let ImplItem::Fn(f) = impl_item
30            && let Some(arg_ident) = find_request_arg(&f.sig)
31        {
32            let pv_ident =
33                proc_macro2::Ident::new("__protovalidate_buffa_req_owned", arg_ident.span());
34
35            let decode: syn::Stmt = syn::parse_quote! {
36                let #pv_ident = #arg_ident.to_owned_message();
37            };
38            let validate: syn::Stmt = syn::parse_quote! {
39                <_ as ::protovalidate_buffa::Validate>::validate(&#pv_ident)
40                    .map_err(::protovalidate_buffa::ValidationError::into_connect_error)?;
41            };
42
43            f.block.stmts.insert(0, decode);
44            f.block.stmts.insert(1, validate);
45        }
46    }
47
48    TokenStream::from(quote! { #item })
49}
50
51/// Returns the ident of the first request parameter whose type exposes
52/// `to_owned_message()`, such as `OwnedView<pb::CreateFooRequestView<'static>>`
53/// or `connectrpc::ServiceRequest<'_, pb::CreateFooRequest>`. Non-handler
54/// methods that lack such a parameter return `None`.
55fn find_request_arg(sig: &syn::Signature) -> Option<syn::Ident> {
56    for arg in &sig.inputs {
57        if let FnArg::Typed(PatType { pat, ty, .. }) = arg
58            && is_request_type(ty)
59            && let syn::Pat::Ident(pat_ident) = pat.as_ref()
60        {
61            return Some(pat_ident.ident.clone());
62        }
63    }
64    None
65}
66
67fn is_request_type(ty: &Type) -> bool {
68    if let Type::Path(TypePath { path, .. }) = ty
69        && let Some(last) = path.segments.last()
70    {
71        return last.ident == "OwnedView" || last.ident == "ServiceRequest";
72    }
73    false
74}