1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3#![cfg_attr(
4 not(test),
5 deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
6)]
7
8use quote::quote;
9use syn::DeriveInput;
10
11pub fn impl_stem_newtype(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
12 let name = &input.ident;
13 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
14
15 match &input.data {
16 syn::Data::Struct(s) => match &s.fields {
17 syn::Fields::Unnamed(f) if f.unnamed.len() == 1 => {}
18 syn::Fields::Unnamed(_) | syn::Fields::Named(_) | syn::Fields::Unit => {
19 return Err(syn::Error::new_spanned(
20 name,
21 "requires tuple struct with one field",
22 ))
23 }
24 },
25 syn::Data::Enum(_) | syn::Data::Union(_) => {
26 return Err(syn::Error::new_spanned(
27 name,
28 "can only be applied to tuple structs",
29 ))
30 }
31 }
32
33 let mut clamp_min: Option<f64> = None;
34 let mut clamp_max: Option<f64> = None;
35 let mut default_value: f64 = 0.0;
36
37 for attr in &input.attrs {
38 if !attr.path().is_ident("stem") {
39 continue;
40 }
41 attr.parse_nested_meta(|meta| {
42 if meta.path.is_ident("clamp_min") {
43 clamp_min = Some(
44 meta.value()?
45 .parse::<syn::LitFloat>()?
46 .base10_parse::<f64>()?,
47 );
48 Ok(())
49 } else if meta.path.is_ident("clamp_max") {
50 clamp_max = Some(
51 meta.value()?
52 .parse::<syn::LitFloat>()?
53 .base10_parse::<f64>()?,
54 );
55 Ok(())
56 } else if meta.path.is_ident("default_value") {
57 default_value = meta
58 .value()?
59 .parse::<syn::LitFloat>()?
60 .base10_parse::<f64>()?;
61 Ok(())
62 } else {
63 Err(meta.error("unknown stem attribute"))
64 }
65 })?;
66 }
67
68 let clamp_expr = match (clamp_min, clamp_max) {
69 (Some(min), Some(max)) => quote! { value.clamp(#min, #max) },
70 (Some(min), None) => quote! { value.max(#min) },
71 (None, Some(max)) => quote! { value.min(#max) },
72 (None, None) => quote! { value },
73 };
74
75 Ok(quote! {
76 impl #impl_generics #name #ty_generics #where_clause {
77 pub fn new(value: f64) -> Self { Self(#clamp_expr) }
78 pub fn value(&self) -> f64 { self.0 }
79 }
80 impl #impl_generics Default for #name #ty_generics #where_clause {
81 fn default() -> Self { Self(#default_value) }
82 }
83 impl #impl_generics From<f64> for #name #ty_generics #where_clause {
84 fn from(value: f64) -> Self { Self::new(value) }
85 }
86 impl #impl_generics From<#name #ty_generics> for f64 #where_clause {
87 fn from(val: #name #ty_generics) -> f64 { val.0 }
88 }
89 })
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use syn::parse_quote;
96
97 #[test]
98 fn test_valid_struct() {
99 let input: DeriveInput = parse_quote! { struct Prob(f64); };
100 let res = impl_stem_newtype(&input);
101 assert!(res.is_ok());
102 }
103
104 #[test]
105 fn test_clamping_attributes() {
106 let input: DeriveInput = parse_quote! {
107 #[stem(clamp_min = 0.0, clamp_max = 1.0)]
108 struct Prob(f64);
109 };
110 let res = impl_stem_newtype(&input).unwrap().to_string(); let normalized: String = res.chars().filter(|c| !c.is_whitespace()).collect();
112 assert!(normalized.contains("clamp("), "Expected clamp in: {}", res);
113 }
114}