Skip to main content

tron_macros/
lib.rs

1use heck::ToSnakeCase;
2use proc_macro::TokenStream;
3use quote::{format_ident, quote};
4use syn::{ItemImpl, parse_macro_input};
5
6use std::path::PathBuf;
7
8/// Trim trailing punctuation/whitespace, remove trailing ';' if present
9fn trim_token(s: &str) -> String {
10    s.trim().trim_end_matches(';').trim().to_string()
11}
12
13#[proc_macro_attribute]
14pub fn auto_impl_bridge(attr: TokenStream, item: TokenStream) -> TokenStream {
15    // Parse input impl block
16    let input = parse_macro_input!(item as ItemImpl);
17    let self_ty = &input.self_ty;
18
19    // Ensure trait is present (impl Trait for Type)
20    let trait_path = match input.trait_.as_ref() {
21        Some((_, path, _)) => path.clone(),
22        None => {
23            return quote! {
24                compile_error!("#[auto_impl_bridge] must be applied to `impl <Trait> for <Type> { }`");
25            }
26            .into();
27        }
28    };
29
30    // parse attribute literal: #[auto_impl_bridge("../proto/bridge.proto")]
31    let proto_lit = match syn::parse::<syn::LitStr>(attr) {
32        Ok(lit) => lit.value(),
33        Err(_) => {
34            return quote! {
35                compile_error!("expected a string literal path to proto, e.g. #[auto_impl_bridge(\"../proto/bridge.proto\")]");
36            }
37            .into();
38        }
39    };
40
41    // Resolve path relative to crate root where macro is invoked
42    let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
43    let proto_path = PathBuf::from(&manifest).join(&proto_lit);
44
45    let proto_src = match std::fs::read_to_string(&proto_path) {
46        Ok(s) => s,
47        Err(e) => {
48            let msg = format!(
49                "auto_impl_bridge: failed to read proto file at {}: {}. \n                 Provide correct path relative to crate root.",
50                proto_path.display(),
51                e
52            );
53            return quote! { compile_error!(#msg); }.into();
54        }
55    };
56
57    let mut rpcs = Vec::new();
58    let mut in_service = false;
59    for line in proto_src.lines() {
60        let line = line.trim();
61        if line.starts_with("service Bridge") {
62            in_service = true;
63            continue;
64        }
65        if in_service && line.starts_with("}") {
66            break;
67        }
68        if in_service && line.starts_with("rpc") {
69            let after_rpc = line["rpc ".len()..].trim();
70            let name_end = after_rpc.find('(').unwrap_or(after_rpc.len());
71            let proto_name = after_rpc[..name_end].trim().to_string();
72            if proto_name.is_empty() {
73                continue;
74            }
75
76            let rest = &after_rpc[name_end..];
77            let req_type = rest
78                .split_once('(')
79                .and_then(|(_, after)| after.split_once(')'))
80                .map(|(inside, _)| inside.trim().to_string())
81                .unwrap_or_default();
82
83            let (req_stream, req_type) = if req_type.starts_with("stream ") {
84                (true, trim_token(&req_type["stream ".len()..]))
85            } else {
86                (false, trim_token(&req_type))
87            };
88
89            let resp_section = rest
90                .split_once("returns")
91                .map(|(_, after)| after)
92                .unwrap_or("");
93            let resp_type = resp_section
94                .split_once('(')
95                .and_then(|(_, after)| after.split_once(')'))
96                .map(|(inside, _)| inside.trim().to_string())
97                .unwrap_or_default();
98
99            let (resp_stream, resp_type) = if resp_type.starts_with("stream ") {
100                (true, trim_token(&resp_type["stream ".len()..]))
101            } else {
102                (false, trim_token(&resp_type))
103            };
104
105            rpcs.push((proto_name, req_stream, req_type, resp_stream, resp_type));
106        }
107    }
108
109    // Generate tokens for methods and associated types
110    let mut assoc_types = Vec::new();
111    let mut method_tokens = Vec::new();
112
113    for (proto_name, req_stream, req_type, resp_stream, resp_type) in rpcs {
114        // method identifier in snake_case
115        let method_snake = proto_name.to_snake_case();
116        let method_ident = format_ident!("{}", method_snake);
117        let handler_ident = format_ident!("handle_{}", method_snake);
118
119        // request/response type paths generated by tonic are typically in crate::bridge::<Type>
120        // parse them as syn::Type
121        let req_ty_str = format!("tron_protos::{}", req_type);
122        let resp_ty_str = format!("tron_protos::{}", resp_type);
123
124        let req_ty: syn::Type = match syn::parse_str(&req_ty_str) {
125            Ok(t) => t,
126            Err(_) => {
127                let msg = format!(
128                    "auto_impl_bridge: failed to parse request type `{}`",
129                    req_ty_str
130                );
131                return quote! { compile_error!(#msg); }.into();
132            }
133        };
134
135        let resp_ty: syn::Type = match syn::parse_str(&resp_ty_str) {
136            Ok(t) => t,
137            Err(_) => {
138                let msg = format!(
139                    "auto_impl_bridge: failed to parse response type `{}`",
140                    resp_ty_str
141                );
142                return quote! { compile_error!(#msg); }.into();
143            }
144        };
145
146        // If response is streaming, add an associated type alias:
147        // type <ProtoName>Stream = crate::<ProtoName>Stream;
148        if resp_stream {
149            let assoc_ident = format_ident!("{}Stream", proto_name);
150            let assoc_target = format_ident!("{}Stream", proto_name);
151            assoc_types.push(quote! {
152                type #assoc_ident = crate::#assoc_target;
153            });
154        }
155
156        // Build method signature based on streaming flags
157        let method_quote = match (req_stream, resp_stream) {
158            (false, false) => {
159                // unary -> unary
160                quote! {
161                    #[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
162                    async fn #method_ident(
163                        &self,
164                        request: tonic::Request<#req_ty>,
165                    ) -> Result<tonic::Response<#resp_ty>, tonic::Status> {
166                        self.#handler_ident(request).await
167                    }
168                }
169            }
170            (true, false) => {
171                // client streaming -> unary response
172                quote! {
173                    #[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
174                    async fn #method_ident(
175                        &self,
176                        request: tonic::Request<tonic::Streaming<#req_ty>>,
177                    ) -> Result<tonic::Response<#resp_ty>, tonic::Status> {
178                        self.#handler_ident(request).await
179                    }
180                }
181            }
182            (false, true) => {
183                // unary -> server streaming. return type uses associated type
184                let assoc_ident = format_ident!("{}Stream", proto_name);
185                quote! {
186                    #[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
187                    async fn #method_ident(
188                        &self,
189                        request: tonic::Request<#req_ty>,
190                    ) -> Result<tonic::Response<Self::#assoc_ident>, tonic::Status> {
191                        self.#handler_ident(request).await
192                    }
193                }
194            }
195            (true, true) => {
196                // bidi streaming -> req is Streaming<T>, resp is associated stream type
197                let assoc_ident = format_ident!("{}Stream", proto_name);
198                quote! {
199                    #[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
200                    async fn #method_ident(
201                        &self,
202                        request: tonic::Request<tonic::Streaming<#req_ty>>,
203                    ) -> Result<tonic::Response<Self::#assoc_ident>, tonic::Status> {
204                        self.#handler_ident(request).await
205                    }
206                }
207            }
208        };
209
210        method_tokens.push(method_quote);
211    }
212
213    // Emit the impl
214    let expanded = quote! {
215        #[tonic::async_trait]
216        impl #trait_path for #self_ty {
217            #(#assoc_types)*
218            #(#method_tokens)*
219        }
220    };
221
222    expanded.into()
223}