pathmod_derive/
lib.rs

1//! Derive macros for Pathmod
2//!
3//! This crate provides two derives:
4//! - `#[derive(Accessor)]` for structs (named or tuple), generating const field accessors
5//!   like `acc_<field>()` or `acc_<idx>()`, plus reconstruction helpers `with_*`.
6//! - `#[derive(EnumAccess)]` for enums (MVP: tuple variants with exactly one field),
7//!   generating helpers like `is_<variant>`, `as_<variant>`, `as_<variant>_mut`,
8//!   `set_<variant>`, and `map_<variant>`.
9//!
10//! Most users should depend on the re-export crate `pathmod` and import:
11//! ```rust
12//! use pathmod::prelude::*;
13//! ```
14//!
15//! Example — struct accessors and composition
16//! ```rust
17//! use pathmod::prelude::*;
18//!
19//! #[derive(Accessor, Debug, PartialEq)]
20//! struct Bar { x: i32 }
21//!
22//! #[derive(Accessor, Debug, PartialEq)]
23//! struct Foo { a: i32, b: Bar }
24//!
25//! let mut foo = Foo { a: 1, b: Bar { x: 2 } };
26//! let acc_bx = Foo::acc_b().compose(Bar::acc_x());
27//! acc_bx.set_mut(&mut foo, |v| *v += 5);
28//! assert_eq!(foo.b.x, 7);
29//! ```
30//!
31//! Example — enum helpers (tuple single-field variants)
32//! ```rust
33//! use pathmod::prelude::*;
34//!
35//! #[derive(EnumAccess, Debug, PartialEq)]
36//! enum Msg { Int(i32), Text(String) }
37//!
38//! let mut m = Msg::Int(5);
39//! assert!(m.is_int());
40//! m.map_int(|v| *v += 10);
41//! assert_eq!(m, Msg::Int(15));
42//! m.set_text("hi".to_string());
43//! assert!(m.is_text());
44//! ```
45
46extern crate proc_macro;
47use proc_macro::TokenStream;
48use quote::{format_ident, quote};
49use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident};
50
51fn expand(input: DeriveInput) -> proc_macro2::TokenStream {
52    let ty_ident = input.ident;
53    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
54
55    match input.data {
56        Data::Struct(ref s) => match s.fields {
57            Fields::Named(ref fields_named) => {
58                let acc_fns = fields_named.named.iter().map(|f| {
59                    let fname: &Ident = f.ident.as_ref().unwrap();
60                    let acc_fn = format_ident!("acc_{}", fname);
61                    let fty = &f.ty;
62                    quote! {
63                        /// Accessor to the `#fname` field.
64                        pub const fn #acc_fn() -> pathmod::Accessor<#ty_ident #ty_generics, #fty> {
65                            let off = core::mem::offset_of!(#ty_ident #ty_generics, #fname) as isize;
66                            // SAFETY: `off` is computed from the field offset within the same allocation.
67                            unsafe { pathmod::Accessor::<#ty_ident #ty_generics, #fty>::from_offset(off) }
68                        }
69                    }
70                });
71
72                let with_fns = fields_named.named.iter().map(|f| {
73                    let fname: &Ident = f.ident.as_ref().unwrap();
74                    let with_fn = format_ident!("with_{}", fname);
75                    let fty = &f.ty;
76                    quote! {
77                        /// Return a new value with `#fname` replaced by `new_val`.
78                        ///
79                        /// This consumes `self` and reconstructs `Self` without cloning
80                        /// any other fields (they are moved). This is the building block
81                        /// for minimal-clone (actually zero-clone) reconstruction up the path.
82                        pub fn #with_fn(mut self, new_val: #fty) -> Self {
83                            self.#fname = new_val;
84                            self
85                        }
86                    }
87                });
88
89                quote! {
90                    impl #impl_generics #ty_ident #ty_generics #where_clause {
91                        #(#acc_fns)*
92                        #(#with_fns)*
93                    }
94                }
95            }
96            Fields::Unnamed(ref fields_unnamed) => {
97                let acc_fns = fields_unnamed.unnamed.iter().enumerate().map(|(i, f)| {
98                    let acc_fn = format_ident!("acc_{}", i);
99                    let fty = &f.ty;
100                    let index = syn::Index::from(i);
101                    quote! {
102                        /// Accessor to the tuple field at index #i.
103                        pub const fn #acc_fn() -> pathmod::Accessor<#ty_ident #ty_generics, #fty> {
104                            let off = core::mem::offset_of!(#ty_ident #ty_generics, #index) as isize;
105                            // SAFETY: `off` is computed from the field offset within the same allocation.
106                            unsafe { pathmod::Accessor::<#ty_ident #ty_generics, #fty>::from_offset(off) }
107                        }
108                    }
109                });
110                let with_fns = fields_unnamed.unnamed.iter().enumerate().map(|(i, f)| {
111                    let with_fn = format_ident!("with_{}", i);
112                    let fty = &f.ty;
113                    let index = syn::Index::from(i);
114                    quote! {
115                        /// Return a new value with tuple field at index #i replaced by `new_val`.
116                        ///
117                        /// Consumes `self` and reconstructs `Self` without cloning other fields.
118                        pub fn #with_fn(mut self, new_val: #fty) -> Self {
119                            self.#index = new_val;
120                            self
121                        }
122                    }
123                });
124                quote! {
125                    impl #impl_generics #ty_ident #ty_generics #where_clause {
126                        #(#acc_fns)*
127                        #(#with_fns)*
128                    }
129                }
130            }
131            Fields::Unit => {
132                let msg = "#[derive(Accessor)] does not support unit structs";
133                quote! { compile_error!(#msg); }
134            }
135        },
136        _ => {
137            let msg = "#[derive(Accessor)] can only be used on structs";
138            quote! { compile_error!(#msg); }
139        }
140    }
141}
142
143/// Derive field accessors for a struct (named or tuple).
144///
145/// Generates, for each field:
146/// - `pub const acc_*() -> pathmod::Accessor<Self, FieldTy>` accessors, and
147/// - `with_*` reconstruction helpers that consume `self` and replace just that field.
148///
149/// See the crate-level docs for usage examples.
150#[proc_macro_derive(Accessor)]
151pub fn accessor_derive(input: TokenStream) -> TokenStream {
152    let input: DeriveInput = parse_macro_input!(input as DeriveInput);
153    let ts = expand(input);
154    TokenStream::from(ts)
155}
156
157fn expand_enum(input: DeriveInput) -> proc_macro2::TokenStream {
158    // Note: Keep control flow linear to help coverage tools attribute regions cleanly.
159    let ty_ident = input.ident;
160    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
161
162    match input.data {
163        Data::Enum(en) => {
164            // Build method sets per variant for single-field variants only
165            let mut per_variant_tokens = Vec::new();
166            let mut error_msg: Option<&'static str> = None;
167            for v in en.variants.iter() {
168                let v_ident = &v.ident;
169                match &v.fields {
170                    Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
171                        let fty = &fields.unnamed.first().unwrap().ty;
172                        let is_fn = format_ident!("is_{}", v_ident.to_string().to_lowercase());
173                        let as_fn = format_ident!("as_{}", v_ident.to_string().to_lowercase());
174                        let as_mut_fn =
175                            format_ident!("as_{}_mut", v_ident.to_string().to_lowercase());
176                        let set_fn = format_ident!("set_{}", v_ident.to_string().to_lowercase());
177                        let map_fn = format_ident!("map_{}", v_ident.to_string().to_lowercase());
178                        per_variant_tokens.push(quote! {
179                            #[inline]
180                            pub fn #is_fn(&self) -> bool { matches!(self, Self::#v_ident(_)) }
181                            #[inline]
182                            pub fn #as_fn(&self) -> Option<& #fty> { if let Self::#v_ident(ref v) = self { Some(v) } else { None } }
183                            #[inline]
184                            pub fn #as_mut_fn(&mut self) -> Option<&mut #fty> { if let Self::#v_ident(ref mut v) = self { Some(v) } else { None } }
185                            #[inline]
186                            pub fn #set_fn(&mut self, val: #fty) { *self = Self::#v_ident(val); }
187                            #[inline]
188                            pub fn #map_fn(&mut self, f: impl FnOnce(&mut #fty)) { if let Self::#v_ident(ref mut v) = self { f(v); } }
189                        });
190                    }
191                    Fields::Named(fields) if fields.named.len() == 1 => {
192                        let _ = &fields; // keep pattern usage without warnings
193                        error_msg = Some("#[derive(EnumAccess)] currently supports only tuple variants with exactly one field; named-field single variants are not yet supported");
194                        break;
195                    }
196                    Fields::Unit => {
197                        error_msg = Some(
198                            "#[derive(EnumAccess)] does not support unit variants in this MVP",
199                        );
200                        break;
201                    }
202                    _ => {
203                        error_msg = Some("#[derive(EnumAccess)] supports only tuple variants with exactly one field");
204                        break;
205                    }
206                }
207            }
208            if let Some(msg) = error_msg {
209                return quote! { compile_error!(#msg); };
210            }
211            quote! {
212                impl #impl_generics #ty_ident #ty_generics #where_clause {
213                    #(#per_variant_tokens)*
214                }
215            }
216        }
217        _ => {
218            quote! { compile_error!("#[derive(EnumAccess)] can only be used on enums"); }
219        }
220    }
221}
222
223/// Derive helpers for enum variants (MVP: tuple variants with exactly one field).
224///
225/// Generates per-variant helpers: `is_*`, `as_*`, `as_*_mut`, `set_*`, `map_*`.
226/// See crate-level docs for examples and limitations.
227#[proc_macro_derive(EnumAccess)]
228pub fn enum_access_derive(input: TokenStream) -> TokenStream {
229    let input: DeriveInput = parse_macro_input!(input as DeriveInput);
230    let ts = expand_enum(input);
231    TokenStream::from(ts)
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use syn::parse_quote;
238
239    #[test]
240    fn expands_named_struct() {
241        let di: DeriveInput = parse_quote! {
242            struct S { a: i32, b: i64 }
243        };
244        let out = expand(di);
245        let s = out.to_string();
246        assert!(s.contains("acc_a"));
247        assert!(s.contains("acc_b"));
248    }
249
250    #[test]
251    fn expands_tuple_struct() {
252        let di: DeriveInput = parse_quote! {
253            struct P(i32, i64);
254        };
255        let out = expand(di);
256        let s = out.to_string();
257        assert!(s.contains("acc_0"));
258        assert!(s.contains("acc_1"));
259    }
260
261    #[test]
262    fn errors_on_unit_struct() {
263        let di: DeriveInput = parse_quote! { struct U; };
264        let out = expand(di);
265        let s = out.to_string();
266        assert!(s.contains("compile_error") && s.contains("does not support unit structs"));
267    }
268
269    #[test]
270    fn errors_on_enum() {
271        let di: DeriveInput = parse_quote! { enum E { A } };
272        let out = expand(di);
273        let s = out.to_string();
274        assert!(s.contains("compile_error") && s.contains("only be used on structs"));
275    }
276
277    // Additional unit tests for EnumAccess derive expansion
278    #[test]
279    fn enum_access_positive_single_field_tuple_variant() {
280        let di: DeriveInput = parse_quote! { enum Msg { Int(i32), Text(String) } };
281        let out = expand_enum(di);
282        let s = out.to_string();
283        // Check that methods for both variants appear
284        assert!(s.contains("is_int"));
285        assert!(s.contains("as_int"));
286        assert!(s.contains("as_int_mut"));
287        assert!(s.contains("set_int"));
288        assert!(s.contains("map_int"));
289        assert!(s.contains("is_text"));
290        assert!(s.contains("as_text"));
291        assert!(s.contains("as_text_mut"));
292        assert!(s.contains("set_text"));
293        assert!(s.contains("map_text"));
294    }
295
296    #[test]
297    fn enum_access_error_on_unit_variant() {
298        let di: DeriveInput = parse_quote! { enum E { A } };
299        let out = expand_enum(di);
300        let s = out.to_string();
301        assert!(s.contains("compile_error") && s.contains("does not support unit variants"));
302    }
303
304    #[test]
305    fn enum_access_error_on_multi_field_variant() {
306        let di: DeriveInput = parse_quote! { enum E { Both(i32, i32) } };
307        let out = expand_enum(di);
308        let s = out.to_string();
309        assert!(
310            s.contains("compile_error")
311                && s.contains("supports only tuple variants with exactly one field")
312        );
313    }
314
315    #[test]
316    fn enum_access_error_on_named_single_field_variant() {
317        let di: DeriveInput = parse_quote! { enum E { V { v: i32 } } };
318        let out = expand_enum(di);
319        let s = out.to_string();
320        assert!(
321            s.contains("compile_error") && s.contains("currently supports only tuple variants")
322        );
323    }
324
325    #[test]
326    fn enum_access_error_on_non_enum() {
327        let di: DeriveInput = parse_quote! { struct NotEnum { a: i32 } };
328        let out = expand_enum(di);
329        let s = out.to_string();
330        assert!(s.contains("compile_error") && s.contains("can only be used on enums"));
331    }
332
333    // Exercise generics and where-clause propagation in struct expansion
334    #[test]
335    fn expands_generics_with_where_clause() {
336        let di: DeriveInput = parse_quote! {
337            struct Wrap<T: Clone, U>
338            where
339                U: core::fmt::Debug,
340            {
341                t: T,
342                u: U,
343            }
344        };
345        let out = expand(di);
346        let s = out.to_string();
347        // Accessors should be generated for both fields
348        assert!(s.contains("acc_t"));
349        assert!(s.contains("acc_u"));
350        // The output token stream should mention where-clause Debug (stringly check)
351        assert!(s.contains("Debug") || s.contains("where"));
352    }
353
354    // Exercise the zero-variant enum path (should generate an empty impl block)
355    #[test]
356    fn enum_access_empty_enum_generates_impl() {
357        let di: DeriveInput = parse_quote! { enum Z {} };
358        let out = expand_enum(di);
359        let s = out.to_string();
360        // Should produce an impl block for Z even if it contains no methods
361        assert!(s.contains("impl") && s.contains("Z"));
362    }
363}