1pub mod application;
34pub mod bindings;
35pub mod config;
36pub mod handlers;
37pub mod inventory;
38pub mod status_mapping;
39pub mod subscription;
40pub mod theme;
41pub mod update;
42pub mod view;
43
44use crate::DampenDocument;
45use crate::HandlerSignature;
46use proc_macro2::TokenStream;
47use quote::quote;
48use std::path::PathBuf;
49use std::time::SystemTime;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum HandlerSignatureType {
54 Simple,
57
58 WithValue,
61
62 WithCommand,
65}
66
67#[derive(Debug, Clone)]
69pub struct HandlerInfo {
70 pub name: &'static str,
72
73 pub signature_type: HandlerSignatureType,
75
76 pub param_types: &'static [&'static str],
78
79 pub return_type: &'static str,
81
82 pub source_file: &'static str,
84
85 pub source_line: u32,
87}
88
89impl HandlerInfo {
90 pub fn to_signature(&self) -> HandlerSignature {
99 let param_type = match self.signature_type {
101 HandlerSignatureType::Simple => None,
102 HandlerSignatureType::WithValue => {
103 if self.param_types.len() > 1 {
105 Some(self.param_types[1].to_string())
106 } else {
107 None
108 }
109 }
110 HandlerSignatureType::WithCommand => None,
111 };
112
113 let returns_command = matches!(self.signature_type, HandlerSignatureType::WithCommand);
114
115 HandlerSignature {
116 name: self.name.to_string(),
117 param_type,
118 returns_command,
119 }
120 }
121}
122
123#[derive(Debug)]
127pub struct CodegenOutput {
128 pub code: String,
130 pub warnings: Vec<String>,
132}
133
134#[derive(Debug, Clone)]
136pub struct GeneratedApplication {
137 pub code: String,
139
140 pub handlers: Vec<String>,
142
143 pub widgets: Vec<String>,
145
146 pub warnings: Vec<String>,
148}
149
150#[derive(Debug, Clone)]
156pub struct GeneratedCode {
157 pub code: String,
159
160 pub module_name: String,
162
163 pub source_file: PathBuf,
165
166 pub timestamp: SystemTime,
168
169 pub validated: bool,
171}
172
173impl GeneratedCode {
174 pub fn new(code: String, module_name: String, source_file: PathBuf) -> Self {
184 Self {
185 code,
186 module_name,
187 source_file,
188 timestamp: SystemTime::now(),
189 validated: false,
190 }
191 }
192
193 pub fn validate(&mut self) -> Result<(), String> {
198 match syn::parse_file(&self.code) {
199 Ok(_) => {
200 self.validated = true;
201 Ok(())
202 }
203 Err(e) => Err(format!("Syntax validation failed: {}", e)),
204 }
205 }
206
207 pub fn format(&mut self) -> Result<(), String> {
212 match syn::parse_file(&self.code) {
214 Ok(syntax_tree) => {
215 self.code = prettyplease::unparse(&syntax_tree);
217 Ok(())
218 }
219 Err(e) => Err(format!("Failed to parse code for formatting: {}", e)),
220 }
221 }
222
223 pub fn write_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
231 std::fs::write(path, &self.code)
232 }
233}
234
235pub fn generate_application(
257 document: &DampenDocument,
258 model_name: &str,
259 message_name: &str,
260 handlers: &[HandlerSignature],
261) -> Result<CodegenOutput, CodegenError> {
262 let warnings = Vec::new();
263
264 let message_enum = generate_message_enum(handlers);
265
266 let view_fn = view::generate_view(document, model_name, message_name)?;
267
268 let update_arms = update::generate_arms(handlers, message_name)?;
269
270 let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
271 let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
272
273 let combined = quote! {
274 use iced::{Element, Task};
275 use crate::ui::window::*;
276
277 #message_enum
278
279 pub fn new_model() -> (#model_ident, Task<#message_ident>) {
280 (#model_ident::default(), Task::none())
281 }
282
283 pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
284 match message {
285 #update_arms
286 }
287 }
288
289 pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
290 #view_fn
291 }
292 };
293
294 Ok(CodegenOutput {
295 code: combined.to_string(),
296 warnings,
297 })
298}
299
300use crate::ir::theme::ThemeDocument;
301
302pub fn generate_application_with_theme_and_subscriptions(
322 document: &DampenDocument,
323 model_name: &str,
324 message_name: &str,
325 handlers: &[HandlerSignature],
326 theme_document: Option<&ThemeDocument>,
327) -> Result<CodegenOutput, CodegenError> {
328 let warnings = Vec::new();
329
330 let sub_config =
332 subscription::SubscriptionConfig::from_theme_document(theme_document, message_name);
333
334 let message_enum = generate_message_enum_with_subscription(handlers, Some(&sub_config));
336
337 let view_fn = view::generate_view(document, model_name, message_name)?;
338
339 let update_arms = update::generate_arms(handlers, message_name)?;
340
341 let system_theme_arm = subscription::generate_system_theme_update_arm(&sub_config);
343
344 let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
345 let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
346
347 let theme_code = if let Some(theme_doc) = theme_document {
349 match theme::generate_theme_code(theme_doc, &document.style_classes, "app") {
350 Ok(generated) => {
351 let code_str = generated.code;
352 syn::parse_str::<TokenStream>(&code_str).unwrap_or_default()
353 }
354 Err(e) => {
355 return Err(CodegenError::ThemeError(e));
356 }
357 }
358 } else {
359 TokenStream::new()
360 };
361
362 let subscription_fn = subscription::generate_subscription_function(&sub_config);
364
365 let has_theme = theme_document.is_some();
366 let theme_method = if has_theme {
367 quote! {
368 pub fn theme(_model: &#model_ident) -> iced::Theme {
369 app_theme()
370 }
371 }
372 } else {
373 quote! {}
374 };
375
376 let combined = quote! {
377 use iced::{Element, Task, Theme};
378 use crate::ui::window::*;
379 use std::collections::HashMap;
380
381 #theme_code
382
383 #message_enum
384
385 pub fn new_model() -> (#model_ident, Task<#message_ident>) {
386 (#model_ident::default(), Task::none())
387 }
388
389 pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
390 match message {
391 #update_arms
392 #system_theme_arm
393 }
394 }
395
396 pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
397 #view_fn
398 }
399
400 #theme_method
401
402 #subscription_fn
403 };
404
405 Ok(CodegenOutput {
406 code: combined.to_string(),
407 warnings,
408 })
409}
410
411fn generate_message_enum(handlers: &[HandlerSignature]) -> TokenStream {
413 generate_message_enum_with_subscription(handlers, None)
414}
415
416fn generate_message_enum_with_subscription(
418 handlers: &[HandlerSignature],
419 sub_config: Option<&subscription::SubscriptionConfig>,
420) -> TokenStream {
421 let handler_variants: Vec<_> = handlers
422 .iter()
423 .map(|h| {
424 let variant_name = to_upper_camel_case(&h.name);
426 let ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
427
428 if let Some(param_type) = &h.param_type {
429 let type_ident = syn::Ident::new(param_type, proc_macro2::Span::call_site());
430 quote! { #ident(#type_ident) }
431 } else {
432 quote! { #ident }
433 }
434 })
435 .collect();
436
437 let system_theme_variant = sub_config.and_then(subscription::generate_system_theme_variant);
439
440 let all_variants: Vec<TokenStream> = handler_variants
441 .into_iter()
442 .chain(system_theme_variant)
443 .collect();
444
445 if all_variants.is_empty() {
446 return quote! {
447 #[derive(Clone, Debug)]
448 pub enum Message {}
449 };
450 }
451
452 quote! {
453 #[derive(Clone, Debug)]
454 pub enum Message {
455 #(#all_variants),*
456 }
457 }
458}
459
460fn to_upper_camel_case(s: &str) -> String {
462 let mut result = String::new();
463 let mut capitalize_next = true;
464 for c in s.chars() {
465 if c == '_' {
466 capitalize_next = true;
467 } else if capitalize_next {
468 result.push(c.to_ascii_uppercase());
469 capitalize_next = false;
470 } else {
471 result.push(c);
472 }
473 }
474 result
475}
476
477pub fn constant_folding(code: &str) -> String {
484 let mut result = String::with_capacity(code.len());
485
486 for line in code.lines() {
487 let trimmed = line.trim_end();
488 if !trimmed.is_empty() {
489 result.push_str(trimmed);
490 result.push('\n');
491 }
492 }
493
494 result
495}
496
497pub fn validate_handlers(
499 document: &DampenDocument,
500 available_handlers: &[HandlerSignature],
501) -> Result<(), CodegenError> {
502 let handler_names: Vec<_> = available_handlers.iter().map(|h| h.name.clone()).collect();
503
504 fn collect_handlers(node: &crate::WidgetNode, handlers: &mut Vec<String>) {
506 for event in &node.events {
507 handlers.push(event.handler.clone());
508 }
509 for child in &node.children {
510 collect_handlers(child, handlers);
511 }
512 }
513
514 let mut referenced_handlers = Vec::new();
515 collect_handlers(&document.root, &mut referenced_handlers);
516
517 for handler in referenced_handlers {
519 if !handler_names.contains(&handler) {
520 return Err(CodegenError::MissingHandler(handler));
521 }
522 }
523
524 Ok(())
525}
526
527#[derive(Debug, thiserror::Error)]
529pub enum CodegenError {
530 #[error("Handler '{0}' is referenced but not defined")]
531 MissingHandler(String),
532
533 #[error("Invalid widget kind for code generation: {0}")]
534 InvalidWidget(String),
535
536 #[error("Binding expression error: {0}")]
537 BindingError(String),
538
539 #[error("Theme code generation error: {0}")]
540 ThemeError(String),
541
542 #[error("IO error: {0}")]
543 IoError(#[from] std::io::Error),
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use crate::parse;
550
551 #[test]
552 fn test_message_enum_generation() {
553 let handlers = vec![
554 HandlerSignature {
555 name: "increment".to_string(),
556 param_type: None,
557 returns_command: false,
558 },
559 HandlerSignature {
560 name: "update_value".to_string(),
561 param_type: Some("String".to_string()),
562 returns_command: false,
563 },
564 ];
565
566 let tokens = generate_message_enum(&handlers);
567 let code = tokens.to_string();
568
569 assert!(code.contains("Increment"));
570 assert!(code.contains("UpdateValue"));
571 }
572
573 #[test]
574 fn test_handler_validation() {
575 let xml = r#"<column><button on_click="increment" /></column>"#;
576 let doc = parse(xml).unwrap();
577
578 let handlers = vec![HandlerSignature {
579 name: "increment".to_string(),
580 param_type: None,
581 returns_command: false,
582 }];
583
584 assert!(validate_handlers(&doc, &handlers).is_ok());
585
586 let handlers_empty: Vec<HandlerSignature> = vec![];
588 assert!(validate_handlers(&doc, &handlers_empty).is_err());
589 }
590}