Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 86 additions & 43 deletions dapr-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use std::iter;

use proc_macro2::TokenTree;
use quote::{format_ident, quote};
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, LitStr, parse_macro_input};
use syn::{FnArg, Ident, ItemFn, LitStr, Pat, Type, parse_macro_input};

use proc_macro::TokenStream;

Expand Down Expand Up @@ -135,20 +134,11 @@ pub fn actor(_attr: TokenStream, item: TokenStream) -> TokenStream {
#[proc_macro_attribute]
pub fn topic(args: TokenStream, input: TokenStream) -> TokenStream {
let new_input = proc_macro2::TokenStream::from(input);
let mut iter = new_input.clone().into_iter().filter(|i| match i {
TokenTree::Group(_) => true,
TokenTree::Ident(_) => true,
TokenTree::Punct(_) => false,
TokenTree::Literal(_) => false,
});

let mut current = iter.next().unwrap();

while current.to_string() != "fn" {
current = iter.next().unwrap()
}

let name = iter.next().unwrap();
let topic_fn = match syn::parse2::<ItemFn>(new_input.clone()) {
Ok(item) => item,
Err(err) => return err.to_compile_error().into(),
};
let name = topic_fn.sig.ident.clone();

let struct_name = name
.to_string()
Expand All @@ -162,34 +152,20 @@ pub fn topic(args: TokenStream, input: TokenStream) -> TokenStream {
.collect::<Vec<String>>()
.join("");

let name_ident = Ident::new(name.to_string().as_str(), name.span());

let struct_name_ident = Ident::new(struct_name.as_str(), name.span());

let vars: Vec<String> = iter
.next()
.unwrap()
.to_string()
.replace(['(', ')'], "")
.split(':')
.enumerate()
.filter(|&(i, _)| i % 2 != 0)
.map(|(_, i)| i.trim().to_string())
.collect();

assert_eq!(vars.len(), 1, "Expected to only have one input variable");

let parse_statement = match vars[0] == *"String" {
true => {
quote! {
let message = message.to_string();
}
let input_ty = match topic_input_type(&topic_fn) {
Ok(ty) => ty,
Err(err) => return err.to_compile_error().into(),
};

let parse_statement = if is_string_type(&input_ty) {
quote! {
let message = message.to_string();
}
false => {
let type_ident = format_ident!("{}", vars[0]);
quote! {
let message: #type_ident = dapr::serde_json::from_str(message.to_string().as_str()).unwrap();
}
} else {
quote! {
let message: #input_ty = dapr::serde_json::from_str(message.to_string().as_str()).unwrap();
}
};

Expand Down Expand Up @@ -218,7 +194,7 @@ pub fn topic(args: TokenStream, input: TokenStream) -> TokenStream {

#parse_statement

#name_ident(message).await;
#name(message).await;

Ok(tonic::Response::new(TopicEventResponse::default()))
}
Expand All @@ -236,3 +212,70 @@ pub fn topic(args: TokenStream, input: TokenStream) -> TokenStream {

tokens.into()
}

fn topic_input_type(item: &ItemFn) -> syn::Result<Type> {
if item.sig.inputs.len() != 1 {
return Err(syn::Error::new_spanned(
&item.sig.inputs,
"Expected to only have one input variable",
));
}

match item.sig.inputs.first() {
Some(FnArg::Typed(pat_ty)) => {
if !matches!(&*pat_ty.pat, Pat::Ident(_)) {
return Err(syn::Error::new_spanned(
&pat_ty.pat,
"Expected a named input variable",
));
}
Ok((*pat_ty.ty).clone())
}
Some(FnArg::Receiver(receiver)) => Err(syn::Error::new_spanned(
receiver,
"Expected a named input variable",
)),
None => unreachable!("topic_input_type validates the number of inputs"),
}
}

fn is_string_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) if type_path.qself.is_none() => type_path
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "String"),
Comment on lines +247 to +248

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentionally added to allow String types defined by any other crates such as other_crate::String?

_ => false,
}
}

#[cfg(test)]
mod tests {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding tests, please enhance this with negative tests to confirm the above review assumptions.

use super::{is_string_type, topic_input_type};
use syn::parse_quote;

#[test]
fn topic_input_type_supports_fully_qualified_paths() {
let item: syn::ItemFn = parse_quote! {
async fn handle_a_event(order: serde_json::Value) {}
};

let ty = topic_input_type(&item).expect("type should parse");
let rendered = quote::quote!(#ty).to_string();

assert_eq!(rendered, "serde_json :: Value");
assert!(!is_string_type(&ty));
}

#[test]
fn topic_input_type_detects_string_by_last_path_segment() {
let item: syn::ItemFn = parse_quote! {
async fn handle_a_event(order: std::string::String) {}
};

let ty = topic_input_type(&item).expect("type should parse");

assert!(is_string_type(&ty));
}
}
Loading