tokio_scheduler_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::ToTokens;
3use syn::parse::Parser;
4use syn::Ident;
5
6macro_rules! into_compile_error {
7    ($($tt:tt)*) => {
8        syn::Error::new(proc_macro2::Span::call_site(), format!($($tt)*))
9            .to_compile_error()
10        .into()
11    };
12}
13
14/// This macro is used to annotate a struct or a function as a job.
15/// Every job marked by this macro can be automatically registered to the job manager by calling `auto_register_job` fn from the job manager.
16///
17/// The struct must implement the Job trait.
18///
19/// The function must have the signature `async fn(ctx: JobContext) -> anyhow::Result<JobReturn>`.
20///
21/// The function will be converted to a struct that implements the Job trait. Struct name will be
22/// converted to UpperCamelCase.
23///
24/// If you want to use custom name for the fn type job, you can use `name` argument.
25///
26/// ```rust
27/// # use tokio_scheduler_macro::job;
28/// #[job(name="CustomName")]
29/// # fn foo(){}
30/// ```
31///
32/// # Example for fn
33/// ```rust
34/// # use tokio_scheduler_macro::job;
35/// # use tokio_scheduler_types::job::{JobContext, JobReturn};
36///
37/// #[job]
38/// async fn example_job(ctx: JobContext) -> anyhow::Result<JobReturn> {
39///    println!("Hello from example job");
40///   Ok(JobReturn::default())
41/// }
42/// ```
43///
44/// The code above equivalent to:
45/// ```rust
46/// # use tokio_scheduler_types::job::{Job, JobContext, JobFuture, JobReturn};
47///
48/// struct ExampleJob;
49///
50/// impl Job for ExampleJob {
51///    fn get_job_name(&self) -> &'static str {
52///      "ExampleJob"
53/// }
54///
55/// fn execute(&self, ctx: JobContext) -> JobFuture {
56///   Box::pin(async move {
57///         println!("Hello from example job");
58///         Ok(JobReturn::default())
59///         })
60///     }
61/// }
62/// ```
63/// # Example for struct
64/// ```rust
65/// # use tokio_scheduler_macro::job;
66/// # use tokio_scheduler_types::job::{Job, JobContext, JobFuture, JobReturn};
67///
68/// #[job]
69/// struct ExampleJob;
70///
71/// impl Job for ExampleJob {
72///    fn get_job_name(&self) -> &'static str {
73///       "ExampleJob"
74///   }
75///
76///  fn execute(&self, ctx: JobContext) -> JobFuture {
77///     Box::pin(async move {
78///       println!("Hello from example job");
79///       Ok(JobReturn::default())
80///     })
81///     }
82///  }
83/// ```
84#[proc_macro_attribute]
85pub fn job(args: TokenStream, input: TokenStream) -> TokenStream {
86    let parsed_input = syn::parse::<syn::Item>(input).unwrap();
87
88    // Parse the arguments, match #[job(name="xxx")]
89    let args_parsed =
90        syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated.parse(args);
91
92    if args_parsed.is_err() {
93        return into_compile_error!("Invalid arguments.");
94    }
95
96    let args_parsed = args_parsed.unwrap();
97
98    let mut custom_name = None;
99
100    for arg in args_parsed {
101        match arg {
102            syn::Expr::Assign(assign) => {
103                let left = assign.left.to_token_stream().to_string();
104                let right = assign.right.to_token_stream().to_string();
105
106                if left == "name" {
107                    let name = right.trim_matches('"');
108                    if name.is_empty() {
109                        return into_compile_error!("Invalid name.");
110                    }
111
112                    custom_name = Some(name.to_owned());
113                } else {
114                    return into_compile_error!("Invalid arguments.");
115                }
116            }
117            _ => {
118                return into_compile_error!("Invalid arguments.");
119            }
120        }
121    }
122
123    match parsed_input {
124        syn::Item::Struct(item_struct) => {
125            if let Some(_) = custom_name {
126                return into_compile_error!("Struct with custom job name is not supported. Please edit get_job_name() fn directly.");
127            }
128
129            let name = item_struct.ident.to_owned();
130
131            let output = quote::quote! {
132                #item_struct
133
134                ::tokio_scheduler_rs::inventory::submit!(&#name as &dyn ::tokio_scheduler_rs::job::Job);
135            };
136            output.into()
137        }
138        syn::Item::Fn(item_fn) => {
139            // get first argument name
140            let first_arg = item_fn.sig.inputs.first().unwrap();
141            let first_arg = match first_arg {
142                syn::FnArg::Typed(pat) => pat,
143                _ => {
144                    return into_compile_error!("Invalid function signature.");
145                }
146            };
147
148            let first_arg = match first_arg.pat.as_ref() {
149                syn::Pat::Ident(pat) => pat.ident.to_owned(),
150                _ => {
151                    return into_compile_error!("Invalid function signature.");
152                }
153            };
154
155            let body = item_fn.block.clone().stmts;
156
157            let body = quote::quote! {
158                #(#body)*
159            };
160
161            let fn_name = match custom_name {
162                Some(name) => name,
163                None => convert_case::Converter::new()
164                    .to_case(convert_case::Case::UpperCamel)
165                    .convert(item_fn.sig.ident.to_string().as_str()),
166            };
167
168            let struct_ident = Ident::new(&fn_name, item_fn.sig.ident.span());
169
170            let fn_vis = item_fn.vis.clone();
171
172            let output = quote::quote! {
173
174                #fn_vis struct #struct_ident;
175
176                impl ::tokio_scheduler_rs::job::Job for #struct_ident {
177                    fn get_job_name(&self) -> &'static str {
178                        #fn_name
179                    }
180
181                    fn execute(&self, #first_arg: ::tokio_scheduler_rs::job::JobContext) -> ::tokio_scheduler_rs::job::JobFuture {
182                        Box::pin(async move {
183                            #body
184                        })
185                    }
186                }
187
188                ::tokio_scheduler_rs::inventory::submit!(&#struct_ident as &dyn ::tokio_scheduler_rs::job::Job);
189            };
190
191            output.into()
192        }
193        _ => {
194            into_compile_error!("Only struct and fn can be annotated with #[job]")
195        }
196    }
197}