spark_signals_derive/
lib.rs

1// ============================================================================
2// spark-signals-derive
3// Proc macro for deep reactivity via #[derive(Reactive)]
4// ============================================================================
5
6use proc_macro::TokenStream;
7use quote::{quote, format_ident};
8use syn::{parse_macro_input, DeriveInput, Data, Fields};
9
10/// Derive macro for creating reactive versions of structs.
11///
12/// This generates a new struct with `Reactive` prefix where each field
13/// is wrapped in a `Signal<T>`, enabling fine-grained reactivity.
14///
15/// # Example
16///
17/// ```ignore
18/// use spark_signals::Reactive;
19///
20/// #[derive(Reactive)]
21/// struct User {
22///     name: String,
23///     age: i32,
24/// }
25///
26/// // Generated: ReactiveUser with Signal<String> and Signal<i32> fields
27///
28/// let user = ReactiveUser::new("Alice".to_string(), 30);
29///
30/// // Each field is a Signal - changes trigger effects!
31/// effect(move || {
32///     println!("Name: {}", user.name.get());
33/// });
34///
35/// user.name.set("Bob".to_string()); // Effect re-runs!
36/// ```
37///
38/// # What Gets Generated
39///
40/// For a struct like:
41/// ```ignore
42/// #[derive(Reactive)]
43/// struct User {
44///     name: String,
45///     age: i32,
46/// }
47/// ```
48///
49/// The macro generates:
50/// ```ignore
51/// pub struct ReactiveUser {
52///     pub name: Signal<String>,
53///     pub age: Signal<i32>,
54/// }
55///
56/// impl ReactiveUser {
57///     pub fn new(name: String, age: i32) -> Self { ... }
58///     pub fn from_user(user: User) -> Self { ... }
59///     pub fn to_snapshot(&self) -> User { ... }
60/// }
61///
62/// impl Clone for ReactiveUser { ... }
63///
64/// pub fn reactive_user(name: String, age: i32) -> ReactiveUser { ... }
65/// ```
66#[proc_macro_derive(Reactive)]
67pub fn derive_reactive(input: TokenStream) -> TokenStream {
68    let input = parse_macro_input!(input as DeriveInput);
69
70    let name = &input.ident;
71    let reactive_name = format_ident!("Reactive{}", name);
72    let fn_name = format!("reactive_{}", to_snake_case(&name.to_string()));
73    let fn_ident = format_ident!("{}", fn_name);
74
75    // Only works on structs with named fields
76    let fields = match &input.data {
77        Data::Struct(data) => match &data.fields {
78            Fields::Named(fields) => &fields.named,
79            _ => {
80                return syn::Error::new_spanned(
81                    &input,
82                    "Reactive can only be derived for structs with named fields",
83                )
84                .to_compile_error()
85                .into();
86            }
87        },
88        _ => {
89            return syn::Error::new_spanned(
90                &input,
91                "Reactive can only be derived for structs",
92            )
93            .to_compile_error()
94            .into();
95        }
96    };
97
98    // Collect field info
99    let field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect();
100    let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect();
101
102    // Generate the reactive struct fields
103    let reactive_fields = field_names.iter().zip(field_types.iter()).map(|(name, ty)| {
104        quote! {
105            pub #name: spark_signals::Signal<#ty>
106        }
107    });
108
109    // Generate constructor arguments
110    let constructor_args = field_names.iter().zip(field_types.iter()).map(|(name, ty)| {
111        quote! { #name: #ty }
112    });
113
114    // Generate constructor body
115    let constructor_body = field_names.iter().map(|name| {
116        quote! { #name: spark_signals::signal(#name) }
117    });
118
119    // Generate from_original body
120    let from_original_body = field_names.iter().map(|name| {
121        quote! { #name: spark_signals::signal(original.#name) }
122    });
123
124    // Generate to_snapshot body
125    let to_snapshot_body = field_names.iter().map(|name| {
126        quote! { #name: self.#name.peek() }
127    });
128
129    // Generate clone body
130    let clone_body = field_names.iter().map(|name| {
131        quote! { #name: self.#name.clone() }
132    });
133
134    // Generate convenience function args (same as constructor)
135    let fn_args = field_names.iter().zip(field_types.iter()).map(|(name, ty)| {
136        quote! { #name: #ty }
137    });
138
139    // Generate convenience function call args
140    let fn_call_args = field_names.iter().map(|name| {
141        quote! { #name }
142    });
143
144    // Get generics from original struct
145    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
146
147    let expanded = quote! {
148        /// Reactive version of #name with Signal-wrapped fields.
149        ///
150        /// Each field is a `Signal<T>` that can be read with `.get()` and
151        /// written with `.set()`. Reading inside an effect automatically
152        /// tracks that field as a dependency.
153        pub struct #reactive_name #ty_generics #where_clause {
154            #(#reactive_fields),*
155        }
156
157        impl #impl_generics #reactive_name #ty_generics #where_clause {
158            /// Create a new reactive instance with the given values.
159            pub fn new(#(#constructor_args),*) -> Self {
160                Self {
161                    #(#constructor_body),*
162                }
163            }
164
165            /// Create a reactive instance from an existing non-reactive instance.
166            pub fn from_original(original: #name #ty_generics) -> Self {
167                Self {
168                    #(#from_original_body),*
169                }
170            }
171
172            /// Get a snapshot of the current values (using peek, no tracking).
173            pub fn to_snapshot(&self) -> #name #ty_generics {
174                #name {
175                    #(#to_snapshot_body),*
176                }
177            }
178        }
179
180        impl #impl_generics Clone for #reactive_name #ty_generics #where_clause {
181            fn clone(&self) -> Self {
182                Self {
183                    #(#clone_body),*
184                }
185            }
186        }
187
188        /// Create a new reactive #name.
189        pub fn #fn_ident #impl_generics (#(#fn_args),*) -> #reactive_name #ty_generics #where_clause {
190            #reactive_name::new(#(#fn_call_args),*)
191        }
192    };
193
194    TokenStream::from(expanded)
195}
196
197/// Convert PascalCase to snake_case
198fn to_snake_case(s: &str) -> String {
199    let mut result = String::new();
200    for (i, c) in s.chars().enumerate() {
201        if c.is_uppercase() {
202            if i > 0 {
203                result.push('_');
204            }
205            result.push(c.to_lowercase().next().unwrap());
206        } else {
207            result.push(c);
208        }
209    }
210    result
211}