use heck::ToSnakeCase;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{ItemImpl, parse_macro_input};
use std::path::PathBuf;
fn trim_token(s: &str) -> String {
s.trim().trim_end_matches(';').trim().to_string()
}
#[proc_macro_attribute]
pub fn auto_impl_bridge(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemImpl);
let self_ty = &input.self_ty;
let trait_path = match input.trait_.as_ref() {
Some((_, path, _)) => path.clone(),
None => {
return quote! {
compile_error!("#[auto_impl_bridge] must be applied to `impl <Trait> for <Type> { }`");
}
.into();
}
};
let proto_lit = match syn::parse::<syn::LitStr>(attr) {
Ok(lit) => lit.value(),
Err(_) => {
return quote! {
compile_error!("expected a string literal path to proto, e.g. #[auto_impl_bridge(\"../proto/bridge.proto\")]");
}
.into();
}
};
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let proto_path = PathBuf::from(&manifest).join(&proto_lit);
let proto_src = match std::fs::read_to_string(&proto_path) {
Ok(s) => s,
Err(e) => {
let msg = format!(
"auto_impl_bridge: failed to read proto file at {}: {}. \n Provide correct path relative to crate root.",
proto_path.display(),
e
);
return quote! { compile_error!(#msg); }.into();
}
};
let mut rpcs = Vec::new();
let mut in_service = false;
for line in proto_src.lines() {
let line = line.trim();
if line.starts_with("service Bridge") {
in_service = true;
continue;
}
if in_service && line.starts_with("}") {
break;
}
if in_service && line.starts_with("rpc") {
let after_rpc = line["rpc ".len()..].trim();
let name_end = after_rpc.find('(').unwrap_or(after_rpc.len());
let proto_name = after_rpc[..name_end].trim().to_string();
if proto_name.is_empty() {
continue;
}
let rest = &after_rpc[name_end..];
let req_type = rest
.split_once('(')
.and_then(|(_, after)| after.split_once(')'))
.map(|(inside, _)| inside.trim().to_string())
.unwrap_or_default();
let (req_stream, req_type) = if req_type.starts_with("stream ") {
(true, trim_token(&req_type["stream ".len()..]))
} else {
(false, trim_token(&req_type))
};
let resp_section = rest
.split_once("returns")
.map(|(_, after)| after)
.unwrap_or("");
let resp_type = resp_section
.split_once('(')
.and_then(|(_, after)| after.split_once(')'))
.map(|(inside, _)| inside.trim().to_string())
.unwrap_or_default();
let (resp_stream, resp_type) = if resp_type.starts_with("stream ") {
(true, trim_token(&resp_type["stream ".len()..]))
} else {
(false, trim_token(&resp_type))
};
rpcs.push((proto_name, req_stream, req_type, resp_stream, resp_type));
}
}
let mut assoc_types = Vec::new();
let mut method_tokens = Vec::new();
for (proto_name, req_stream, req_type, resp_stream, resp_type) in rpcs {
let method_snake = proto_name.to_snake_case();
let method_ident = format_ident!("{}", method_snake);
let handler_ident = format_ident!("handle_{}", method_snake);
let req_ty_str = format!("tron_protos::{}", req_type);
let resp_ty_str = format!("tron_protos::{}", resp_type);
let req_ty: syn::Type = match syn::parse_str(&req_ty_str) {
Ok(t) => t,
Err(_) => {
let msg = format!(
"auto_impl_bridge: failed to parse request type `{}`",
req_ty_str
);
return quote! { compile_error!(#msg); }.into();
}
};
let resp_ty: syn::Type = match syn::parse_str(&resp_ty_str) {
Ok(t) => t,
Err(_) => {
let msg = format!(
"auto_impl_bridge: failed to parse response type `{}`",
resp_ty_str
);
return quote! { compile_error!(#msg); }.into();
}
};
if resp_stream {
let assoc_ident = format_ident!("{}Stream", proto_name);
let assoc_target = format_ident!("{}Stream", proto_name);
assoc_types.push(quote! {
type #assoc_ident = crate::#assoc_target;
});
}
let method_quote = match (req_stream, resp_stream) {
(false, false) => {
quote! {
#[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
async fn #method_ident(
&self,
request: tonic::Request<#req_ty>,
) -> Result<tonic::Response<#resp_ty>, tonic::Status> {
self.#handler_ident(request).await
}
}
}
(true, false) => {
quote! {
#[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
async fn #method_ident(
&self,
request: tonic::Request<tonic::Streaming<#req_ty>>,
) -> Result<tonic::Response<#resp_ty>, tonic::Status> {
self.#handler_ident(request).await
}
}
}
(false, true) => {
let assoc_ident = format_ident!("{}Stream", proto_name);
quote! {
#[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
async fn #method_ident(
&self,
request: tonic::Request<#req_ty>,
) -> Result<tonic::Response<Self::#assoc_ident>, tonic::Status> {
self.#handler_ident(request).await
}
}
}
(true, true) => {
let assoc_ident = format_ident!("{}Stream", proto_name);
quote! {
#[cfg_attr(any(debug_assertions, test), tracing::instrument(skip(self), fields(request = ?request.get_ref())))]
async fn #method_ident(
&self,
request: tonic::Request<tonic::Streaming<#req_ty>>,
) -> Result<tonic::Response<Self::#assoc_ident>, tonic::Status> {
self.#handler_ident(request).await
}
}
}
};
method_tokens.push(method_quote);
}
let expanded = quote! {
#[tonic::async_trait]
impl #trait_path for #self_ty {
#(#assoc_types)*
#(#method_tokens)*
}
};
expanded.into()
}