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}