1#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
26
27use proc_macro::TokenStream;
28use proc_macro2::Span;
29use quote::quote;
30use syn::{
31 parse::{Parse, ParseStream},
32 parse_macro_input, parse_quote,
33 spanned::Spanned,
34 Data, DeriveInput, Error, Expr, Ident, LitInt, LitStr, Meta, Result,
35};
36
37mod kw {
38 syn::custom_keyword!(submittable);
39}
40
41struct JobbyVariantAttributes {
42 items: syn::punctuated::Punctuated<JobbyVariantAttributeItem, syn::Token![,]>,
43}
44
45impl Parse for JobbyVariantAttributes {
46 fn parse(input: ParseStream<'_>) -> Result<Self> {
47 Ok(Self {
48 items: input.parse_terminated(JobbyVariantAttributeItem::parse)?,
49 })
50 }
51}
52
53enum JobbyVariantAttributeItem {
54 Submittable(VariantSubmittableAttribute),
55}
56
57impl Parse for JobbyVariantAttributeItem {
58 fn parse(input: ParseStream<'_>) -> Result<Self> {
59 let lookahead = input.lookahead1();
60 if lookahead.peek(kw::submittable) {
61 input.parse().map(Self::Submittable)
62 } else {
63 Err(lookahead.error())
64 }
65 }
66}
67
68struct VariantSubmittableAttribute {
69 keyword: kw::submittable,
70}
71
72impl Parse for VariantSubmittableAttribute {
73 fn parse(input: ParseStream) -> Result<Self> {
74 Ok(Self {
75 keyword: input.parse()?,
76 })
77 }
78}
79
80impl Spanned for VariantSubmittableAttribute {
81 fn span(&self) -> Span {
82 self.keyword.span()
83 }
84}
85
86struct VariantInfo {
87 ident: Ident,
88 discriminant: Expr,
89 is_submittable: bool,
90 name: LitStr,
91}
92
93struct EnumInfo {
94 name: Ident,
95 repr: Ident,
96 client_id: LitInt,
97 client_name: LitStr,
98 client_module: Ident,
99 variants: Vec<VariantInfo>,
100}
101
102impl Parse for EnumInfo {
103 fn parse(input: ParseStream) -> Result<Self> {
104 Ok({
105 let input: DeriveInput = input.parse()?;
106 let name = input.ident;
107 let data = match input.data {
108 Data::Enum(data) => data,
109 Data::Union(data) => {
110 return Err(Error::new_spanned(
111 data.union_token,
112 "Expected enum but found union",
113 ))
114 }
115 Data::Struct(data) => {
116 return Err(Error::new_spanned(
117 data.struct_token,
118 "Expected enum but found struct",
119 ))
120 }
121 };
122
123 let mut maybe_repr: Option<Ident> = None;
124 let mut maybe_client_id: Option<LitInt> = None;
125 let mut maybe_client_name: Option<LitStr> = None;
126 let mut maybe_client_module: Option<Ident> = None;
127
128 for attr in input.attrs {
129 let Ok(Meta::List(meta_list)) = attr.parse_meta() else {
130 continue;
131 };
132 let Some(ident) = meta_list.path.get_ident() else {
133 continue;
134 };
135 if ident == "repr" {
136 let mut nested = meta_list.nested.iter();
137 if nested.len() != 1 {
138 return Err(Error::new_spanned(
139 attr,
140 "Expected exactly one `repr` argument",
141 ));
142 }
143 let repr = nested.next().expect("We checked the length above!");
144 let repr: Ident = parse_quote! {
145 #repr
146 };
147 if repr != "u8" {
148 return Err(Error::new_spanned(repr, "JobType must be repr(u8)"));
149 }
150 maybe_repr = Some(repr);
151 } else if ident == "job_type" {
152 let mut nested = meta_list.nested.iter();
153 if nested.len() != 3 {
154 return Err(Error::new_spanned(
155 attr,
156 "Expected exactly three `job_type` arguments",
157 ));
158 }
159 let client_id = nested.next().expect("We checked the length above!");
160 let client_id: LitInt = parse_quote! {
161 #client_id
162 };
163 maybe_client_id = Some(client_id);
164 let client_name = nested.next().expect("We checked the length above!");
165 let client_name: LitStr = parse_quote! {
166 #client_name
167 };
168 maybe_client_name = Some(client_name);
169 let client_module = nested.next().expect("We checked the length above!");
170 let client_module: Ident = parse_quote! {
171 #client_module
172 };
173 maybe_client_module = Some(client_module);
174 } else {
175 }
177 }
178
179 let repr = maybe_repr.ok_or_else(|| {
180 Error::new(Span::call_site(), "Expected exactly one repr(u8) argument!")
181 })?;
182 let client_id = maybe_client_id.ok_or_else(|| {
183 Error::new(
184 Span::call_site(),
185 "Expected to find valid client_id argument!",
186 )
187 })?;
188 let client_name = maybe_client_name.ok_or_else(|| {
189 Error::new(
190 Span::call_site(),
191 "Expected to find valid client_name argument!",
192 )
193 })?;
194 let client_module = maybe_client_module.ok_or_else(|| {
195 Error::new(
196 Span::call_site(),
197 "Expected to find valid client_module argument!",
198 )
199 })?;
200
201 let mut variants: Vec<VariantInfo> = vec![];
202
203 for variant in data.variants {
204 let ident = variant.ident.clone();
205
206 let discriminant = variant
207 .discriminant
208 .as_ref()
209 .map(|d| d.1.clone())
210 .ok_or_else(|| {
211 Error::new_spanned(
212 &variant,
213 "Variant must have a discriminant to ensure forward compatibility",
214 )
215 })?
216 .clone();
217
218 let mut is_submittable = false;
219
220 for attribute in &variant.attrs {
221 if attribute.path.is_ident("job_type") {
222 match attribute.parse_args_with(JobbyVariantAttributes::parse) {
223 Ok(variant_attributes) => {
224 for variant_attribute in variant_attributes.items {
225 match variant_attribute {
226 JobbyVariantAttributeItem::Submittable(_) => {
227 is_submittable = true;
228 }
229 }
230 }
231 }
232 Err(err) => {
233 return Err(Error::new_spanned(
234 attribute,
235 format!("Invalid attribute: {err}"),
236 ));
237 }
238 }
239 }
240 }
241
242 let name = LitStr::new(&ident.to_string(), ident.span());
243
244 variants.push(VariantInfo {
245 ident,
246 discriminant,
247 is_submittable,
248 name,
249 });
250 }
251
252 Self {
253 name,
254 repr,
255 client_id,
256 client_name,
257 client_module,
258 variants,
259 }
260 })
261 }
262}
263
264#[proc_macro_derive(JobType, attributes(job_type, submittable))]
265pub fn derive_job_type(input: TokenStream) -> TokenStream {
266 let enum_info = parse_macro_input!(input as EnumInfo);
267 let name = &enum_info.name;
268 let repr = &enum_info.repr;
269 let client_id = &enum_info.client_id;
270 let client_name = &enum_info.client_name;
271 let client_module = &enum_info.client_module;
272 let helper_module_name = Ident::new(
273 &format!("{}_{}", "jobby_init_", &name.to_string()),
274 name.span(),
275 );
276
277 let mut from_str_arms = Vec::new();
278 let mut metadata_arms = Vec::new();
279 for variant in enum_info.variants {
280 let ident = &variant.ident;
281 let output = &variant.name;
282 let is_submittable = &variant.is_submittable;
283 let discriminant = &variant.discriminant;
284 from_str_arms.push(quote! { #name::#ident => #output });
285 metadata_arms.push(quote! {
286 jobby::JobTypeMetadata {
287 unnamespaced_id: jobby::UnnamespacedJobType::from(Self::#ident).id(),
288 base_id: #discriminant,
289 client_id: #client_id,
290 name: #output,
291 client_name: #client_name,
292 is_submittable: #is_submittable,
293 }
294 });
295 }
296
297 TokenStream::from(quote! {
298 impl From<#name> for #repr {
299 #[inline]
300 fn from(enum_value: #name) -> Self {
301 enum_value as Self
302 }
303 }
304
305 impl From<#name> for &'static str {
306 fn from(enum_value: #name) -> Self {
307 match enum_value {
308 #(#from_str_arms),*
309 }
310 }
311 }
312
313 impl jobby::JobType for #name {
314 #[inline]
315 fn client_id() -> usize {
316 #client_id
317 }
318
319 fn list_metadata() -> Vec<jobby::JobTypeMetadata> {
320 vec![
321 #(#metadata_arms),*
322 ]
323 }
324 }
325
326 #[allow(non_snake_case)]
327 mod #helper_module_name {
328 use jobby::JobType;
329 pub fn initialize(
330 rocket: &jobby::rocket::Rocket<jobby::rocket::Build>,
331 ) -> Result<Box<dyn jobby::Module + Send + Sync>, jobby::Error> {
332 Ok(Box::new(<super::#client_module as jobby::Module>::initialize(rocket)?))
333 }
334 pub fn list_metadata() -> Vec<jobby::JobTypeMetadata> {
335 super::#name::list_metadata()
336 }
337 }
338
339 jobby::inventory::submit! {
340 jobby::ClientModule::new(#client_id, #client_name, #helper_module_name::initialize, #helper_module_name::list_metadata)
341 }
342 })
343}