bevy_agent/
utils.rs

1//! Utility functions for Bevy AI
2
3use crate::error::{BevyAIError, Result};
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use walkdir::WalkDir;
8
9/// Code analysis utilities
10pub mod code_analysis {
11    use super::*;
12    use syn::{File, Item, ItemFn, ItemStruct, ItemEnum, ItemImpl};
13    
14    /// Analyze Rust code structure
15    pub struct CodeAnalyzer;
16    
17    /// Code structure information
18    #[derive(Debug, Clone)]
19    pub struct CodeStructure {
20        /// Functions found in the code
21        pub functions: Vec<FunctionInfo>,
22        /// Structs found in the code
23        pub structs: Vec<StructInfo>,
24        /// Enums found in the code
25        pub enums: Vec<EnumInfo>,
26        /// Implementation blocks found in the code
27        pub impls: Vec<ImplInfo>,
28        /// Import statements found in the code
29        pub imports: Vec<String>,
30        /// Bevy features used in the code
31        pub features_used: Vec<String>,
32    }
33    
34    /// Information about a function
35    #[derive(Debug, Clone)]
36    pub struct FunctionInfo {
37        /// Name of the function
38        pub name: String,
39        /// Whether the function is public
40        pub is_public: bool,
41        /// Whether the function is async
42        pub is_async: bool,
43        /// Function parameters
44        pub parameters: Vec<String>,
45        /// Return type (if any)
46        pub return_type: Option<String>,
47        /// Line number where the function is defined
48        pub line_number: usize,
49    }
50    
51    /// Information about a struct
52    #[derive(Debug, Clone)]
53    pub struct StructInfo {
54        /// Name of the struct
55        pub name: String,
56        /// Whether the struct is public
57        pub is_public: bool,
58        /// Fields of the struct
59        pub fields: Vec<String>,
60        /// Derive attributes on the struct
61        pub derives: Vec<String>,
62        /// Line number where the struct is defined
63        pub line_number: usize,
64    }
65    
66    /// Information about an enum
67    #[derive(Debug, Clone)]
68    pub struct EnumInfo {
69        /// Name of the enum
70        pub name: String,
71        /// Whether the enum is public
72        pub is_public: bool,
73        /// Variants of the enum
74        pub variants: Vec<String>,
75        /// Derive attributes on the enum
76        pub derives: Vec<String>,
77        /// Line number where the enum is defined
78        pub line_number: usize,
79    }
80    
81    /// Information about an implementation block
82    #[derive(Debug, Clone)]
83    pub struct ImplInfo {
84        /// Target type being implemented
85        pub target: String,
86        /// Trait being implemented (if any)
87        pub trait_name: Option<String>,
88        /// Methods in the implementation
89        pub methods: Vec<String>,
90        /// Line number where the impl block starts
91        pub line_number: usize,
92    }
93    
94    impl CodeAnalyzer {
95        /// Analyze a Rust source file
96        pub fn analyze_file<P: AsRef<Path>>(path: P) -> Result<CodeStructure> {
97            let content = fs::read_to_string(&path)?;
98            Self::analyze_code(&content)
99        }
100        
101        /// Analyze Rust code from a string
102        pub fn analyze_code(code: &str) -> Result<CodeStructure> {
103            let syntax_tree: File = syn::parse_str(code)
104                .map_err(|e| BevyAIError::CodeParsing(format!("Failed to parse Rust code: {}", e)))?;
105            
106            let mut structure = CodeStructure {
107                functions: Vec::new(),
108                structs: Vec::new(),
109                enums: Vec::new(),
110                impls: Vec::new(),
111                imports: Vec::new(),
112                features_used: Vec::new(),
113            };
114            
115            for item in syntax_tree.items {
116                match item {
117                    Item::Fn(func) => {
118                        structure.functions.push(Self::analyze_function(&func));
119                    }
120                    Item::Struct(struct_item) => {
121                        structure.structs.push(Self::analyze_struct(&struct_item));
122                    }
123                    Item::Enum(enum_item) => {
124                        structure.enums.push(Self::analyze_enum(&enum_item));
125                    }
126                    Item::Impl(impl_item) => {
127                        structure.impls.push(Self::analyze_impl(&impl_item));
128                    }
129                    Item::Use(use_item) => {
130                        structure.imports.push(quote::quote!(#use_item).to_string());
131                    }
132                    _ => {}
133                }
134            }
135            
136            // Analyze Bevy features being used
137            structure.features_used = Self::detect_bevy_features(code);
138            
139            Ok(structure)
140        }
141        
142        fn analyze_function(func: &ItemFn) -> FunctionInfo {
143            FunctionInfo {
144                name: func.sig.ident.to_string(),
145                is_public: matches!(func.vis, syn::Visibility::Public(_)),
146                is_async: func.sig.asyncness.is_some(),
147                parameters: func.sig.inputs.iter()
148                    .map(|param| quote::quote!(#param).to_string())
149                    .collect(),
150                return_type: Self::extract_return_type(func.sig.output.clone()),
151                line_number: 0, // TODO: Extract line numbers
152            }
153        }
154        
155        fn analyze_struct(struct_item: &ItemStruct) -> StructInfo {
156            StructInfo {
157                name: struct_item.ident.to_string(),
158                is_public: matches!(struct_item.vis, syn::Visibility::Public(_)),
159                fields: struct_item.fields.iter()
160                    .map(|field| {
161                        field.ident.as_ref()
162                            .map(|i| i.to_string())
163                            .unwrap_or_else(|| "unnamed".to_string())
164                    })
165                    .collect(),
166                derives: struct_item.attrs.iter()
167                    .filter_map(|attr| {
168                        if attr.path().is_ident("derive") {
169                            Some(quote::quote!(#attr).to_string())
170                        } else {
171                            None
172                        }
173                    })
174                    .collect(),
175                line_number: 0,
176            }
177        }
178        
179        fn analyze_enum(enum_item: &ItemEnum) -> EnumInfo {
180            EnumInfo {
181                name: enum_item.ident.to_string(),
182                is_public: matches!(enum_item.vis, syn::Visibility::Public(_)),
183                variants: enum_item.variants.iter()
184                    .map(|variant| variant.ident.to_string())
185                    .collect(),
186                derives: enum_item.attrs.iter()
187                    .filter_map(|attr| {
188                        if attr.path().is_ident("derive") {
189                            Some(quote::quote!(#attr).to_string())
190                        } else {
191                            None
192                        }
193                    })
194                    .collect(),
195                line_number: 0,
196            }
197        }
198        
199        fn analyze_impl(impl_item: &ItemImpl) -> ImplInfo {
200            ImplInfo {
201                target: quote::quote!(#impl_item.self_ty).to_string(),
202                trait_name: impl_item.trait_.as_ref()
203                    .map(|(_, path, _)| quote::quote!(#path).to_string()),
204                methods: impl_item.items.iter()
205                    .filter_map(|item| {
206                        if let syn::ImplItem::Fn(method) = item {
207                            Some(method.sig.ident.to_string())
208                        } else {
209                            None
210                        }
211                    })
212                    .collect(),
213                line_number: 0,
214            }
215        }
216        
217        fn detect_bevy_features(code: &str) -> Vec<String> {
218            let mut features = Vec::new();
219            
220            let feature_patterns = [
221                ("2D Rendering", vec!["Camera2dBundle", "Sprite", "SpriteBundle"]),
222                ("3D Rendering", vec!["Camera3dBundle", "PbrBundle", "Mesh"]),
223                ("UI", vec!["ButtonBundle", "TextBundle", "NodeBundle"]),
224                ("Audio", vec!["AudioBundle", "AudioSource"]),
225                ("Physics", vec!["RigidBody", "Collider", "Velocity"]),
226                ("Animation", vec!["AnimationPlayer", "AnimationClip"]),
227                ("Networking", vec!["NetworkEvent", "Connection"]),
228                ("Input", vec!["ButtonInput", "KeyCode", "MouseButton"]),
229                ("ECS", vec!["Component", "Resource", "System"]),
230                ("Transform", vec!["Transform", "GlobalTransform"]),
231                ("Time", vec!["Time", "Timer"]),
232                ("Events", vec!["EventReader", "EventWriter"]),
233                ("Assets", vec!["Assets", "Handle", "AssetServer"]),
234                ("Scene", vec!["Scene", "SceneBundle"]),
235                ("Lighting", vec!["DirectionalLight", "PointLight", "SpotLight"]),
236            ];
237            
238            for (feature_name, patterns) in &feature_patterns {
239                for pattern in patterns {
240                    if code.contains(pattern) {
241                        features.push(feature_name.to_string());
242                        break;
243                    }
244                }
245            }
246            
247            features
248        }
249    
250        // Helper function to extract return type
251        fn extract_return_type(return_type: syn::ReturnType) -> Option<String> {
252            match return_type {
253                syn::ReturnType::Default => None,
254                syn::ReturnType::Type(_, ty) => Some(quote::quote!(#ty).to_string()),
255            }
256        }
257    }
258}
259
260/// File system utilities
261pub mod fs_utils {
262    use super::*;
263    
264    /// Find all Rust files in a directory
265    pub fn find_rust_files<P: AsRef<Path>>(dir: P) -> Result<Vec<PathBuf>> {
266        let mut rust_files = Vec::new();
267        
268        for entry in WalkDir::new(dir) {
269            let entry = entry?;
270            if entry.path().extension().map_or(false, |ext| ext == "rs") {
271                rust_files.push(entry.path().to_path_buf());
272            }
273        }
274        
275        Ok(rust_files)
276    }
277    
278    /// Get file extension
279    pub fn get_extension<P: AsRef<Path>>(path: P) -> Option<String> {
280        path.as_ref()
281            .extension()
282            .and_then(|ext| ext.to_str())
283            .map(|s| s.to_string())
284    }
285    
286    /// Create a backup of a file
287    pub fn backup_file<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
288        let path = path.as_ref();
289        let backup_path = path.with_extension(
290            format!("{}.backup.{}", 
291                path.extension().unwrap_or_default().to_string_lossy(),
292                chrono::Utc::now().timestamp()
293            )
294        );
295        
296        fs::copy(path, &backup_path)?;
297        Ok(backup_path)
298    }
299    
300    /// Calculate MD5 hash of a file
301    pub fn calculate_file_hash<P: AsRef<Path>>(path: P) -> Result<String> {
302        let content = fs::read(path)?;
303        Ok(format!("{:x}", md5::compute(&content)))
304    }
305    
306    /// Count lines in a file
307    pub fn count_lines<P: AsRef<Path>>(path: P) -> Result<usize> {
308        let content = fs::read_to_string(path)?;
309        Ok(content.lines().count())
310    }
311}
312
313/// Build system utilities
314pub mod build_utils {
315    use super::*;
316    
317    /// Check if Rust toolchain is available
318    pub fn check_rust_toolchain() -> Result<String> {
319        let output = Command::new("rustc")
320            .arg("--version")
321            .output()?;
322        
323        if output.status.success() {
324            Ok(String::from_utf8_lossy(&output.stdout).to_string())
325        } else {
326            Err(BevyAIError::build_system("Rust toolchain not found".to_string()))
327        }
328    }
329    
330    /// Check if Cargo is available
331    pub fn check_cargo() -> Result<String> {
332        let output = Command::new("cargo")
333            .arg("--version")
334            .output()?;
335        
336        if output.status.success() {
337            Ok(String::from_utf8_lossy(&output.stdout).to_string())
338        } else {
339            Err(BevyAIError::build_system("Cargo not found".to_string()))
340        }
341    }
342    
343    /// Run cargo check
344    pub fn cargo_check<P: AsRef<Path>>(project_path: P) -> Result<String> {
345        let output = Command::new("cargo")
346            .arg("check")
347            .current_dir(project_path)
348            .output()?;
349        
350        Ok(format!("{}\n{}", 
351           String::from_utf8_lossy(&output.stdout),
352           String::from_utf8_lossy(&output.stderr)))
353    }
354    
355    /// Run cargo clippy
356    pub fn cargo_clippy<P: AsRef<Path>>(project_path: P) -> Result<String> {
357        let output = Command::new("cargo")
358            .args(&["clippy", "--", "-D", "warnings"])
359            .current_dir(project_path)
360            .output()?;
361        
362        Ok(format!("{}\n{}", 
363           String::from_utf8_lossy(&output.stdout),
364           String::from_utf8_lossy(&output.stderr)))
365    }
366    
367    /// Run cargo fmt
368    pub fn cargo_fmt<P: AsRef<Path>>(project_path: P) -> Result<String> {
369        let output = Command::new("cargo")
370            .arg("fmt")
371            .current_dir(project_path)
372            .output()?;
373        
374        if output.status.success() {
375            Ok("Code formatted successfully".to_string())
376        } else {
377            Err(BevyAIError::build_system(
378                String::from_utf8_lossy(&output.stderr).to_string()
379            ))
380        }
381    }
382}
383
384/// Text processing utilities
385pub mod text_utils {
386    use super::*;
387    
388    /// Extract code blocks from markdown text
389    pub fn extract_code_blocks(text: &str) -> Vec<(Option<String>, String)> {
390        let mut code_blocks = Vec::new();
391        let lines: Vec<&str> = text.lines().collect();
392        let mut i = 0;
393        
394        while i < lines.len() {
395            if lines[i].starts_with("```") {
396                let language = if lines[i].len() > 3 {
397                    Some(lines[i][3..].trim().to_string())
398                } else {
399                    None
400                };
401                
402                let mut code = String::new();
403                i += 1;
404                
405                while i < lines.len() && !lines[i].starts_with("```") {
406                    code.push_str(lines[i]);
407                    code.push('\n');
408                    i += 1;
409                }
410                
411                code_blocks.push((language, code.trim().to_string()));
412            }
413            i += 1;
414        }
415        
416        code_blocks
417    }
418    
419    /// Clean up AI-generated code
420    pub fn clean_ai_code(code: &str) -> String {
421        // Remove common AI artifacts
422        let mut cleaned = code.to_string();
423        
424        // Remove explanatory comments that start with "Note:" or "TODO:"
425        cleaned = cleaned.lines()
426            .filter(|line| {
427                let trimmed = line.trim();
428                !trimmed.starts_with("// Note:") && 
429                !trimmed.starts_with("// TODO: Add") &&
430                !trimmed.starts_with("// TODO: Implement")
431            })
432            .collect::<Vec<_>>()
433            .join("\n");
434        
435        // Normalize whitespace
436        cleaned = cleaned.trim().to_string();
437        
438        cleaned
439    }
440    
441    /// Format Rust code using rustfmt
442    pub fn format_rust_code(code: &str) -> Result<String> {
443        use std::io::Write;
444        use std::process::Stdio;
445        
446        let mut child = Command::new("rustfmt")
447            .stdin(Stdio::piped())
448            .stdout(Stdio::piped())
449            .stderr(Stdio::piped())
450            .spawn()?;
451        
452        if let Some(stdin) = child.stdin.as_mut() {
453            stdin.write_all(code.as_bytes())?;
454        }
455        
456        let output = child.wait_with_output()?;
457        
458        if output.status.success() {
459            Ok(String::from_utf8_lossy(&output.stdout).to_string())
460        } else {
461            // If rustfmt fails, return original code
462            Ok(code.to_string())
463        }
464    }
465    
466    /// Convert snake_case to PascalCase
467    pub fn snake_to_pascal(input: &str) -> String {
468        input.split('_')
469            .map(|word| {
470                let mut chars = word.chars();
471                match chars.next() {
472                    None => String::new(),
473                    Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
474                }
475            })
476            .collect()
477    }
478    
479    /// Convert PascalCase to snake_case
480    pub fn pascal_to_snake(input: &str) -> String {
481        let mut result = String::new();
482        let mut prev_was_upper = false;
483        
484        for (i, c) in input.chars().enumerate() {
485            if c.is_uppercase() && i > 0 && !prev_was_upper {
486                result.push('_');
487            }
488            result.push(c.to_lowercase().next().unwrap_or(c));
489            prev_was_upper = c.is_uppercase();
490        }
491        
492        result
493    }
494}
495
496/// Configuration utilities
497pub mod config_utils {
498    use super::*;
499    use crate::config::AIConfig;
500    
501    /// Validate AI configuration
502    pub fn validate_config(config: &AIConfig) -> Result<Vec<String>> {
503        let mut warnings = Vec::new();
504        
505        if config.openai.is_none() && config.anthropic.is_none() && config.google.is_none() {
506            warnings.push("No AI providers configured".to_string());
507        }
508        
509        if config.generation.temperature < 0.0 || config.generation.temperature > 2.0 {
510            warnings.push("Temperature should be between 0.0 and 2.0".to_string());
511        }
512        
513        if config.generation.max_tokens < 100 {
514            warnings.push("Max tokens is very low, may result in incomplete responses".to_string());
515        }
516        
517        Ok(warnings)
518    }
519    
520    /// Get default Bevy version
521    pub fn get_latest_bevy_version() -> Result<String> {
522        // In a real implementation, this would query crates.io API
523        Ok("0.12".to_string())
524    }
525    
526    /// Get recommended dependencies for a game type
527    pub fn get_recommended_dependencies(game_type: &str) -> Vec<String> {
528        match game_type.to_lowercase().as_str() {
529            "platformer" | "2d" => vec![
530                "bevy".to_string(),
531            ],
532            "fps" | "3d" | "shooter" => vec![
533                "bevy".to_string(),
534                "bevy_rapier3d".to_string(),
535            ],
536            "physics" => vec![
537                "bevy".to_string(),
538                "bevy_rapier2d".to_string(),
539                "bevy_rapier3d".to_string(),
540            ],
541            "ui" | "menu" => vec![
542                "bevy".to_string(),
543                "bevy_egui".to_string(),
544            ],
545            _ => vec![
546                "bevy".to_string(),
547            ],
548        }
549    }
550}