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}