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}