const_tweaker_attribute/
lib.rs

1use darling::FromMeta;
2use proc_macro::TokenStream;
3use quote::{format_ident, quote, ToTokens};
4use std::{i16, i32, i64, i8, u16, u32, u64, u8, usize};
5use syn::{
6    parse_macro_input, spanned::Spanned, AttributeArgs, Error, Expr, ItemConst, Type,
7    Type::Reference,
8};
9
10type TokenStream2 = proc_macro2::TokenStream;
11
12/// The metadata for rendering the web GUI.
13#[derive(Debug, FromMeta)]
14struct Metadata<T>
15where
16    T: FromMeta,
17{
18    #[darling(default)]
19    min: Option<T>,
20    #[darling(default)]
21    max: Option<T>,
22    #[darling(default)]
23    step: Option<T>,
24}
25
26impl<T: FromMeta> Metadata<T> {
27    pub fn from_attributes(args: AttributeArgs) -> Result<Self, TokenStream> {
28        match Metadata::from_list(&args) {
29            Ok(v) => Ok(v),
30            Err(e) => Err(TokenStream::from(e.write_errors())),
31        }
32    }
33}
34
35/// Convert a given type to a const_tweaker Field with metadata.
36fn field_init<T>(
37    field_type: &str,
38    ty: &Type,
39    metadata: Metadata<T>,
40    default_value: Expr,
41    default_min: T,
42    default_max: T,
43    default_step: T,
44) -> Result<TokenStream2, TokenStream>
45where
46    T: FromMeta + ToTokens,
47{
48    let min = metadata.min.unwrap_or(default_min);
49    let max = metadata.max.unwrap_or(default_max);
50    let step = metadata.step.unwrap_or(default_step);
51
52    Ok(match field_type {
53        "f32" => quote! {
54            const_tweaker::Field::F32 {
55                value: #default_value as f32,
56                min: #min,
57                max: #max,
58                step: #step,
59
60                module: module_path!().to_string(),
61                file: file!().to_string(),
62                line: line!(),
63            }
64        },
65        "f64" => quote! {
66            const_tweaker::Field::F64 {
67                value: #default_value,
68                min: #min,
69                max: #max,
70                step: #step,
71
72                module: module_path!().to_string(),
73                file: file!().to_string(),
74                line: line!(),
75            }
76        },
77        "i8" => quote! {
78            const_tweaker::Field::I8 {
79                value: #default_value,
80                min: #min,
81                max: #max,
82                step: #step,
83
84                module: module_path!().to_string(),
85                file: file!().to_string(),
86                line: line!(),
87            }
88        },
89        "u8" => quote! {
90            const_tweaker::Field::U8 {
91                value: #default_value,
92                min: #min,
93                max: #max,
94                step: #step,
95
96                module: module_path!().to_string(),
97                file: file!().to_string(),
98                line: line!(),
99            }
100        },
101        "i16" => quote! {
102            const_tweaker::Field::I16 {
103                value: #default_value,
104                min: #min,
105                max: #max,
106                step: #step,
107
108                module: module_path!().to_string(),
109                file: file!().to_string(),
110                line: line!(),
111            }
112        },
113        "u16" => quote! {
114            const_tweaker::Field::U16 {
115                value: #default_value,
116                min: #min,
117                max: #max,
118                step: #step,
119
120                module: module_path!().to_string(),
121                file: file!().to_string(),
122                line: line!(),
123            }
124        },
125        "i32" => quote! {
126            const_tweaker::Field::I32 {
127                value: #default_value,
128                min: #min,
129                max: #max,
130                step: #step,
131
132                module: module_path!().to_string(),
133                file: file!().to_string(),
134                line: line!(),
135            }
136        },
137        "u32" => quote! {
138            const_tweaker::Field::U32 {
139                value: #default_value,
140                min: #min,
141                max: #max,
142                step: #step,
143
144                module: module_path!().to_string(),
145                file: file!().to_string(),
146                line: line!(),
147            }
148        },
149        "i64" => quote! {
150            const_tweaker::Field::I64 {
151                value: #default_value,
152                min: #min,
153                max: #max,
154                step: #step,
155
156                module: module_path!().to_string(),
157                file: file!().to_string(),
158                line: line!(),
159            }
160        },
161        "u64" => quote! {
162            const_tweaker::Field::U64 {
163                value: #default_value,
164                min: #min,
165                max: #max,
166                step: #step,
167
168                module: module_path!().to_string(),
169                file: file!().to_string(),
170                line: line!(),
171            }
172        },
173        "usize" => quote! {
174            const_tweaker::Field::Usize {
175                value: #default_value,
176                min: #min,
177                max: #max,
178                step: #step,
179
180                module: module_path!().to_string(),
181                file: file!().to_string(),
182                line: line!(),
183            }
184        },
185        "bool" => quote! {
186            const_tweaker::Field::Bool {
187                value: #default_value,
188
189                module: module_path!().to_string(),
190                file: file!().to_string(),
191                line: line!(),
192            }
193        },
194        "str" => quote! {
195            const_tweaker::Field::String {
196                value: #default_value.to_string(),
197
198                module: module_path!().to_string(),
199                file: file!().to_string(),
200                line: line!(),
201            }
202        },
203        _ => {
204            return mismatching_type_error(&ty);
205        }
206    })
207}
208
209/// Convert a given type to a const_tweaker Field type.
210fn field_name(field_type: &str, ty: &Type) -> Result<TokenStream2, TokenStream> {
211    match field_type {
212        "f32" => Ok(quote! { const_tweaker::Field::F32 }),
213        "f64" => Ok(quote! { const_tweaker::Field::F64 }),
214        "i8" => Ok(quote! { const_tweaker::Field::I8 }),
215        "u8" => Ok(quote! { const_tweaker::Field::U8 }),
216        "i16" => Ok(quote! { const_tweaker::Field::I16 }),
217        "u16" => Ok(quote! { const_tweaker::Field::U16 }),
218        "i32" => Ok(quote! { const_tweaker::Field::I32 }),
219        "u32" => Ok(quote! { const_tweaker::Field::U32 }),
220        "i64" => Ok(quote! { const_tweaker::Field::I64 }),
221        "u64" => Ok(quote! { const_tweaker::Field::U64 }),
222        "usize" => Ok(quote! { const_tweaker::Field::Usize }),
223        "bool" => Ok(quote! { const_tweaker::Field::Bool }),
224        "str" => Ok(quote! { const_tweaker::Field::String }),
225        _ => mismatching_type_error(&ty),
226    }
227}
228
229/// Get the field type as a string.
230fn field_type(ty: &Type) -> Result<String, TokenStream> {
231    if let Type::Path(type_path) = &*ty {
232        match type_path.path.get_ident() {
233            Some(type_ident) => Ok(type_ident.to_string()),
234            None => mismatching_type_error(&ty),
235        }
236    } else {
237        mismatching_type_error(&ty)
238    }
239}
240
241/// The error message when there's a type mismatch.
242fn mismatching_type_error<T>(ty: &Type) -> Result<T, TokenStream> {
243    Err(TokenStream::from(
244        Error::new(
245            ty.span(),
246            "expected bool, &str, f32, f64, i8, u8, i16, u16, i32, u32, i64, u64, i128, u128 or usize, other types are not supported in const_tweaker (yet)",
247        )
248        .to_compile_error(),
249    ))
250}
251
252/// Proc macro call but with a result, which allows the use of `?`.
253fn tweak_impl(args: AttributeArgs, input: ItemConst) -> Result<TokenStream, TokenStream> {
254    let name = input.ident;
255    let init_name = format_ident!("{}_init", name);
256    let ty = if let Reference(type_ref) = *input.ty {
257        type_ref.elem
258    } else {
259        input.ty
260    };
261    let field_type = field_type(&*ty)?;
262    let field_name = field_name(&field_type, &*ty)?;
263    let field_init = match &*field_type {
264        "f32" => field_init::<f32>(
265            &field_type,
266            &*ty,
267            Metadata::from_attributes(args)?,
268            *input.expr,
269            0.0,
270            1.0,
271            0.001,
272        )?,
273        "f64" => field_init::<f64>(
274            &field_type,
275            &*ty,
276            Metadata::from_attributes(args)?,
277            *input.expr,
278            0.0,
279            1.0,
280            0.001,
281        )?,
282        "i8" => field_init::<i8>(
283            &field_type,
284            &*ty,
285            Metadata::from_attributes(args)?,
286            *input.expr,
287            i8::MIN,
288            i8::MAX,
289            1,
290        )?,
291        "u8" => field_init::<u8>(
292            &field_type,
293            &*ty,
294            Metadata::from_attributes(args)?,
295            *input.expr,
296            u8::MIN,
297            u8::MAX,
298            1,
299        )?,
300        "i16" => field_init::<i16>(
301            &field_type,
302            &*ty,
303            Metadata::from_attributes(args)?,
304            *input.expr,
305            i16::MIN,
306            i16::MAX,
307            1,
308        )?,
309        "u16" => field_init::<u16>(
310            &field_type,
311            &*ty,
312            Metadata::from_attributes(args)?,
313            *input.expr,
314            u16::MIN,
315            u16::MAX,
316            1,
317        )?,
318        "i32" => field_init::<i32>(
319            &field_type,
320            &*ty,
321            Metadata::from_attributes(args)?,
322            *input.expr,
323            i32::MIN,
324            i32::MAX,
325            1,
326        )?,
327        "u32" => field_init::<u32>(
328            &field_type,
329            &*ty,
330            Metadata::from_attributes(args)?,
331            *input.expr,
332            u32::MIN,
333            u32::MAX,
334            1,
335        )?,
336        "i64" => field_init::<i64>(
337            &field_type,
338            &*ty,
339            Metadata::from_attributes(args)?,
340            *input.expr,
341            i64::MIN,
342            i64::MAX,
343            1,
344        )?,
345        "u64" => field_init::<u64>(
346            &field_type,
347            &*ty,
348            Metadata::from_attributes(args)?,
349            *input.expr,
350            u64::MIN,
351            u64::MAX,
352            1,
353        )?,
354        "usize" => field_init::<usize>(
355            &field_type,
356            &*ty,
357            Metadata::from_attributes(args)?,
358            *input.expr,
359            usize::MIN,
360            usize::MAX,
361            1,
362        )?,
363        "bool" => field_init(
364            &field_type,
365            &*ty,
366            Metadata::from_attributes(args)?,
367            *input.expr,
368            0,
369            0,
370            0,
371        )?,
372        "str" => field_init(
373            &field_type,
374            &*ty,
375            Metadata::from_attributes(args)?,
376            *input.expr,
377            0,
378            0,
379            0,
380        )?,
381        _ => {
382            return mismatching_type_error(&ty);
383        }
384    };
385
386    let type_impls = if field_type == "str" {
387        quote! {
388            impl std::convert::From<#name> for &#ty {
389                fn from(original: #name) -> &'static #ty {
390                    original.get()
391                }
392            }
393        }
394    } else {
395        quote! {
396            impl std::convert::From<#name> for #ty {
397                fn from(original: #name) -> #ty {
398                    *original.get()
399                }
400            }
401        }
402    };
403
404    let result = quote! {
405        #[allow(non_camel_case_types)]
406        #[doc(hidden)]
407        #[derive(Copy, Clone)]
408        pub struct #name {
409            __private_field: ()
410        }
411
412        impl #name {
413            pub fn get(&self) -> &'static #ty {
414                // Retrieve the value from the datastore and unwrap it
415                match const_tweaker::DATA.get(concat!(module_path!(), "::", stringify!(#name))).expect("Value should have been added already").value() {
416                    #field_name { ref value, .. } => unsafe {
417                        // Make the reference static, so it leaks, but that shouldn't matter
418                        // because there will always be one reference since the dashmap is global
419                        std::mem::transmute::<&#ty, &'static #ty>(value as &#ty)
420                    },
421                    _ => panic!("Type mismatch, this probably means there's a duplicate value in the map, please report an issue")
422                }
423            }
424        }
425
426        // Automatically unwrap the primitive value from the struct when dereferencing
427        impl std::ops::Deref for #name {
428            type Target = #ty;
429
430            fn deref(&self) -> &'static #ty {
431                self.get()
432            }
433        }
434
435        impl std::fmt::Debug for #name {
436            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
437                write!(f, "{:?}", self.get())
438            }
439        }
440
441        impl std::fmt::Display for #name {
442            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
443                write!(f, "{:?}", self.get())
444            }
445        }
446
447        #type_impls
448
449        // A static variable is created as an instance of the above defined struct
450        static #name: #name = #name { __private_field: () };
451
452        #[allow(non_snake_case)]
453        #[const_tweaker::ctor]
454        fn #init_name() {
455            // Insert the value when the module is loaded
456            const_tweaker::DATA.insert(concat!(module_path!(), "::", stringify!(#name)), #field_init);
457        }
458    };
459
460    Ok(result.into())
461}
462
463/// Expose a const variable to the web GUI so it can be changed from a live setting.
464#[proc_macro_attribute]
465pub fn tweak(args: TokenStream, input: TokenStream) -> TokenStream {
466    let args = parse_macro_input!(args as AttributeArgs);
467    let input = parse_macro_input!(input as ItemConst);
468
469    match tweak_impl(args, input) {
470        Ok(result) => result,
471        Err(err) => err,
472    }
473}