Skip to main content

helium_wsl_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::parse::{Parse, ParseStream};
4use syn::{braced, Expr, Ident, Token, Type};
5
6#[derive(Clone)]
7struct ConfigInput {
8    root: StructDef,
9}
10
11#[derive(Clone)]
12struct StructDef {
13    name: Ident,
14    fields: Vec<Field>,
15}
16
17#[derive(Clone)]
18enum Field {
19    Leaf {
20        name: Ident,
21        ty: Box<Type>,
22        default: Option<Expr>,
23    },
24    Nested {
25        name: Ident,
26        fields: Vec<Field>,
27    },
28}
29
30impl Parse for ConfigInput {
31    fn parse(input: ParseStream) -> syn::Result<Self> {
32        let root: StructDef = input.parse()?;
33        Ok(ConfigInput { root })
34    }
35}
36
37impl Parse for StructDef {
38    fn parse(input: ParseStream) -> syn::Result<Self> {
39        let name: Ident = input.parse()?;
40        let content;
41        braced!(content in input);
42        let mut fields = Vec::new();
43        while !content.is_empty() {
44            fields.push(content.parse::<Field>()?);
45            if content.is_empty() {
46                break;
47            }
48            let _ = content.parse::<Token![,]>();
49        }
50        Ok(StructDef { name, fields })
51    }
52}
53
54impl Parse for Field {
55    fn parse(input: ParseStream) -> syn::Result<Self> {
56        let name: Ident = input.parse()?;
57        let _: Token![:] = input.parse()?;
58
59        if input.peek(syn::token::Brace) {
60            let content;
61            braced!(content in input);
62            let mut fields = Vec::new();
63            while !content.is_empty() {
64                fields.push(content.parse::<Field>()?);
65                if content.is_empty() {
66                    break;
67                }
68                let _ = content.parse::<Token![,]>();
69            }
70            Ok(Field::Nested { name, fields })
71        } else {
72                let ty: Box<Type> = Box::new(input.parse()?);
73            let default = if input.peek(Token![=]) {
74                let _: Token![=] = input.parse()?;
75                Some(input.parse::<Expr>()?)
76            } else {
77                None
78            };
79            Ok(Field::Leaf { name, ty, default })
80        }
81    }
82}
83
84fn pascal_case(name: &Ident) -> String {
85    let s = name.to_string();
86    let mut result = String::with_capacity(s.len());
87    let mut capitalize = true;
88    for c in s.chars() {
89        if c == '_' {
90            capitalize = true;
91        } else if capitalize {
92            result.push(c.to_ascii_uppercase());
93            capitalize = false;
94        } else {
95            result.push(c);
96        }
97    }
98    result
99}
100
101fn gen_structs(def: &StructDef, _parent_name: &Ident) -> proc_macro2::TokenStream {
102    let struct_name = &def.name;
103    let field_defs: Vec<_> = def
104        .fields
105        .iter()
106        .map(|f| gen_field(f, struct_name))
107        .collect();
108    let struct_fields: Vec<_> = field_defs.iter().map(|f| &f.0).collect();
109    let default_fields: Vec<_> = field_defs.iter().map(|f| &f.1).collect();
110    let nested_structs: Vec<_> = def
111        .fields
112        .iter()
113        .filter_map(|f| {
114            if let Field::Nested { name, fields } = f {
115                let nested_name = format_ident!(
116                    "{}{}",
117                    struct_name,
118                    pascal_case(name)
119                );
120                let nested_def = StructDef {
121                    name: nested_name,
122                    fields: fields.clone(),
123                };
124                Some(gen_structs(&nested_def, struct_name))
125            } else {
126                None
127            }
128        })
129        .collect();
130
131    quote! {
132        #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
133        pub struct #struct_name {
134            #(#struct_fields),*
135        }
136
137        impl Default for #struct_name {
138            fn default() -> Self {
139                Self {
140                    #(#default_fields),*
141                }
142            }
143        }
144
145        #(#nested_structs)*
146    }
147}
148
149fn gen_field(field: &Field, parent: &Ident) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
150    match field {
151        Field::Leaf {
152            name,
153            ty,
154            default,
155        } => {
156            let field_def = quote! { #[serde(default)] pub #name: #ty };
157            let default_val = match default {
158                Some(expr) => quote! { #expr },
159                None => quote! { Default::default() },
160            };
161            let default_def = quote! { #name: #default_val };
162            (field_def, default_def)
163        }
164        Field::Nested { name, .. } => {
165            let nested_type = format_ident!("{}{}", parent, pascal_case(name));
166            let field_def = quote! { #[serde(default)] pub #name: #nested_type };
167            let default_def = quote! { #name: #nested_type::default() };
168            (field_def, default_def)
169        }
170    }
171}
172
173fn gen_load_method(root: &StructDef) -> proc_macro2::TokenStream {
174    let name = &root.name;
175    quote! {
176        impl #name {
177            /// Load config from a JSON file.
178            ///
179            /// If the file does not exist, the default config is written to
180            /// the path (creating parent directories as needed) and returned.
181            /// If the file exists but contains invalid JSON, a parse error is
182            /// returned.
183            pub fn load(path: impl AsRef<std::path::Path>) -> Result<Self, helium::ConfigError> {
184                match std::fs::read_to_string(path.as_ref()) {
185                    Ok(content) => {
186                        serde_json::from_str(&content).map_err(helium::ConfigError::parsing)
187                    }
188                    Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
189                        if let Some(parent) = path.as_ref().parent() {
190                            let _ = std::fs::create_dir_all(parent);
191                        }
192                        let default = Self::default();
193                        if let Ok(json) = serde_json::to_string_pretty(&default) {
194                            let _ = std::fs::write(path.as_ref(), json);
195                        }
196                        Ok(default)
197                    }
198                    Err(e) => Err(helium::ConfigError::reading(e)),
199                }
200            }
201
202            /// Re-read the config from a JSON file, replacing the current values.
203            pub fn reload(&mut self, path: impl AsRef<std::path::Path>) -> Result<(), helium::ConfigError> {
204                let content = std::fs::read_to_string(path.as_ref()).map_err(helium::ConfigError::reading)?;
205                *self = serde_json::from_str(&content).map_err(helium::ConfigError::parsing)?;
206                Ok(())
207            }
208
209            /// Save config to a JSON file, creating parent directories if needed.
210            pub fn save(&self, path: impl AsRef<std::path::Path>) -> Result<(), helium::ConfigError> {
211                if let Some(parent) = path.as_ref().parent() {
212                    std::fs::create_dir_all(parent).map_err(helium::ConfigError::reading)?;
213                }
214                let content = serde_json::to_string_pretty(self).map_err(helium::ConfigError::parsing)?;
215                std::fs::write(path.as_ref(), content).map_err(helium::ConfigError::reading)?;
216                Ok(())
217            }
218        }
219    }
220}
221
222#[proc_macro]
223pub fn helium_config(input: TokenStream) -> TokenStream {
224    let config: ConfigInput = syn::parse_macro_input!(input);
225    let root = &config.root;
226
227    let structs = gen_structs(root, &root.name);
228    let load = gen_load_method(root);
229
230    let expanded = quote! {
231        #structs
232        #load
233    };
234
235    TokenStream::from(expanded)
236}
237
238/// Parse input for `helium_struct!`.
239struct StructInput {
240    name: Ident,
241    fields: Vec<(Ident, Box<Type>)>,
242}
243
244impl Parse for StructInput {
245    fn parse(input: ParseStream) -> syn::Result<Self> {
246        let name: Ident = input.parse()?;
247        let content;
248        braced!(content in input);
249        let mut fields = Vec::new();
250        while !content.is_empty() {
251            let field_name: Ident = content.parse()?;
252            let _: Token![:] = content.parse()?;
253            let ty: Box<Type> = Box::new(content.parse()?);
254            fields.push((field_name, ty));
255            if content.is_empty() {
256                break;
257            }
258            let _ = content.parse::<Token![,]>();
259        }
260        Ok(StructInput { name, fields })
261    }
262}
263
264/// Generate a plain struct with a `new()` constructor.
265///
266/// The generated struct derives `Debug`, `Clone`, and `PartialEq`.
267/// Each field is `pub`.
268///
269/// ```ignore
270/// helium_struct! {
271///     Foo {
272///         name: String,
273///         count: i32,
274///     }
275/// }
276/// ```
277#[proc_macro]
278pub fn helium_struct(input: TokenStream) -> TokenStream {
279    let input: StructInput = syn::parse_macro_input!(input);
280    let name = &input.name;
281    let field_names: Vec<_> = input.fields.iter().map(|(n, _)| n).collect();
282    let field_tys: Vec<_> = input.fields.iter().map(|(_, t)| t).collect();
283
284    let expanded = quote! {
285        #[derive(Debug, Clone, PartialEq)]
286        pub struct #name {
287            #(pub #field_names: #field_tys),*
288        }
289
290        impl #name {
291            /// Create a new instance with the given field values.
292            pub fn new(#(#field_names: #field_tys),*) -> Self {
293                Self { #(#field_names),* }
294            }
295        }
296    };
297
298    TokenStream::from(expanded)
299}
300
301/// Parse input for `helium_model!`.
302struct ModelInput {
303    name: Ident,
304    ty: Box<Type>,
305}
306
307impl Parse for ModelInput {
308    fn parse(input: ParseStream) -> syn::Result<Self> {
309        let name: Ident = input.parse()?;
310        let content;
311        syn::parenthesized!(content in input);
312        let ty: Box<Type> = Box::new(content.parse()?);
313        Ok(ModelInput { name, ty })
314    }
315}
316
317/// Generate a Slint-compatible model wrapper around `Vec<T>`.
318///
319/// The generated type wraps `slint::VecModel<T>` and exposes `push`,
320/// `clear`, `from_vec`, and `row_count`. It converts to
321/// `slint_interpreter::Value` for use with Slint `for` loops.
322///
323/// ```ignore
324/// helium_model! { MyModel(i32) }
325/// ```
326#[proc_macro]
327pub fn helium_model(input: TokenStream) -> TokenStream {
328    let input: ModelInput = syn::parse_macro_input!(input);
329    let name = &input.name;
330    let ty = &input.ty;
331
332    let expanded = quote! {
333        #[derive(Debug)]
334        pub struct #name {
335            inner: slint::VecModel<#ty>,
336        }
337
338        impl #name {
339            /// Create an empty model.
340            pub fn new() -> Self {
341                Self { inner: slint::VecModel::new() }
342            }
343
344            /// Create a model from a `Vec`.
345            pub fn from_vec(data: Vec<#ty>) -> Self {
346                Self { inner: slint::VecModel::from(data) }
347            }
348
349            /// Append an item.
350            pub fn push(&self, value: #ty) {
351                self.inner.push(value);
352            }
353
354            /// Replace the backing data.
355            pub fn set_vec(&self, data: Vec<#ty>) {
356                self.inner.set_vec(data);
357            }
358
359            /// Remove all items.
360            pub fn clear(&self) {
361                self.inner.clear();
362            }
363
364            /// Number of items in the model.
365            pub fn row_count(&self) -> usize {
366                self.inner.row_count()
367            }
368        }
369
370        impl Default for #name {
371            fn default() -> Self {
372                Self::new()
373            }
374        }
375
376        impl From<Vec<#ty>> for #name {
377            fn from(data: Vec<#ty>) -> Self {
378                Self::from_vec(data)
379            }
380        }
381
382        impl From<#name> for slint_interpreter::Value {
383            fn from(model: #name) -> Self {
384                let model_rc: slint::ModelRc<#ty> = model.inner.into();
385                model_rc.into()
386            }
387        }
388    };
389
390    TokenStream::from(expanded)
391}