Compare commits
	
		
			2 Commits
		
	
	
		
			7828f82f58
			...
			8285fdb7b9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8285fdb7b9 | ||
|  | 1ebd591f19 | 
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -22,4 +22,8 @@ Cargo.lock | ||||
| /rhai_test_template | ||||
| /rhai_test_download | ||||
| /rhai_test_fs | ||||
| run_rhai_tests.log | ||||
| run_rhai_tests.log | ||||
| new_location | ||||
| log.txt | ||||
| file.txt | ||||
| fix_doc* | ||||
| @@ -1,9 +1,9 @@ | ||||
| use std::process::Command; | ||||
| use std::path::Path; | ||||
| use std::fs; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
| use std::fs; | ||||
| use std::io; | ||||
| use std::path::Path; | ||||
| use std::process::Command; | ||||
|  | ||||
| // Define a custom error type for download operations | ||||
| #[derive(Debug)] | ||||
| @@ -26,11 +26,17 @@ pub enum DownloadError { | ||||
| impl fmt::Display for DownloadError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e), | ||||
|             DownloadError::CreateDirectoryFailed(e) => { | ||||
|                 write!(f, "Error creating directories: {}", e) | ||||
|             } | ||||
|             DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e), | ||||
|             DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url), | ||||
|             DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e), | ||||
|             DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min), | ||||
|             DownloadError::FileTooSmall(size, min) => write!( | ||||
|                 f, | ||||
|                 "Error: Downloaded file is too small ({}KB < {}KB)", | ||||
|                 size, min | ||||
|             ), | ||||
|             DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e), | ||||
|             DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e), | ||||
|             DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e), | ||||
| @@ -74,12 +80,18 @@ impl Error for DownloadError { | ||||
|  * | ||||
|  * # Examples | ||||
|  * | ||||
|  * ``` | ||||
|  * // Download a file with no minimum size requirement | ||||
|  * let path = download("https://example.com/file.txt", "/tmp/", 0)?; | ||||
|  * ```no_run | ||||
|  * use sal::os::download; | ||||
|  * | ||||
|  * // Download a file with minimum size requirement of 100KB | ||||
|  * let path = download("https://example.com/file.zip", "/tmp/", 100)?; | ||||
|  * fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|  *     // Download a file with no minimum size requirement | ||||
|  *     let path = download("https://example.com/file.txt", "/tmp/", 0)?; | ||||
|  * | ||||
|  *     // Download a file with minimum size requirement of 100KB | ||||
|  *     let path = download("https://example.com/file.zip", "/tmp/", 100)?; | ||||
|  * | ||||
|  *     Ok(()) | ||||
|  * } | ||||
|  * ``` | ||||
|  * | ||||
|  * # Notes | ||||
| @@ -91,30 +103,41 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl | ||||
|     // Create parent directories if they don't exist | ||||
|     let dest_path = Path::new(dest); | ||||
|     fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?; | ||||
|      | ||||
|  | ||||
|     // Extract filename from URL | ||||
|     let filename = match url.split('/').last() { | ||||
|         Some(name) => name, | ||||
|         None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string())) | ||||
|         None => { | ||||
|             return Err(DownloadError::InvalidUrl( | ||||
|                 "cannot extract filename".to_string(), | ||||
|             )) | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     // Create a full path for the downloaded file | ||||
|     let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename); | ||||
|      | ||||
|  | ||||
|     // Create a temporary path for downloading | ||||
|     let temp_path = format!("{}.download", file_path); | ||||
|      | ||||
|  | ||||
|     // Use curl to download the file with progress bar | ||||
|     println!("Downloading {} to {}", url, file_path); | ||||
|     let output = Command::new("curl") | ||||
|         .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) | ||||
|         .args(&[ | ||||
|             "--progress-bar", | ||||
|             "--location", | ||||
|             "--fail", | ||||
|             "--output", | ||||
|             &temp_path, | ||||
|             url, | ||||
|         ]) | ||||
|         .status() | ||||
|         .map_err(DownloadError::CurlExecutionFailed)?; | ||||
|          | ||||
|  | ||||
|     if !output.success() { | ||||
|         return Err(DownloadError::DownloadFailed(url.to_string())); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Show file size after download | ||||
|     match fs::metadata(&temp_path) { | ||||
|         Ok(metadata) => { | ||||
| @@ -122,14 +145,20 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl | ||||
|             let size_kb = size_bytes / 1024; | ||||
|             let size_mb = size_kb / 1024; | ||||
|             if size_mb > 1 { | ||||
|                 println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0)); | ||||
|                 println!( | ||||
|                     "Download complete! File size: {:.2} MB", | ||||
|                     size_bytes as f64 / (1024.0 * 1024.0) | ||||
|                 ); | ||||
|             } else { | ||||
|                 println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0); | ||||
|                 println!( | ||||
|                     "Download complete! File size: {:.2} KB", | ||||
|                     size_bytes as f64 / 1024.0 | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
|         } | ||||
|         Err(_) => println!("Download complete!"), | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Check file size if minimum size is specified | ||||
|     if min_size_kb > 0 { | ||||
|         let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; | ||||
| @@ -139,57 +168,59 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl | ||||
|             return Err(DownloadError::FileTooSmall(size_kb, min_size_kb)); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Check if it's a compressed file that needs extraction | ||||
|     let lower_url = url.to_lowercase(); | ||||
|     let is_archive = lower_url.ends_with(".tar.gz") || | ||||
|                      lower_url.ends_with(".tgz") || | ||||
|                      lower_url.ends_with(".tar") || | ||||
|                      lower_url.ends_with(".zip"); | ||||
|      | ||||
|     let is_archive = lower_url.ends_with(".tar.gz") | ||||
|         || lower_url.ends_with(".tgz") | ||||
|         || lower_url.ends_with(".tar") | ||||
|         || lower_url.ends_with(".zip"); | ||||
|  | ||||
|     if is_archive { | ||||
|         // Extract the file using the appropriate command with progress indication | ||||
|         println!("Extracting {} to {}", temp_path, dest); | ||||
|         let output = if lower_url.ends_with(".zip") { | ||||
|             Command::new("unzip") | ||||
|                 .args(&["-o", &temp_path, "-d", dest])  // Removed -q for verbosity | ||||
|                 .args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity | ||||
|                 .status() | ||||
|         } else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") { | ||||
|             Command::new("tar") | ||||
|                 .args(&["-xzvf", &temp_path, "-C", dest])  // Added v for verbosity | ||||
|                 .args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity | ||||
|                 .status() | ||||
|         } else { | ||||
|             Command::new("tar") | ||||
|                 .args(&["-xvf", &temp_path, "-C", dest])  // Added v for verbosity | ||||
|                 .args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity | ||||
|                 .status() | ||||
|         }; | ||||
|          | ||||
|  | ||||
|         match output { | ||||
|             Ok(status) => { | ||||
|                 if !status.success() { | ||||
|                     return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string())); | ||||
|                     return Err(DownloadError::ExtractionFailed( | ||||
|                         "Error extracting archive".to_string(), | ||||
|                     )); | ||||
|                 } | ||||
|             }, | ||||
|             } | ||||
|             Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), | ||||
|         } | ||||
|          | ||||
|  | ||||
|         // Show number of extracted files | ||||
|         match fs::read_dir(dest) { | ||||
|             Ok(entries) => { | ||||
|                 let count = entries.count(); | ||||
|                 println!("Extraction complete! Extracted {} files/directories", count); | ||||
|             }, | ||||
|             } | ||||
|             Err(_) => println!("Extraction complete!"), | ||||
|         } | ||||
|          | ||||
|  | ||||
|         // Remove the temporary file | ||||
|         fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; | ||||
|          | ||||
|  | ||||
|         Ok(dest.to_string()) | ||||
|     } else { | ||||
|         // Just rename the temporary file to the final destination | ||||
|         fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; | ||||
|          | ||||
|  | ||||
|         Ok(file_path) | ||||
|     } | ||||
| } | ||||
| @@ -210,12 +241,18 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl | ||||
|  * | ||||
|  * # Examples | ||||
|  * | ||||
|  * ``` | ||||
|  * // Download a file with no minimum size requirement | ||||
|  * let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?; | ||||
|  * ```no_run | ||||
|  * use sal::os::download_file; | ||||
|  * | ||||
|  * // Download a file with minimum size requirement of 100KB | ||||
|  * let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?; | ||||
|  * fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|  *     // Download a file with no minimum size requirement | ||||
|  *     let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?; | ||||
|  * | ||||
|  *     // Download a file with minimum size requirement of 100KB | ||||
|  *     let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?; | ||||
|  * | ||||
|  *     Ok(()) | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> { | ||||
| @@ -224,21 +261,28 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, | ||||
|     if let Some(parent) = dest_path.parent() { | ||||
|         fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Create a temporary path for downloading | ||||
|     let temp_path = format!("{}.download", dest); | ||||
|      | ||||
|  | ||||
|     // Use curl to download the file with progress bar | ||||
|     println!("Downloading {} to {}", url, dest); | ||||
|     let output = Command::new("curl") | ||||
|         .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) | ||||
|         .args(&[ | ||||
|             "--progress-bar", | ||||
|             "--location", | ||||
|             "--fail", | ||||
|             "--output", | ||||
|             &temp_path, | ||||
|             url, | ||||
|         ]) | ||||
|         .status() | ||||
|         .map_err(DownloadError::CurlExecutionFailed)?; | ||||
|          | ||||
|  | ||||
|     if !output.success() { | ||||
|         return Err(DownloadError::DownloadFailed(url.to_string())); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Show file size after download | ||||
|     match fs::metadata(&temp_path) { | ||||
|         Ok(metadata) => { | ||||
| @@ -246,14 +290,20 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, | ||||
|             let size_kb = size_bytes / 1024; | ||||
|             let size_mb = size_kb / 1024; | ||||
|             if size_mb > 1 { | ||||
|                 println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0)); | ||||
|                 println!( | ||||
|                     "Download complete! File size: {:.2} MB", | ||||
|                     size_bytes as f64 / (1024.0 * 1024.0) | ||||
|                 ); | ||||
|             } else { | ||||
|                 println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0); | ||||
|                 println!( | ||||
|                     "Download complete! File size: {:.2} KB", | ||||
|                     size_bytes as f64 / 1024.0 | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
|         } | ||||
|         Err(_) => println!("Download complete!"), | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Check file size if minimum size is specified | ||||
|     if min_size_kb > 0 { | ||||
|         let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; | ||||
| @@ -263,10 +313,10 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, | ||||
|             return Err(DownloadError::FileTooSmall(size_kb, min_size_kb)); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Rename the temporary file to the final destination | ||||
|     fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; | ||||
|      | ||||
|  | ||||
|     Ok(dest.to_string()) | ||||
| } | ||||
|  | ||||
| @@ -284,27 +334,38 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, | ||||
|  * | ||||
|  * # Examples | ||||
|  * | ||||
|  * ``` | ||||
|  * // Make a file executable | ||||
|  * chmod_exec("/path/to/file")?; | ||||
|  * ```no_run | ||||
|  * use sal::os::chmod_exec; | ||||
|  * | ||||
|  * fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|  *     // Make a file executable | ||||
|  *     chmod_exec("/path/to/file")?; | ||||
|  *     Ok(()) | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn chmod_exec(path: &str) -> Result<String, DownloadError> { | ||||
|     let path_obj = Path::new(path); | ||||
|      | ||||
|  | ||||
|     // Check if the path exists and is a file | ||||
|     if !path_obj.exists() { | ||||
|         return Err(DownloadError::NotAFile(format!("Path does not exist: {}", path))); | ||||
|         return Err(DownloadError::NotAFile(format!( | ||||
|             "Path does not exist: {}", | ||||
|             path | ||||
|         ))); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     if !path_obj.is_file() { | ||||
|         return Err(DownloadError::NotAFile(format!("Path is not a file: {}", path))); | ||||
|         return Err(DownloadError::NotAFile(format!( | ||||
|             "Path is not a file: {}", | ||||
|             path | ||||
|         ))); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Get current permissions | ||||
|     let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?; | ||||
|     let mut permissions = metadata.permissions(); | ||||
|      | ||||
|  | ||||
|     // Set executable bit for user, group, and others | ||||
|     #[cfg(unix)] | ||||
|     { | ||||
| @@ -314,47 +375,55 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> { | ||||
|         let new_mode = mode | 0o111; | ||||
|         permissions.set_mode(new_mode); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     #[cfg(not(unix))] | ||||
|     { | ||||
|         // On non-Unix platforms, we can't set executable bit directly | ||||
|         // Just return success with a warning | ||||
|         return Ok(format!("Made {} executable (note: non-Unix platform, may not be fully supported)", path)); | ||||
|         return Ok(format!( | ||||
|             "Made {} executable (note: non-Unix platform, may not be fully supported)", | ||||
|             path | ||||
|         )); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Apply the new permissions | ||||
|     fs::set_permissions(path, permissions).map_err(|e| | ||||
|     fs::set_permissions(path, permissions).map_err(|e| { | ||||
|         DownloadError::CommandExecutionFailed(io::Error::new( | ||||
|             io::ErrorKind::Other, | ||||
|             format!("Failed to set executable permissions: {}", e) | ||||
|             format!("Failed to set executable permissions: {}", e), | ||||
|         )) | ||||
|     )?; | ||||
|      | ||||
|     })?; | ||||
|  | ||||
|     Ok(format!("Made {} executable", path)) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Download a file and install it if it's a supported package format. | ||||
|  *  | ||||
|  * | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * | ||||
|  * * `url` - The URL to download from | ||||
|  * * `min_size_kb` - Minimum required file size in KB (0 for no minimum) | ||||
|  *  | ||||
|  * | ||||
|  * # Returns | ||||
|  *  | ||||
|  * | ||||
|  * * `Ok(String)` - The path where the file was saved or extracted | ||||
|  * * `Err(DownloadError)` - An error if the download or installation failed | ||||
|  *  | ||||
|  * | ||||
|  * # Examples | ||||
|  *  | ||||
|  * | ||||
|  * ```no_run | ||||
|  * use sal::os::download_install; | ||||
|  * | ||||
|  * fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|  *     // Download and install a .deb package | ||||
|  *     let result = download_install("https://example.com/package.deb", 100)?; | ||||
|  *     Ok(()) | ||||
|  * } | ||||
|  * ``` | ||||
|  * // Download and install a .deb package | ||||
|  * let result = download_install("https://example.com/package.deb", 100)?; | ||||
|  * ``` | ||||
|  *  | ||||
|  * | ||||
|  * # Notes | ||||
|  *  | ||||
|  * | ||||
|  * Currently only supports .deb packages on Debian-based systems. | ||||
|  * For other file types, it behaves the same as the download function. | ||||
|  */ | ||||
| @@ -362,19 +431,23 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE | ||||
|     // Extract filename from URL | ||||
|     let filename = match url.split('/').last() { | ||||
|         Some(name) => name, | ||||
|         None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string())) | ||||
|         None => { | ||||
|             return Err(DownloadError::InvalidUrl( | ||||
|                 "cannot extract filename".to_string(), | ||||
|             )) | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     // Create a proper destination path | ||||
|     let dest_path = format!("/tmp/{}", filename); | ||||
|  | ||||
|     // Check if it's a compressed file that needs extraction | ||||
|     let lower_url = url.to_lowercase(); | ||||
|     let is_archive = lower_url.ends_with(".tar.gz") || | ||||
|                      lower_url.ends_with(".tgz") || | ||||
|                      lower_url.ends_with(".tar") || | ||||
|                      lower_url.ends_with(".zip"); | ||||
|      | ||||
|     let is_archive = lower_url.ends_with(".tar.gz") | ||||
|         || lower_url.ends_with(".tgz") | ||||
|         || lower_url.ends_with(".tar") | ||||
|         || lower_url.ends_with(".zip"); | ||||
|  | ||||
|     let download_result = if is_archive { | ||||
|         // For archives, use the directory-based download function | ||||
|         download(url, "/tmp", min_size_kb)? | ||||
| @@ -382,13 +455,13 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE | ||||
|         // For regular files, use the file-specific download function | ||||
|         download_file(url, &dest_path, min_size_kb)? | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     // Check if the downloaded result is a file | ||||
|     let path = Path::new(&dest_path); | ||||
|     if !path.is_file() { | ||||
|         return Ok(download_result); // Not a file, might be an extracted directory | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Check if it's a .deb package | ||||
|     if dest_path.to_lowercase().ends_with(".deb") { | ||||
|         // Check if we're on a Debian-based platform | ||||
| @@ -396,26 +469,28 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE | ||||
|             .arg("-c") | ||||
|             .arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version") | ||||
|             .status(); | ||||
|              | ||||
|  | ||||
|         match platform_check { | ||||
|             Ok(status) => { | ||||
|                 if !status.success() { | ||||
|                     return Err(DownloadError::PlatformNotSupported( | ||||
|                         "Cannot install .deb package: not on a Debian-based system".to_string() | ||||
|                         "Cannot install .deb package: not on a Debian-based system".to_string(), | ||||
|                     )); | ||||
|                 } | ||||
|             }, | ||||
|             Err(_) => return Err(DownloadError::PlatformNotSupported( | ||||
|                 "Failed to check system compatibility for .deb installation".to_string() | ||||
|             )), | ||||
|             } | ||||
|             Err(_) => { | ||||
|                 return Err(DownloadError::PlatformNotSupported( | ||||
|                     "Failed to check system compatibility for .deb installation".to_string(), | ||||
|                 )) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|  | ||||
|         // Install the .deb package non-interactively | ||||
|         println!("Installing package: {}", dest_path); | ||||
|         let install_result = Command::new("sudo") | ||||
|             .args(&["dpkg", "--install", &dest_path]) | ||||
|             .status(); | ||||
|              | ||||
|  | ||||
|         match install_result { | ||||
|             Ok(status) => { | ||||
|                 if !status.success() { | ||||
| @@ -424,24 +499,24 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE | ||||
|                     let fix_deps = Command::new("sudo") | ||||
|                         .args(&["apt-get", "install", "-f", "-y"]) | ||||
|                         .status(); | ||||
|                          | ||||
|  | ||||
|                     if let Ok(fix_status) = fix_deps { | ||||
|                         if !fix_status.success() { | ||||
|                             return Err(DownloadError::InstallationFailed( | ||||
|                                 "Failed to resolve package dependencies".to_string() | ||||
|                                 "Failed to resolve package dependencies".to_string(), | ||||
|                             )); | ||||
|                         } | ||||
|                     } else { | ||||
|                         return Err(DownloadError::InstallationFailed( | ||||
|                             "Failed to resolve package dependencies".to_string() | ||||
|                             "Failed to resolve package dependencies".to_string(), | ||||
|                         )); | ||||
|                     } | ||||
|                 } | ||||
|                 println!("Package installation completed successfully"); | ||||
|             }, | ||||
|             } | ||||
|             Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), | ||||
|         } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     Ok(download_result) | ||||
| } | ||||
|   | ||||
							
								
								
									
										597
									
								
								src/os/fs.rs
									
									
									
									
									
								
							
							
						
						
									
										597
									
								
								src/os/fs.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -794,7 +794,7 @@ pub fn query_opt_with_pool_params( | ||||
| /// This function sends a notification on the specified channel with the specified payload. | ||||
| /// | ||||
| /// Example: | ||||
| /// ``` | ||||
| /// ```no_run | ||||
| /// use sal::postgresclient::notify; | ||||
| /// | ||||
| /// notify("my_channel", "Hello, world!").expect("Failed to send notification"); | ||||
| @@ -810,7 +810,7 @@ pub fn notify(channel: &str, payload: &str) -> Result<(), PostgresError> { | ||||
| /// This function sends a notification on the specified channel with the specified payload using the connection pool. | ||||
| /// | ||||
| /// Example: | ||||
| /// ``` | ||||
| /// ```no_run | ||||
| /// use sal::postgresclient::notify_with_pool; | ||||
| /// | ||||
| /// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification"); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| use std::process::Command; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
| use std::io; | ||||
| use std::process::Command; | ||||
|  | ||||
| /// Error type for process management operations | ||||
| ///  | ||||
| /// | ||||
| /// This enum represents various errors that can occur during process management | ||||
| /// operations such as listing, finding, or killing processes. | ||||
| #[derive(Debug)] | ||||
| @@ -23,11 +23,18 @@ pub enum ProcessError { | ||||
| impl fmt::Display for ProcessError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             ProcessError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e), | ||||
|             ProcessError::CommandExecutionFailed(e) => { | ||||
|                 write!(f, "Failed to execute command: {}", e) | ||||
|             } | ||||
|             ProcessError::CommandFailed(e) => write!(f, "{}", e), | ||||
|             ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern), | ||||
|             ProcessError::MultipleProcessesFound(pattern, count) =>  | ||||
|                 write!(f, "Multiple processes ({}) found matching '{}'", count, pattern), | ||||
|             ProcessError::NoProcessFound(pattern) => { | ||||
|                 write!(f, "No processes found matching '{}'", pattern) | ||||
|             } | ||||
|             ProcessError::MultipleProcessesFound(pattern, count) => write!( | ||||
|                 f, | ||||
|                 "Multiple processes ({}) found matching '{}'", | ||||
|                 count, pattern | ||||
|             ), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -53,18 +60,20 @@ pub struct ProcessInfo { | ||||
|  | ||||
| /** | ||||
|  * Check if a command exists in PATH. | ||||
|  *  | ||||
|  * | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * | ||||
|  * * `cmd` - The command to check | ||||
|  *  | ||||
|  * | ||||
|  * # Returns | ||||
|  *  | ||||
|  * | ||||
|  * * `Option<String>` - The full path to the command if found, None otherwise | ||||
|  *  | ||||
|  * | ||||
|  * # Examples | ||||
|  *  | ||||
|  * | ||||
|  * ``` | ||||
|  * use sal::process::which; | ||||
|  * | ||||
|  * match which("git") { | ||||
|  *     Some(path) => println!("Git is installed at: {}", path), | ||||
|  *     None => println!("Git is not installed"), | ||||
| @@ -74,14 +83,12 @@ pub struct ProcessInfo { | ||||
| pub fn which(cmd: &str) -> Option<String> { | ||||
|     #[cfg(target_os = "windows")] | ||||
|     let which_cmd = "where"; | ||||
|      | ||||
|  | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     let which_cmd = "which"; | ||||
|      | ||||
|     let output = Command::new(which_cmd) | ||||
|         .arg(cmd) | ||||
|         .output(); | ||||
|      | ||||
|  | ||||
|     let output = Command::new(which_cmd).arg(cmd).output(); | ||||
|  | ||||
|     match output { | ||||
|         Ok(out) => { | ||||
|             if out.status.success() { | ||||
| @@ -90,29 +97,34 @@ pub fn which(cmd: &str) -> Option<String> { | ||||
|             } else { | ||||
|                 None | ||||
|             } | ||||
|         }, | ||||
|         Err(_) => None | ||||
|         } | ||||
|         Err(_) => None, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Kill processes matching a pattern. | ||||
|  *  | ||||
|  * | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * | ||||
|  * * `pattern` - The pattern to match against process names | ||||
|  *  | ||||
|  * | ||||
|  * # Returns | ||||
|  *  | ||||
|  * | ||||
|  * * `Ok(String)` - A success message indicating processes were killed or none were found | ||||
|  * * `Err(ProcessError)` - An error if the kill operation failed | ||||
|  *  | ||||
|  * | ||||
|  * # Examples | ||||
|  *  | ||||
|  * | ||||
|  * ``` | ||||
|  * // Kill all processes with "server" in their name | ||||
|  * let result = kill("server")?; | ||||
|  * println!("{}", result); | ||||
|  * use sal::process::kill; | ||||
|  * | ||||
|  * fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|  *     let result = kill("server")?; | ||||
|  *     println!("{}", result); | ||||
|  *     Ok(()) | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn kill(pattern: &str) -> Result<String, ProcessError> { | ||||
| @@ -121,7 +133,7 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> { | ||||
|     { | ||||
|         // On Windows, use taskkill with wildcard support | ||||
|         let mut args = vec!["/F"]; // Force kill | ||||
|          | ||||
|  | ||||
|         if pattern.contains('*') { | ||||
|             // If it contains wildcards, use filter | ||||
|             args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]); | ||||
| @@ -129,12 +141,12 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> { | ||||
|             // Otherwise use image name directly | ||||
|             args.extend(&["/IM", pattern]); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         let output = Command::new("taskkill") | ||||
|             .args(&args) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|  | ||||
|         if output.status.success() { | ||||
|             Ok("Successfully killed processes".to_string()) | ||||
|         } else { | ||||
| @@ -144,14 +156,20 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> { | ||||
|                 if stdout.contains("No tasks") { | ||||
|                     Ok("No matching processes found".to_string()) | ||||
|                 } else { | ||||
|                     Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout))) | ||||
|                     Err(ProcessError::CommandFailed(format!( | ||||
|                         "Failed to kill processes: {}", | ||||
|                         stdout | ||||
|                     ))) | ||||
|                 } | ||||
|             } else { | ||||
|                 Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error))) | ||||
|                 Err(ProcessError::CommandFailed(format!( | ||||
|                     "Failed to kill processes: {}", | ||||
|                     error | ||||
|                 ))) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     { | ||||
|         // On Unix-like systems, use pkill which has built-in pattern matching | ||||
| @@ -160,7 +178,7 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> { | ||||
|             .arg(pattern) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|  | ||||
|         // pkill returns 0 if processes were killed, 1 if none matched | ||||
|         if output.status.success() { | ||||
|             Ok("Successfully killed processes".to_string()) | ||||
| @@ -168,39 +186,47 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> { | ||||
|             Ok("No matching processes found".to_string()) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error))) | ||||
|             Err(ProcessError::CommandFailed(format!( | ||||
|                 "Failed to kill processes: {}", | ||||
|                 error | ||||
|             ))) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * List processes matching a pattern (or all if pattern is empty). | ||||
|  *  | ||||
|  * | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * | ||||
|  * * `pattern` - The pattern to match against process names (empty string for all processes) | ||||
|  *  | ||||
|  * | ||||
|  * # Returns | ||||
|  *  | ||||
|  * | ||||
|  * * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes | ||||
|  * * `Err(ProcessError)` - An error if the list operation failed | ||||
|  *  | ||||
|  * | ||||
|  * # Examples | ||||
|  *  | ||||
|  * | ||||
|  * ``` | ||||
|  * // List all processes | ||||
|  * let processes = process_list("")?; | ||||
|  *  | ||||
|  * // List processes with "server" in their name | ||||
|  * let processes = process_list("server")?; | ||||
|  * for proc in processes { | ||||
|  *     println!("PID: {}, Name: {}", proc.pid, proc.name); | ||||
|  * use sal::process::process_list; | ||||
|  * | ||||
|  * fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|  *     let processes = process_list("")?; | ||||
|  * | ||||
|  *     // List processes with "server" in their name | ||||
|  *     let processes = process_list("server")?; | ||||
|  *     for proc in processes { | ||||
|  *         println!("PID: {}, Name: {}", proc.pid, proc.name); | ||||
|  *     } | ||||
|  *     Ok(()) | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { | ||||
|     let mut processes = Vec::new(); | ||||
|      | ||||
|  | ||||
|     // Platform specific implementations | ||||
|     #[cfg(target_os = "windows")] | ||||
|     { | ||||
| @@ -209,22 +235,23 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { | ||||
|             .args(&["process", "list", "brief"]) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|  | ||||
|         if output.status.success() { | ||||
|             let stdout = String::from_utf8_lossy(&output.stdout).to_string(); | ||||
|              | ||||
|  | ||||
|             // Parse output (assuming format: Handle Name Priority) | ||||
|             for line in stdout.lines().skip(1) { // Skip header | ||||
|             for line in stdout.lines().skip(1) { | ||||
|                 // Skip header | ||||
|                 let parts: Vec<&str> = line.trim().split_whitespace().collect(); | ||||
|                 if parts.len() >= 2 { | ||||
|                     let pid = parts[0].parse::<i64>().unwrap_or(0); | ||||
|                     let name = parts[1].to_string(); | ||||
|                      | ||||
|  | ||||
|                     // Filter by pattern if provided | ||||
|                     if !pattern.is_empty() && !name.contains(pattern) { | ||||
|                         continue; | ||||
|                     } | ||||
|                      | ||||
|  | ||||
|                     processes.push(ProcessInfo { | ||||
|                         pid, | ||||
|                         name, | ||||
| @@ -235,10 +262,13 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { | ||||
|             } | ||||
|         } else { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr).to_string(); | ||||
|             return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr))); | ||||
|             return Err(ProcessError::CommandFailed(format!( | ||||
|                 "Failed to list processes: {}", | ||||
|                 stderr | ||||
|             ))); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     { | ||||
|         // Unix implementation using ps | ||||
| @@ -246,22 +276,23 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { | ||||
|             .args(&["-eo", "pid,comm"]) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|  | ||||
|         if output.status.success() { | ||||
|             let stdout = String::from_utf8_lossy(&output.stdout).to_string(); | ||||
|              | ||||
|  | ||||
|             // Parse output (assuming format: PID COMMAND) | ||||
|             for line in stdout.lines().skip(1) { // Skip header | ||||
|             for line in stdout.lines().skip(1) { | ||||
|                 // Skip header | ||||
|                 let parts: Vec<&str> = line.trim().split_whitespace().collect(); | ||||
|                 if parts.len() >= 2 { | ||||
|                     let pid = parts[0].parse::<i64>().unwrap_or(0); | ||||
|                     let name = parts[1].to_string(); | ||||
|                      | ||||
|  | ||||
|                     // Filter by pattern if provided | ||||
|                     if !pattern.is_empty() && !name.contains(pattern) { | ||||
|                         continue; | ||||
|                     } | ||||
|                      | ||||
|  | ||||
|                     processes.push(ProcessInfo { | ||||
|                         pid, | ||||
|                         name, | ||||
| @@ -272,38 +303,49 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { | ||||
|             } | ||||
|         } else { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr).to_string(); | ||||
|             return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr))); | ||||
|             return Err(ProcessError::CommandFailed(format!( | ||||
|                 "Failed to list processes: {}", | ||||
|                 stderr | ||||
|             ))); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     Ok(processes) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get a single process matching the pattern (error if 0 or more than 1 match). | ||||
|  *  | ||||
|  * | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * | ||||
|  * * `pattern` - The pattern to match against process names | ||||
|  *  | ||||
|  * | ||||
|  * # Returns | ||||
|  *  | ||||
|  * | ||||
|  * * `Ok(ProcessInfo)` - Information about the matching process | ||||
|  * * `Err(ProcessError)` - An error if no process or multiple processes match | ||||
|  *  | ||||
|  * | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let process = process_get("unique-server-name")?; | ||||
|  * println!("Found process: {} (PID: {})", process.name, process.pid); | ||||
|  * | ||||
|  * ```no_run | ||||
|  * use sal::process::process_get; | ||||
|  * | ||||
|  * fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|  *     let process = process_get("unique-server-name")?; | ||||
|  *     println!("Found process: {} (PID: {})", process.name, process.pid); | ||||
|  *     Ok(()) | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> { | ||||
|     let processes = process_list(pattern)?; | ||||
|      | ||||
|  | ||||
|     match processes.len() { | ||||
|         0 => Err(ProcessError::NoProcessFound(pattern.to_string())), | ||||
|         1 => Ok(processes[0].clone()), | ||||
|         _ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())), | ||||
|         _ => Err(ProcessError::MultipleProcessesFound( | ||||
|             pattern.to_string(), | ||||
|             processes.len(), | ||||
|         )), | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -116,7 +116,7 @@ pub use os::copy as os_copy; | ||||
| /// | ||||
| /// # Example | ||||
| /// | ||||
| /// ``` | ||||
| /// ```ignore | ||||
| /// use rhai::Engine; | ||||
| /// use sal::rhai; | ||||
| /// | ||||
| @@ -124,7 +124,8 @@ pub use os::copy as os_copy; | ||||
| /// rhai::register(&mut engine); | ||||
| /// | ||||
| /// // Now you can use SAL functions in Rhai scripts | ||||
| /// let result = engine.eval::<bool>("exist('some_file.txt')").unwrap(); | ||||
| /// // You can evaluate Rhai scripts with SAL functions | ||||
| /// let result = engine.eval::<i64>("exist('some_file.txt')").unwrap(); | ||||
| /// ``` | ||||
| pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> { | ||||
|     // Register OS module functions | ||||
|   | ||||
| @@ -1,30 +1,32 @@ | ||||
| /** | ||||
|  * Dedent a multiline string by removing common leading whitespace. | ||||
|  *  | ||||
|  * | ||||
|  * This function analyzes all non-empty lines in the input text to determine | ||||
|  * the minimum indentation level, then removes that amount of whitespace | ||||
|  * from the beginning of each line. This is useful for working with | ||||
|  * multi-line strings in code that have been indented to match the | ||||
|  * surrounding code structure. | ||||
|  *  | ||||
|  * | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * | ||||
|  * * `text` - The multiline string to dedent | ||||
|  *  | ||||
|  * | ||||
|  * # Returns | ||||
|  *  | ||||
|  * | ||||
|  * * `String` - The dedented string | ||||
|  *  | ||||
|  * | ||||
|  * # Examples | ||||
|  *  | ||||
|  * | ||||
|  * ``` | ||||
|  * use sal::text::dedent; | ||||
|  * | ||||
|  * let indented = "    line 1\n    line 2\n        line 3"; | ||||
|  * let dedented = dedent(indented); | ||||
|  * assert_eq!(dedented, "line 1\nline 2\n    line 3"); | ||||
|  * ``` | ||||
|  *  | ||||
|  * | ||||
|  * # Notes | ||||
|  *  | ||||
|  * | ||||
|  * - Empty lines are preserved but have all leading whitespace removed | ||||
|  * - Tabs are counted as 4 spaces for indentation purposes | ||||
|  */ | ||||
| @@ -32,7 +34,8 @@ pub fn dedent(text: &str) -> String { | ||||
|     let lines: Vec<&str> = text.lines().collect(); | ||||
|  | ||||
|     // Find the minimum indentation level (ignore empty lines) | ||||
|     let min_indent = lines.iter() | ||||
|     let min_indent = lines | ||||
|         .iter() | ||||
|         .filter(|line| !line.trim().is_empty()) | ||||
|         .map(|line| { | ||||
|             let mut spaces = 0; | ||||
| @@ -51,7 +54,8 @@ pub fn dedent(text: &str) -> String { | ||||
|         .unwrap_or(0); | ||||
|  | ||||
|     // Remove that many spaces from the beginning of each line | ||||
|     lines.iter() | ||||
|     lines | ||||
|         .iter() | ||||
|         .map(|line| { | ||||
|             if line.trim().is_empty() { | ||||
|                 return String::new(); | ||||
| @@ -59,22 +63,22 @@ pub fn dedent(text: &str) -> String { | ||||
|  | ||||
|             let mut count = 0; | ||||
|             let mut chars = line.chars().peekable(); | ||||
|              | ||||
|  | ||||
|             // Skip initial spaces up to min_indent | ||||
|             while count < min_indent && chars.peek().is_some() { | ||||
|                 match chars.peek() { | ||||
|                     Some(' ') => { | ||||
|                         chars.next(); | ||||
|                         count += 1; | ||||
|                     }, | ||||
|                     } | ||||
|                     Some('\t') => { | ||||
|                         chars.next(); | ||||
|                         count += 4; | ||||
|                     }, | ||||
|                     } | ||||
|                     _ => break, | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|  | ||||
|             // Return the remaining characters | ||||
|             chars.collect::<String>() | ||||
|         }) | ||||
| @@ -82,24 +86,25 @@ pub fn dedent(text: &str) -> String { | ||||
|         .join("\n") | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Prefix a multiline string with a specified prefix. | ||||
|  *  | ||||
|  * | ||||
|  * This function adds the specified prefix to the beginning of each line in the input text. | ||||
|  *  | ||||
|  * | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * | ||||
|  * * `text` - The multiline string to prefix | ||||
|  * * `prefix` - The prefix to add to each line | ||||
|  *  | ||||
|  * | ||||
|  * # Returns | ||||
|  *  | ||||
|  * | ||||
|  * * `String` - The prefixed string | ||||
|  *  | ||||
|  * | ||||
|  * # Examples | ||||
|  *  | ||||
|  * | ||||
|  * ``` | ||||
|  * use sal::text::prefix; | ||||
|  * | ||||
|  * let text = "line 1\nline 2\nline 3"; | ||||
|  * let prefixed = prefix(text, "    "); | ||||
|  * assert_eq!(prefixed, "    line 1\n    line 2\n    line 3"); | ||||
|   | ||||
| @@ -32,7 +32,7 @@ impl TemplateBuilder { | ||||
|     /// ``` | ||||
|     pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> { | ||||
|         let path_str = template_path.as_ref().to_string_lossy().to_string(); | ||||
|          | ||||
|  | ||||
|         // Verify the template file exists | ||||
|         if !Path::new(&path_str).exists() { | ||||
|             return Err(io::Error::new( | ||||
| @@ -40,14 +40,14 @@ impl TemplateBuilder { | ||||
|                 format!("Template file not found: {}", path_str), | ||||
|             )); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         Ok(Self { | ||||
|             template_path: path_str, | ||||
|             context: Context::new(), | ||||
|             tera: None, | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Adds a variable to the template context. | ||||
|     /// | ||||
|     /// # Arguments | ||||
| @@ -61,12 +61,15 @@ impl TemplateBuilder { | ||||
|     /// | ||||
|     /// # Example | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// ```no_run | ||||
|     /// use sal::text::TemplateBuilder; | ||||
|     /// | ||||
|     /// let builder = TemplateBuilder::open("templates/example.html")? | ||||
|     ///     .add_var("title", "Hello World") | ||||
|     ///     .add_var("username", "John Doe"); | ||||
|     /// fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     ///     let builder = TemplateBuilder::open("templates/example.html")? | ||||
|     ///         .add_var("title", "Hello World") | ||||
|     ///         .add_var("username", "John Doe"); | ||||
|     ///     Ok(()) | ||||
|     /// } | ||||
|     /// ``` | ||||
|     pub fn add_var<S, V>(mut self, name: S, value: V) -> Self | ||||
|     where | ||||
| @@ -76,7 +79,7 @@ impl TemplateBuilder { | ||||
|         self.context.insert(name.as_ref(), &value); | ||||
|         self | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Adds multiple variables to the template context from a HashMap. | ||||
|     /// | ||||
|     /// # Arguments | ||||
| @@ -89,16 +92,19 @@ impl TemplateBuilder { | ||||
|     /// | ||||
|     /// # Example | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// ```no_run | ||||
|     /// use sal::text::TemplateBuilder; | ||||
|     /// use std::collections::HashMap; | ||||
|     /// | ||||
|     /// let mut vars = HashMap::new(); | ||||
|     /// vars.insert("title", "Hello World"); | ||||
|     /// vars.insert("username", "John Doe"); | ||||
|     /// fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     ///     let mut vars = HashMap::new(); | ||||
|     ///     vars.insert("title", "Hello World"); | ||||
|     ///     vars.insert("username", "John Doe"); | ||||
|     /// | ||||
|     /// let builder = TemplateBuilder::open("templates/example.html")? | ||||
|     ///     .add_vars(vars); | ||||
|     ///     let builder = TemplateBuilder::open("templates/example.html")? | ||||
|     ///         .add_vars(vars); | ||||
|     ///     Ok(()) | ||||
|     /// } | ||||
|     /// ``` | ||||
|     pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self | ||||
|     where | ||||
| @@ -110,7 +116,7 @@ impl TemplateBuilder { | ||||
|         } | ||||
|         self | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Initializes the Tera template engine with the template file. | ||||
|     /// | ||||
|     /// This method is called automatically by render() if not called explicitly. | ||||
| @@ -122,24 +128,24 @@ impl TemplateBuilder { | ||||
|         if self.tera.is_none() { | ||||
|             // Create a new Tera instance with just this template | ||||
|             let mut tera = Tera::default(); | ||||
|              | ||||
|  | ||||
|             // Read the template content | ||||
|             let template_content = fs::read_to_string(&self.template_path) | ||||
|                 .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?; | ||||
|              | ||||
|  | ||||
|             // Add the template to Tera | ||||
|             let template_name = Path::new(&self.template_path) | ||||
|                 .file_name() | ||||
|                 .and_then(|n| n.to_str()) | ||||
|                 .unwrap_or("template"); | ||||
|                  | ||||
|  | ||||
|             tera.add_raw_template(template_name, &template_content)?; | ||||
|             self.tera = Some(tera); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Renders the template with the current context. | ||||
|     /// | ||||
|     /// # Returns | ||||
| @@ -148,31 +154,34 @@ impl TemplateBuilder { | ||||
|     /// | ||||
|     /// # Example | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// ```no_run | ||||
|     /// use sal::text::TemplateBuilder; | ||||
|     /// | ||||
|     /// let result = TemplateBuilder::open("templates/example.html")? | ||||
|     ///     .add_var("title", "Hello World") | ||||
|     ///     .add_var("username", "John Doe") | ||||
|     ///     .render()?; | ||||
|     /// fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     ///     let result = TemplateBuilder::open("templates/example.html")? | ||||
|     ///         .add_var("title", "Hello World") | ||||
|     ///         .add_var("username", "John Doe") | ||||
|     ///         .render()?; | ||||
|     /// | ||||
|     /// println!("Rendered template: {}", result); | ||||
|     ///     println!("Rendered template: {}", result); | ||||
|     ///     Ok(()) | ||||
|     /// } | ||||
|     /// ``` | ||||
|     pub fn render(&mut self) -> Result<String, tera::Error> { | ||||
|         // Initialize Tera if not already done | ||||
|         self.initialize_tera()?; | ||||
|          | ||||
|  | ||||
|         // Get the template name | ||||
|         let template_name = Path::new(&self.template_path) | ||||
|             .file_name() | ||||
|             .and_then(|n| n.to_str()) | ||||
|             .unwrap_or("template"); | ||||
|          | ||||
|  | ||||
|         // Render the template | ||||
|         let tera = self.tera.as_ref().unwrap(); | ||||
|         tera.render(template_name, &self.context) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Renders the template and writes the result to a file. | ||||
|     /// | ||||
|     /// # Arguments | ||||
| @@ -185,19 +194,25 @@ impl TemplateBuilder { | ||||
|     /// | ||||
|     /// # Example | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// ```no_run | ||||
|     /// use sal::text::TemplateBuilder; | ||||
|     /// | ||||
|     /// TemplateBuilder::open("templates/example.html")? | ||||
|     ///     .add_var("title", "Hello World") | ||||
|     ///     .add_var("username", "John Doe") | ||||
|     ///     .render_to_file("output.html")?; | ||||
|     /// fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     ///     TemplateBuilder::open("templates/example.html")? | ||||
|     ///         .add_var("title", "Hello World") | ||||
|     ///         .add_var("username", "John Doe") | ||||
|     ///         .render_to_file("output.html")?; | ||||
|     ///     Ok(()) | ||||
|     /// } | ||||
|     /// ``` | ||||
|     pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> { | ||||
|         let rendered = self.render().map_err(|e| { | ||||
|             io::Error::new(io::ErrorKind::Other, format!("Template rendering error: {}", e)) | ||||
|             io::Error::new( | ||||
|                 io::ErrorKind::Other, | ||||
|                 format!("Template rendering error: {}", e), | ||||
|             ) | ||||
|         })?; | ||||
|          | ||||
|  | ||||
|         fs::write(output_path, rendered) | ||||
|     } | ||||
| } | ||||
| @@ -207,70 +222,68 @@ mod tests { | ||||
|     use super::*; | ||||
|     use std::io::Write; | ||||
|     use tempfile::NamedTempFile; | ||||
|      | ||||
|  | ||||
|     #[test] | ||||
|     fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> { | ||||
|         // Create a temporary template file | ||||
|         let temp_file = NamedTempFile::new()?; | ||||
|         let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n"; | ||||
|         fs::write(temp_file.path(), template_content)?; | ||||
|          | ||||
|  | ||||
|         // Create a template builder and add variables | ||||
|         let mut builder = TemplateBuilder::open(temp_file.path())?; | ||||
|         builder = builder | ||||
|             .add_var("name", "John") | ||||
|             .add_var("place", "Rust"); | ||||
|          | ||||
|         builder = builder.add_var("name", "John").add_var("place", "Rust"); | ||||
|  | ||||
|         // Render the template | ||||
|         let result = builder.render()?; | ||||
|         assert_eq!(result, "Hello, John! Welcome to Rust.\n"); | ||||
|          | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     #[test] | ||||
|     fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> { | ||||
|         // Create a temporary template file | ||||
|         let temp_file = NamedTempFile::new()?; | ||||
|         let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n"; | ||||
|         fs::write(temp_file.path(), template_content)?; | ||||
|          | ||||
|  | ||||
|         // Create a template builder and add variables | ||||
|         let mut builder = TemplateBuilder::open(temp_file.path())?; | ||||
|          | ||||
|  | ||||
|         // Add variables including a boolean and a vector | ||||
|         builder = builder | ||||
|             .add_var("name", "Alice") | ||||
|             .add_var("show_greeting", true) | ||||
|             .add_var("items", vec!["apple", "banana", "cherry"]); | ||||
|          | ||||
|  | ||||
|         // Render the template | ||||
|         let result = builder.render()?; | ||||
|         assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n"); | ||||
|          | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     #[test] | ||||
|     fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> { | ||||
|         // Create a temporary template file | ||||
|         let mut temp_file = NamedTempFile::new()?; | ||||
|         writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?; | ||||
|         temp_file.flush()?; | ||||
|          | ||||
|  | ||||
|         // Create a HashMap of variables | ||||
|         let mut vars = HashMap::new(); | ||||
|         vars.insert("greeting", "Hi"); | ||||
|         vars.insert("name", "Bob"); | ||||
|          | ||||
|  | ||||
|         // Create a template builder and add variables from HashMap | ||||
|         let mut builder = TemplateBuilder::open(temp_file.path())?; | ||||
|         builder = builder.add_vars(vars); | ||||
|          | ||||
|  | ||||
|         // Render the template | ||||
|         let result = builder.render()?; | ||||
|         assert_eq!(result, "Hi, Bob!\n"); | ||||
|          | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|     #[test] | ||||
| @@ -279,20 +292,19 @@ mod tests { | ||||
|         let temp_file = NamedTempFile::new()?; | ||||
|         let template_content = "{{ message }}\n"; | ||||
|         fs::write(temp_file.path(), template_content)?; | ||||
|          | ||||
|          | ||||
|  | ||||
|         // Create an output file | ||||
|         let output_file = NamedTempFile::new()?; | ||||
|          | ||||
|  | ||||
|         // Create a template builder, add a variable, and render to file | ||||
|         let mut builder = TemplateBuilder::open(temp_file.path())?; | ||||
|         builder = builder.add_var("message", "This is a test"); | ||||
|         builder.render_to_file(output_file.path())?; | ||||
|          | ||||
|  | ||||
|         // Read the output file and verify its contents | ||||
|         let content = fs::read_to_string(output_file.path())?; | ||||
|         assert_eq!(content, "This is a test\n"); | ||||
|          | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user