Skip to main content

inspect_derive/
lib.rs

1//! Derive macro for the `Inspect` trait.
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{Data, DeriveInput, Fields, parse_macro_input};
6
7#[proc_macro_derive(Inspect, attributes(inspect))]
8pub fn derive_inspect(input: TokenStream) -> TokenStream {
9    let input = parse_macro_input!(input as DeriveInput);
10
11    let name = &input.ident;
12    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
13
14    // Build where clause with Inspect bounds
15    let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::parse_quote!(where));
16
17    for param in &input.generics.params {
18        if let syn::GenericParam::Type(type_param) = param {
19            let ident = &type_param.ident;
20            where_clause.predicates.push(syn::parse_quote!(#ident: inspect_core::Inspect));
21        }
22    }
23
24    let inspect_impl = match &input.data {
25        Data::Struct(data_struct) => impl_struct(name, &data_struct.fields),
26        Data::Enum(data_enum) => impl_enum(name, data_enum),
27        Data::Union(_) => {
28            return syn::Error::new_spanned(name, "Inspect cannot be derived for unions")
29                .to_compile_error()
30                .into();
31        }
32    };
33
34    let expanded = quote! {
35        impl #impl_generics inspect_core::Inspect for #name #ty_generics #where_clause {
36            fn inspect(&self, cx: &mut inspect_core::InspectCx<'_>) -> inspect_core::ValueRef<'_> {
37                #inspect_impl
38            }
39        }
40    };
41
42    TokenStream::from(expanded)
43}
44
45fn impl_struct(name: &syn::Ident, fields: &Fields) -> proc_macro2::TokenStream {
46    let type_name = name.to_string();
47
48    match fields {
49        Fields::Named(fields_named) => {
50            let field_inspections: Vec<_> = fields_named
51                .named
52                .iter()
53                .enumerate()
54                .filter_map(|(idx, field)| {
55                    let attrs = FieldAttributes::parse(&field.attrs);
56
57                    if attrs.skip {
58                        return None;
59                    }
60
61                    let field_name = field.ident.as_ref().unwrap();
62                    let field_name_str = attrs.rename.unwrap_or_else(|| field_name.to_string());
63
64                    let sensitivity = match (attrs.secret, attrs.sensitive) {
65                        (true, _) => quote!(inspect_core::Sensitivity::Secret),
66                        (_, true) => quote!(inspect_core::Sensitivity::Sensitive),
67                        _ => quote!(inspect_core::Sensitivity::Normal),
68                    };
69
70                    Some(quote! {
71                        (
72                            inspect_core::FieldInfo::named(#field_name_str, #idx)
73                                .with_sensitivity(#sensitivity),
74                            self.#field_name.inspect(cx)
75                        )
76                    })
77                })
78                .collect();
79
80            quote! {
81                cx.visit_node();
82                let fields = vec![#(#field_inspections),*];
83
84                inspect_core::ValueRef::with_children(
85                    inspect_core::Kind::Struct,
86                    inspect_core::TypeInfo::new(#type_name),
87                    inspect_core::Children::direct(fields),
88                )
89            }
90        }
91        Fields::Unnamed(fields_unnamed) => {
92            let field_count = fields_unnamed.unnamed.len();
93            let field_inspections: Vec<_> = (0..field_count)
94                .map(|idx| {
95                    let idx_token = syn::Index::from(idx);
96                    quote! {
97                        (
98                            inspect_core::FieldInfo::tuple(#idx),
99                            self.#idx_token.inspect(cx)
100                        )
101                    }
102                })
103                .collect();
104
105            quote! {
106                cx.visit_node();
107                let fields = vec![#(#field_inspections),*];
108
109                inspect_core::ValueRef::with_children(
110                    inspect_core::Kind::TupleStruct,
111                    inspect_core::TypeInfo::new(#type_name),
112                    inspect_core::Children::direct(fields),
113                )
114            }
115        }
116        Fields::Unit => {
117            quote! {
118                cx.visit_node();
119                inspect_core::ValueRef::with_type(
120                    inspect_core::Kind::Struct,
121                    inspect_core::TypeInfo::new(#type_name),
122                )
123            }
124        }
125    }
126}
127
128fn impl_enum(name: &syn::Ident, data_enum: &syn::DataEnum) -> proc_macro2::TokenStream {
129    let type_name = name.to_string();
130
131    let variant_arms: Vec<_> = data_enum
132        .variants
133        .iter()
134        .enumerate()
135        .map(|(variant_idx, variant)| {
136            let variant_name = &variant.ident;
137            let variant_name_str = variant_name.to_string();
138
139            match &variant.fields {
140                Fields::Named(fields) => {
141                    let field_names: Vec<_> = fields.named.iter()
142                        .map(|f| f.ident.as_ref().unwrap())
143                        .collect();
144
145                    let field_inspections: Vec<_> = fields.named.iter()
146                        .enumerate()
147                        .map(|(idx, field)| {
148                            let field_name = field.ident.as_ref().unwrap();
149                            let field_name_str = field_name.to_string();
150
151                            quote! {
152                                (
153                                    inspect_core::FieldInfo::named(#field_name_str, #idx),
154                                    #field_name.inspect(cx)
155                                )
156                            }
157                        })
158                        .collect();
159
160                    quote! {
161                        #name::#variant_name { #(#field_names),* } => {
162                            cx.visit_node();
163                            let variant = inspect_core::VariantInfo::new(#variant_name_str, #variant_idx);
164                            let fields = vec![#(#field_inspections),*];
165
166                            inspect_core::ValueRef::with_children(
167                                inspect_core::Kind::Enum,
168                                inspect_core::TypeInfo::new(#type_name),
169                                inspect_core::Children::direct(fields),
170                            )
171                            .with_variant(variant)
172                        }
173                    }
174                }
175                Fields::Unnamed(fields) => {
176                    let field_count = fields.unnamed.len();
177                    let field_bindings: Vec<_> = (0..field_count)
178                        .map(|i| quote::format_ident!("field_{}", i))
179                        .collect();
180
181                    let field_inspections: Vec<_> = field_bindings.iter()
182                        .enumerate()
183                        .map(|(idx, binding)| {
184                            quote! {
185                                (
186                                    inspect_core::FieldInfo::tuple(#idx),
187                                    #binding.inspect(cx)
188                                )
189                            }
190                        })
191                        .collect();
192
193                    quote! {
194                        #name::#variant_name(#(#field_bindings),*) => {
195                            cx.visit_node();
196                            let variant = inspect_core::VariantInfo::new(#variant_name_str, #variant_idx);
197                            let fields = vec![#(#field_inspections),*];
198
199                            inspect_core::ValueRef::with_children(
200                                inspect_core::Kind::Enum,
201                                inspect_core::TypeInfo::new(#type_name),
202                                inspect_core::Children::direct(fields),
203                            )
204                            .with_variant(variant)
205                        }
206                    }
207                }
208                Fields::Unit => {
209                    quote! {
210                        #name::#variant_name => {
211                            cx.visit_node();
212                            let variant = inspect_core::VariantInfo::new(#variant_name_str, #variant_idx);
213
214                            inspect_core::ValueRef::with_children(
215                                inspect_core::Kind::Enum,
216                                inspect_core::TypeInfo::new(#type_name),
217                                inspect_core::Children::direct(vec![]),
218                            )
219                            .with_variant(variant)
220                        }
221                    }
222                }
223            }
224        })
225        .collect();
226
227    quote! {
228        match self {
229            #(#variant_arms),*
230        }
231    }
232}
233
234#[derive(Default)]
235struct FieldAttributes {
236    skip: bool,
237    secret: bool,
238    sensitive: bool,
239    rename: Option<String>,
240}
241
242impl FieldAttributes {
243    fn parse(attrs: &[syn::Attribute]) -> Self {
244        let mut result = Self::default();
245
246        for attr in attrs {
247            if !attr.path().is_ident("inspect") {
248                continue;
249            }
250
251            let _ = attr.parse_nested_meta(|meta| {
252                if meta.path.is_ident("skip") {
253                    result.skip = true;
254                } else if meta.path.is_ident("secret") {
255                    result.secret = true;
256                } else if meta.path.is_ident("sensitive") {
257                    result.sensitive = true;
258                } else if meta.path.is_ident("rename") {
259                    if let Ok(value) = meta.value() {
260                        if let Ok(s) = value.parse::<syn::LitStr>() {
261                            result.rename = Some(s.value());
262                        }
263                    }
264                }
265                Ok(())
266            });
267        }
268
269        result
270    }
271}