dampen_core/codegen/
inventory.rs

1//! Handler inventory extraction for build scripts
2//!
3//! This module provides utilities for build.rs scripts to extract handler metadata
4//! from Rust source files that use the `inventory_handlers!` macro.
5
6use crate::HandlerSignature;
7use std::path::Path;
8
9/// Extract handler names from an `inventory_handlers!` macro invocation in a Rust file.
10///
11/// This function parses the source file and looks for the `inventory_handlers!` macro,
12/// extracting the list of handler names declared within it.
13///
14/// # Arguments
15///
16/// * `rs_file_path` - Path to the .rs file to parse
17///
18/// # Returns
19///
20/// A vector of handler names found in the inventory, or an empty vector if:
21/// - The file doesn't exist
22/// - The file has no `inventory_handlers!` macro
23/// - The macro is empty
24///
25/// # Example
26///
27/// ```rust,ignore
28/// let handlers = extract_handler_names_from_file("src/ui/window.rs");
29/// // Returns: vec!["increment", "decrement", "reset"]
30/// ```
31pub fn extract_handler_names_from_file(rs_file_path: &Path) -> Vec<String> {
32    let content = match std::fs::read_to_string(rs_file_path) {
33        Ok(content) => content,
34        Err(_) => return vec![],
35    };
36
37    extract_handler_names_from_source(&content)
38}
39
40/// Extract handler names from Rust source code containing `inventory_handlers!` macro.
41///
42/// # Arguments
43///
44/// * `source` - Rust source code as a string
45///
46/// # Returns
47///
48/// A vector of handler names found in the inventory
49fn extract_handler_names_from_source(source: &str) -> Vec<String> {
50    // Parse the file with syn
51    let syntax = match syn::parse_file(source) {
52        Ok(syntax) => syntax,
53        Err(_) => return vec![],
54    };
55
56    // Look for inventory_handlers! macro invocation
57    for item in syntax.items {
58        if let syn::Item::Macro(mac) = item {
59            let path = &mac.mac.path;
60
61            // Check if this is our inventory_handlers macro
62            if path.segments.last().map(|s| s.ident.to_string())
63                == Some("inventory_handlers".to_string())
64            {
65                // Parse the token stream to extract handler names
66                return parse_handler_list_from_tokens(&mac.mac.tokens);
67            }
68        }
69    }
70
71    vec![]
72}
73
74/// Parse handler names from the token stream of inventory_handlers! macro.
75fn parse_handler_list_from_tokens(tokens: &proc_macro2::TokenStream) -> Vec<String> {
76    let mut handlers = Vec::new();
77    let tokens_str = tokens.to_string();
78
79    // Simple tokenization: split by commas and trim whitespace
80    for part in tokens_str.split(',') {
81        let name = part.trim().to_string();
82        if !name.is_empty() {
83            handlers.push(name);
84        }
85    }
86
87    handlers
88}
89
90/// Extract full handler metadata from a Rust file.
91///
92/// This function looks for the `inventory_handlers!` macro to get handler names,
93/// then analyzes the function signatures marked with `#[ui_handler]` to determine
94/// their parameter and return types.
95///
96/// # Arguments
97///
98/// * `rs_file_path` - Path to the .rs file to parse
99///
100/// # Returns
101///
102/// A vector of HandlerSignature objects with complete metadata
103pub fn extract_handler_signatures_from_file(rs_file_path: &Path) -> Vec<HandlerSignature> {
104    let content = match std::fs::read_to_string(rs_file_path) {
105        Ok(content) => content,
106        Err(_) => return vec![],
107    };
108
109    let handler_names = extract_handler_names_from_source(&content);
110
111    // Parse the file to extract function signatures
112    let syntax = match syn::parse_file(&content) {
113        Ok(syntax) => syntax,
114        Err(_) => {
115            // Fallback to simple signatures if parsing fails
116            return handler_names
117                .into_iter()
118                .map(|name| HandlerSignature {
119                    name,
120                    param_type: None,
121                    returns_command: false,
122                })
123                .collect();
124        }
125    };
126
127    // Extract signatures for each handler by finding the corresponding function
128    handler_names
129        .into_iter()
130        .map(|name| {
131            // Look for the function with this name and #[ui_handler] attribute
132            if let Some(signature) = find_handler_function_signature(&syntax, &name) {
133                signature
134            } else {
135                // Fallback to simple signature
136                HandlerSignature {
137                    name,
138                    param_type: None,
139                    returns_command: false,
140                }
141            }
142        })
143        .collect()
144}
145
146/// Find a function with the given name and extract its signature
147fn find_handler_function_signature(
148    syntax: &syn::File,
149    handler_name: &str,
150) -> Option<HandlerSignature> {
151    use syn::{FnArg, Item, ReturnType};
152
153    for item in &syntax.items {
154        if let Item::Fn(func) = item {
155            // Check if this is the function we're looking for
156            if func.sig.ident == handler_name {
157                // Check if it has #[ui_handler] attribute
158                let has_ui_handler_attr = func.attrs.iter().any(|attr| {
159                    attr.path().segments.last().map(|s| s.ident.to_string())
160                        == Some("ui_handler".to_string())
161                });
162
163                if !has_ui_handler_attr {
164                    continue;
165                }
166
167                // Analyze the signature
168                let mut param_type: Option<String> = None;
169                let mut param_count = 0;
170
171                for input in &func.sig.inputs {
172                    if let FnArg::Typed(pat_type) = input {
173                        param_count += 1;
174                        // If there's more than one parameter (first is always &mut Model)
175                        // then the second one is the value parameter
176                        if param_count > 1 {
177                            let ty = &pat_type.ty;
178                            let type_str = quote::quote!(#ty).to_string();
179                            // Clean up the type string (remove extra spaces)
180                            param_type = Some(type_str.replace(" ", ""));
181                        }
182                    }
183                }
184
185                // Check if it returns Command
186                let returns_command = if let ReturnType::Type(_, ty) = &func.sig.output {
187                    let return_str = quote::quote!(#ty).to_string();
188                    return_str.contains("Command")
189                } else {
190                    false
191                };
192
193                return Some(HandlerSignature {
194                    name: handler_name.to_string(),
195                    param_type,
196                    returns_command,
197                });
198            }
199        }
200    }
201
202    None
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_extract_handler_names() {
211        let source = r#"
212            use dampen_macros::{ui_handler, inventory_handlers};
213
214            #[ui_handler]
215            fn increment(model: &mut Model) {
216                model.count += 1;
217            }
218
219            #[ui_handler]
220            fn decrement(model: &mut Model) {
221                model.count -= 1;
222            }
223
224            inventory_handlers! {
225                increment,
226                decrement
227            }
228        "#;
229
230        let handlers = extract_handler_names_from_source(source);
231        assert_eq!(handlers, vec!["increment", "decrement"]);
232    }
233
234    #[test]
235    fn test_extract_empty_inventory() {
236        let source = r#"
237            use dampen_macros::ui_handler;
238
239            #[ui_handler]
240            fn my_handler(model: &mut Model) {}
241        "#;
242
243        let handlers = extract_handler_names_from_source(source);
244        assert!(handlers.is_empty());
245    }
246
247    #[test]
248    fn test_extract_single_handler() {
249        let source = r#"
250            inventory_handlers! { greet }
251        "#;
252
253        let handlers = extract_handler_names_from_source(source);
254        assert_eq!(handlers, vec!["greet"]);
255    }
256}