1use heck::ToSnakeCase;
2use proc_macro::TokenStream;
3use quote::{format_ident, quote};
4use syn::{ItemImpl, parse_macro_input};
5
6use std::path::PathBuf;
7
8fn 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 let input = parse_macro_input!(item as ItemImpl);
17 let self_ty = &input.self_ty;
18
19 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 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 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 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 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 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 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 let method_quote = match (req_stream, resp_stream) {
158 (false, false) => {
159 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 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 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 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 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}