Compare commits

...

2 Commits

Author SHA1 Message Date
Mahmoud Emad
8285fdb7b9 Merge branch 'main' of https://git.ourworld.tf/herocode/sal
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled
2025-05-10 08:50:16 +03:00
Mahmoud Emad
1ebd591f19 feat: Enhance documentation and add .gitignore entries
- Add new documentation sections for PostgreSQL installer
  functions and usage examples.  Improves clarity and
  completeness of the documentation.
- Add new files and patterns to .gitignore to prevent
  unnecessary files from being committed to the repository.
  Improves repository cleanliness and reduces clutter.
2025-05-10 08:50:05 +03:00
8 changed files with 766 additions and 504 deletions

6
.gitignore vendored
View File

@ -22,4 +22,8 @@ Cargo.lock
/rhai_test_template /rhai_test_template
/rhai_test_download /rhai_test_download
/rhai_test_fs /rhai_test_fs
run_rhai_tests.log run_rhai_tests.log
new_location
log.txt
file.txt
fix_doc*

View File

@ -1,9 +1,9 @@
use std::process::Command;
use std::path::Path;
use std::fs;
use std::fmt;
use std::error::Error; use std::error::Error;
use std::fmt;
use std::fs;
use std::io; use std::io;
use std::path::Path;
use std::process::Command;
// Define a custom error type for download operations // Define a custom error type for download operations
#[derive(Debug)] #[derive(Debug)]
@ -26,11 +26,17 @@ pub enum DownloadError {
impl fmt::Display for DownloadError { impl fmt::Display for DownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { 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::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url), DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e), 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::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e), DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e), DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
@ -74,12 +80,18 @@ impl Error for DownloadError {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* // Download a file with no minimum size requirement * use sal::os::download;
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
* *
* // Download a file with minimum size requirement of 100KB * fn main() -> Result<(), Box<dyn std::error::Error>> {
* let path = download("https://example.com/file.zip", "/tmp/", 100)?; * // 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 * # 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 // Create parent directories if they don't exist
let dest_path = Path::new(dest); let dest_path = Path::new(dest);
fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?; fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
// Extract filename from URL // Extract filename from URL
let filename = match url.split('/').last() { let filename = match url.split('/').last() {
Some(name) => name, 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 // Create a full path for the downloaded file
let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename); let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
// Create a temporary path for downloading // Create a temporary path for downloading
let temp_path = format!("{}.download", file_path); let temp_path = format!("{}.download", file_path);
// Use curl to download the file with progress bar // Use curl to download the file with progress bar
println!("Downloading {} to {}", url, file_path); println!("Downloading {} to {}", url, file_path);
let output = Command::new("curl") let output = Command::new("curl")
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) .args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status() .status()
.map_err(DownloadError::CurlExecutionFailed)?; .map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() { if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string())); return Err(DownloadError::DownloadFailed(url.to_string()));
} }
// Show file size after download // Show file size after download
match fs::metadata(&temp_path) { match fs::metadata(&temp_path) {
Ok(metadata) => { 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_kb = size_bytes / 1024;
let size_mb = size_kb / 1024; let size_mb = size_kb / 1024;
if size_mb > 1 { 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 { } 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!"), Err(_) => println!("Download complete!"),
} }
// Check file size if minimum size is specified // Check file size if minimum size is specified
if min_size_kb > 0 { if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; 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)); return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
} }
} }
// Check if it's a compressed file that needs extraction // Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase(); let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz") || let is_archive = lower_url.ends_with(".tar.gz")
lower_url.ends_with(".tgz") || || lower_url.ends_with(".tgz")
lower_url.ends_with(".tar") || || lower_url.ends_with(".tar")
lower_url.ends_with(".zip"); || lower_url.ends_with(".zip");
if is_archive { if is_archive {
// Extract the file using the appropriate command with progress indication // Extract the file using the appropriate command with progress indication
println!("Extracting {} to {}", temp_path, dest); println!("Extracting {} to {}", temp_path, dest);
let output = if lower_url.ends_with(".zip") { let output = if lower_url.ends_with(".zip") {
Command::new("unzip") Command::new("unzip")
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity .args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
.status() .status()
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") { } else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
Command::new("tar") Command::new("tar")
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity .args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
.status() .status()
} else { } else {
Command::new("tar") Command::new("tar")
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity .args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
.status() .status()
}; };
match output { match output {
Ok(status) => { Ok(status) => {
if !status.success() { 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)), Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
} }
// Show number of extracted files // Show number of extracted files
match fs::read_dir(dest) { match fs::read_dir(dest) {
Ok(entries) => { Ok(entries) => {
let count = entries.count(); let count = entries.count();
println!("Extraction complete! Extracted {} files/directories", count); println!("Extraction complete! Extracted {} files/directories", count);
}, }
Err(_) => println!("Extraction complete!"), Err(_) => println!("Extraction complete!"),
} }
// Remove the temporary file // Remove the temporary file
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
Ok(dest.to_string()) Ok(dest.to_string())
} else { } else {
// Just rename the temporary file to the final destination // Just rename the temporary file to the final destination
fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(file_path) Ok(file_path)
} }
} }
@ -210,12 +241,18 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* // Download a file with no minimum size requirement * use sal::os::download_file;
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
* *
* // Download a file with minimum size requirement of 100KB * fn main() -> Result<(), Box<dyn std::error::Error>> {
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?; * // 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> { 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() { if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?; fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
} }
// Create a temporary path for downloading // Create a temporary path for downloading
let temp_path = format!("{}.download", dest); let temp_path = format!("{}.download", dest);
// Use curl to download the file with progress bar // Use curl to download the file with progress bar
println!("Downloading {} to {}", url, dest); println!("Downloading {} to {}", url, dest);
let output = Command::new("curl") let output = Command::new("curl")
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) .args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status() .status()
.map_err(DownloadError::CurlExecutionFailed)?; .map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() { if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string())); return Err(DownloadError::DownloadFailed(url.to_string()));
} }
// Show file size after download // Show file size after download
match fs::metadata(&temp_path) { match fs::metadata(&temp_path) {
Ok(metadata) => { 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_kb = size_bytes / 1024;
let size_mb = size_kb / 1024; let size_mb = size_kb / 1024;
if size_mb > 1 { 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 { } 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!"), Err(_) => println!("Download complete!"),
} }
// Check file size if minimum size is specified // Check file size if minimum size is specified
if min_size_kb > 0 { if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; 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)); return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
} }
} }
// Rename the temporary file to the final destination // Rename the temporary file to the final destination
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(dest.to_string()) Ok(dest.to_string())
} }
@ -284,27 +334,38 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* // Make a file executable * use sal::os::chmod_exec;
* chmod_exec("/path/to/file")?; *
* 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> { pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
let path_obj = Path::new(path); let path_obj = Path::new(path);
// Check if the path exists and is a file // Check if the path exists and is a file
if !path_obj.exists() { 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() { 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 // Get current permissions
let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?; let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
let mut permissions = metadata.permissions(); let mut permissions = metadata.permissions();
// Set executable bit for user, group, and others // Set executable bit for user, group, and others
#[cfg(unix)] #[cfg(unix)]
{ {
@ -314,47 +375,55 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
let new_mode = mode | 0o111; let new_mode = mode | 0o111;
permissions.set_mode(new_mode); permissions.set_mode(new_mode);
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
// On non-Unix platforms, we can't set executable bit directly // On non-Unix platforms, we can't set executable bit directly
// Just return success with a warning // 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 // 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( DownloadError::CommandExecutionFailed(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
format!("Failed to set executable permissions: {}", e) format!("Failed to set executable permissions: {}", e),
)) ))
)?; })?;
Ok(format!("Made {} executable", path)) Ok(format!("Made {} executable", path))
} }
/** /**
* Download a file and install it if it's a supported package format. * Download a file and install it if it's a supported package format.
* *
* # Arguments * # Arguments
* *
* * `url` - The URL to download from * * `url` - The URL to download from
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum) * * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
* *
* # Returns * # Returns
* *
* * `Ok(String)` - The path where the file was saved or extracted * * `Ok(String)` - The path where the file was saved or extracted
* * `Err(DownloadError)` - An error if the download or installation failed * * `Err(DownloadError)` - An error if the download or installation failed
* *
* # Examples * # 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 * # Notes
* *
* Currently only supports .deb packages on Debian-based systems. * Currently only supports .deb packages on Debian-based systems.
* For other file types, it behaves the same as the download function. * 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 // Extract filename from URL
let filename = match url.split('/').last() { let filename = match url.split('/').last() {
Some(name) => name, 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 // Create a proper destination path
let dest_path = format!("/tmp/{}", filename); let dest_path = format!("/tmp/{}", filename);
// Check if it's a compressed file that needs extraction // Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase(); let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz") || let is_archive = lower_url.ends_with(".tar.gz")
lower_url.ends_with(".tgz") || || lower_url.ends_with(".tgz")
lower_url.ends_with(".tar") || || lower_url.ends_with(".tar")
lower_url.ends_with(".zip"); || lower_url.ends_with(".zip");
let download_result = if is_archive { let download_result = if is_archive {
// For archives, use the directory-based download function // For archives, use the directory-based download function
download(url, "/tmp", min_size_kb)? 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 // For regular files, use the file-specific download function
download_file(url, &dest_path, min_size_kb)? download_file(url, &dest_path, min_size_kb)?
}; };
// Check if the downloaded result is a file // Check if the downloaded result is a file
let path = Path::new(&dest_path); let path = Path::new(&dest_path);
if !path.is_file() { if !path.is_file() {
return Ok(download_result); // Not a file, might be an extracted directory return Ok(download_result); // Not a file, might be an extracted directory
} }
// Check if it's a .deb package // Check if it's a .deb package
if dest_path.to_lowercase().ends_with(".deb") { if dest_path.to_lowercase().ends_with(".deb") {
// Check if we're on a Debian-based platform // 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("-c")
.arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version") .arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
.status(); .status();
match platform_check { match platform_check {
Ok(status) => { Ok(status) => {
if !status.success() { if !status.success() {
return Err(DownloadError::PlatformNotSupported( 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( Err(_) => {
"Failed to check system compatibility for .deb installation".to_string() return Err(DownloadError::PlatformNotSupported(
)), "Failed to check system compatibility for .deb installation".to_string(),
))
}
} }
// Install the .deb package non-interactively // Install the .deb package non-interactively
println!("Installing package: {}", dest_path); println!("Installing package: {}", dest_path);
let install_result = Command::new("sudo") let install_result = Command::new("sudo")
.args(&["dpkg", "--install", &dest_path]) .args(&["dpkg", "--install", &dest_path])
.status(); .status();
match install_result { match install_result {
Ok(status) => { Ok(status) => {
if !status.success() { 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") let fix_deps = Command::new("sudo")
.args(&["apt-get", "install", "-f", "-y"]) .args(&["apt-get", "install", "-f", "-y"])
.status(); .status();
if let Ok(fix_status) = fix_deps { if let Ok(fix_status) = fix_deps {
if !fix_status.success() { if !fix_status.success() {
return Err(DownloadError::InstallationFailed( return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string() "Failed to resolve package dependencies".to_string(),
)); ));
} }
} else { } else {
return Err(DownloadError::InstallationFailed( return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string() "Failed to resolve package dependencies".to_string(),
)); ));
} }
} }
println!("Package installation completed successfully"); println!("Package installation completed successfully");
}, }
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
} }
} }
Ok(download_result) Ok(download_result)
} }

File diff suppressed because it is too large Load Diff

View File

@ -794,7 +794,7 @@ pub fn query_opt_with_pool_params(
/// This function sends a notification on the specified channel with the specified payload. /// This function sends a notification on the specified channel with the specified payload.
/// ///
/// Example: /// Example:
/// ``` /// ```no_run
/// use sal::postgresclient::notify; /// use sal::postgresclient::notify;
/// ///
/// notify("my_channel", "Hello, world!").expect("Failed to send notification"); /// 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. /// This function sends a notification on the specified channel with the specified payload using the connection pool.
/// ///
/// Example: /// Example:
/// ``` /// ```no_run
/// use sal::postgresclient::notify_with_pool; /// use sal::postgresclient::notify_with_pool;
/// ///
/// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification"); /// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification");

View File

@ -1,10 +1,10 @@
use std::process::Command;
use std::fmt;
use std::error::Error; use std::error::Error;
use std::fmt;
use std::io; use std::io;
use std::process::Command;
/// Error type for process management operations /// Error type for process management operations
/// ///
/// This enum represents various errors that can occur during process management /// This enum represents various errors that can occur during process management
/// operations such as listing, finding, or killing processes. /// operations such as listing, finding, or killing processes.
#[derive(Debug)] #[derive(Debug)]
@ -23,11 +23,18 @@ pub enum ProcessError {
impl fmt::Display for ProcessError { impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { 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::CommandFailed(e) => write!(f, "{}", e),
ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern), ProcessError::NoProcessFound(pattern) => {
ProcessError::MultipleProcessesFound(pattern, count) => write!(f, "No processes found matching '{}'", pattern)
write!(f, "Multiple processes ({}) found matching '{}'", count, 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. * Check if a command exists in PATH.
* *
* # Arguments * # Arguments
* *
* * `cmd` - The command to check * * `cmd` - The command to check
* *
* # Returns * # Returns
* *
* * `Option<String>` - The full path to the command if found, None otherwise * * `Option<String>` - The full path to the command if found, None otherwise
* *
* # Examples * # Examples
* *
* ``` * ```
* use sal::process::which;
*
* match which("git") { * match which("git") {
* Some(path) => println!("Git is installed at: {}", path), * Some(path) => println!("Git is installed at: {}", path),
* None => println!("Git is not installed"), * None => println!("Git is not installed"),
@ -74,14 +83,12 @@ pub struct ProcessInfo {
pub fn which(cmd: &str) -> Option<String> { pub fn which(cmd: &str) -> Option<String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let which_cmd = "where"; let which_cmd = "where";
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let which_cmd = "which"; let which_cmd = "which";
let output = Command::new(which_cmd) let output = Command::new(which_cmd).arg(cmd).output();
.arg(cmd)
.output();
match output { match output {
Ok(out) => { Ok(out) => {
if out.status.success() { if out.status.success() {
@ -90,29 +97,34 @@ pub fn which(cmd: &str) -> Option<String> {
} else { } else {
None None
} }
}, }
Err(_) => None Err(_) => None,
} }
} }
/** /**
* Kill processes matching a pattern. * Kill processes matching a pattern.
* *
* # Arguments * # Arguments
* *
* * `pattern` - The pattern to match against process names * * `pattern` - The pattern to match against process names
* *
* # Returns * # Returns
* *
* * `Ok(String)` - A success message indicating processes were killed or none were found * * `Ok(String)` - A success message indicating processes were killed or none were found
* * `Err(ProcessError)` - An error if the kill operation failed * * `Err(ProcessError)` - An error if the kill operation failed
* *
* # Examples * # Examples
* *
* ``` * ```
* // Kill all processes with "server" in their name * // Kill all processes with "server" in their name
* let result = kill("server")?; * use sal::process::kill;
* println!("{}", result); *
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = kill("server")?;
* println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn kill(pattern: &str) -> Result<String, ProcessError> { 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 // On Windows, use taskkill with wildcard support
let mut args = vec!["/F"]; // Force kill let mut args = vec!["/F"]; // Force kill
if pattern.contains('*') { if pattern.contains('*') {
// If it contains wildcards, use filter // If it contains wildcards, use filter
args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]); args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
@ -129,12 +141,12 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
// Otherwise use image name directly // Otherwise use image name directly
args.extend(&["/IM", pattern]); args.extend(&["/IM", pattern]);
} }
let output = Command::new("taskkill") let output = Command::new("taskkill")
.args(&args) .args(&args)
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() { if output.status.success() {
Ok("Successfully killed processes".to_string()) Ok("Successfully killed processes".to_string())
} else { } else {
@ -144,14 +156,20 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
if stdout.contains("No tasks") { if stdout.contains("No tasks") {
Ok("No matching processes found".to_string()) Ok("No matching processes found".to_string())
} else { } else {
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout))) Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
stdout
)))
} }
} else { } 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"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
{ {
// On Unix-like systems, use pkill which has built-in pattern matching // 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) .arg(pattern)
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
// pkill returns 0 if processes were killed, 1 if none matched // pkill returns 0 if processes were killed, 1 if none matched
if output.status.success() { if output.status.success() {
Ok("Successfully killed processes".to_string()) 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()) Ok("No matching processes found".to_string())
} else { } else {
let error = String::from_utf8_lossy(&output.stderr); 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). * List processes matching a pattern (or all if pattern is empty).
* *
* # Arguments * # Arguments
* *
* * `pattern` - The pattern to match against process names (empty string for all processes) * * `pattern` - The pattern to match against process names (empty string for all processes)
* *
* # Returns * # Returns
* *
* * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes * * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
* * `Err(ProcessError)` - An error if the list operation failed * * `Err(ProcessError)` - An error if the list operation failed
* *
* # Examples * # Examples
* *
* ``` * ```
* // List all processes * // List all processes
* let processes = process_list("")?; * use sal::process::process_list;
* *
* // List processes with "server" in their name * fn main() -> Result<(), Box<dyn std::error::Error>> {
* let processes = process_list("server")?; * let processes = process_list("")?;
* for proc in processes { *
* println!("PID: {}, Name: {}", proc.pid, proc.name); * // 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> { pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
let mut processes = Vec::new(); let mut processes = Vec::new();
// Platform specific implementations // Platform specific implementations
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@ -209,22 +235,23 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
.args(&["process", "list", "brief"]) .args(&["process", "list", "brief"])
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() { if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: Handle Name Priority) // 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(); let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0); let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string(); let name = parts[1].to_string();
// Filter by pattern if provided // Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) { if !pattern.is_empty() && !name.contains(pattern) {
continue; continue;
} }
processes.push(ProcessInfo { processes.push(ProcessInfo {
pid, pid,
name, name,
@ -235,10 +262,13 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
} }
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 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"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
{ {
// Unix implementation using ps // Unix implementation using ps
@ -246,22 +276,23 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
.args(&["-eo", "pid,comm"]) .args(&["-eo", "pid,comm"])
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() { if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: PID COMMAND) // 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(); let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0); let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string(); let name = parts[1].to_string();
// Filter by pattern if provided // Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) { if !pattern.is_empty() && !name.contains(pattern) {
continue; continue;
} }
processes.push(ProcessInfo { processes.push(ProcessInfo {
pid, pid,
name, name,
@ -272,38 +303,49 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
} }
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 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) Ok(processes)
} }
/** /**
* Get a single process matching the pattern (error if 0 or more than 1 match). * Get a single process matching the pattern (error if 0 or more than 1 match).
* *
* # Arguments * # Arguments
* *
* * `pattern` - The pattern to match against process names * * `pattern` - The pattern to match against process names
* *
* # Returns * # Returns
* *
* * `Ok(ProcessInfo)` - Information about the matching process * * `Ok(ProcessInfo)` - Information about the matching process
* * `Err(ProcessError)` - An error if no process or multiple processes match * * `Err(ProcessError)` - An error if no process or multiple processes match
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* let process = process_get("unique-server-name")?; * use sal::process::process_get;
* println!("Found process: {} (PID: {})", process.name, process.pid); *
* 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> { pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
let processes = process_list(pattern)?; let processes = process_list(pattern)?;
match processes.len() { match processes.len() {
0 => Err(ProcessError::NoProcessFound(pattern.to_string())), 0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
1 => Ok(processes[0].clone()), 1 => Ok(processes[0].clone()),
_ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())), _ => Err(ProcessError::MultipleProcessesFound(
pattern.to_string(),
processes.len(),
)),
} }
} }

View File

@ -116,7 +116,7 @@ pub use os::copy as os_copy;
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```ignore
/// use rhai::Engine; /// use rhai::Engine;
/// use sal::rhai; /// use sal::rhai;
/// ///
@ -124,7 +124,8 @@ pub use os::copy as os_copy;
/// rhai::register(&mut engine); /// rhai::register(&mut engine);
/// ///
/// // Now you can use SAL functions in Rhai scripts /// // 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>> { pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register OS module functions // Register OS module functions

View File

@ -1,30 +1,32 @@
/** /**
* Dedent a multiline string by removing common leading whitespace. * Dedent a multiline string by removing common leading whitespace.
* *
* This function analyzes all non-empty lines in the input text to determine * This function analyzes all non-empty lines in the input text to determine
* the minimum indentation level, then removes that amount of whitespace * the minimum indentation level, then removes that amount of whitespace
* from the beginning of each line. This is useful for working with * from the beginning of each line. This is useful for working with
* multi-line strings in code that have been indented to match the * multi-line strings in code that have been indented to match the
* surrounding code structure. * surrounding code structure.
* *
* # Arguments * # Arguments
* *
* * `text` - The multiline string to dedent * * `text` - The multiline string to dedent
* *
* # Returns * # Returns
* *
* * `String` - The dedented string * * `String` - The dedented string
* *
* # Examples * # Examples
* *
* ``` * ```
* use sal::text::dedent;
*
* let indented = " line 1\n line 2\n line 3"; * let indented = " line 1\n line 2\n line 3";
* let dedented = dedent(indented); * let dedented = dedent(indented);
* assert_eq!(dedented, "line 1\nline 2\n line 3"); * assert_eq!(dedented, "line 1\nline 2\n line 3");
* ``` * ```
* *
* # Notes * # Notes
* *
* - Empty lines are preserved but have all leading whitespace removed * - Empty lines are preserved but have all leading whitespace removed
* - Tabs are counted as 4 spaces for indentation purposes * - 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(); let lines: Vec<&str> = text.lines().collect();
// Find the minimum indentation level (ignore empty lines) // Find the minimum indentation level (ignore empty lines)
let min_indent = lines.iter() let min_indent = lines
.iter()
.filter(|line| !line.trim().is_empty()) .filter(|line| !line.trim().is_empty())
.map(|line| { .map(|line| {
let mut spaces = 0; let mut spaces = 0;
@ -51,7 +54,8 @@ pub fn dedent(text: &str) -> String {
.unwrap_or(0); .unwrap_or(0);
// Remove that many spaces from the beginning of each line // Remove that many spaces from the beginning of each line
lines.iter() lines
.iter()
.map(|line| { .map(|line| {
if line.trim().is_empty() { if line.trim().is_empty() {
return String::new(); return String::new();
@ -59,22 +63,22 @@ pub fn dedent(text: &str) -> String {
let mut count = 0; let mut count = 0;
let mut chars = line.chars().peekable(); let mut chars = line.chars().peekable();
// Skip initial spaces up to min_indent // Skip initial spaces up to min_indent
while count < min_indent && chars.peek().is_some() { while count < min_indent && chars.peek().is_some() {
match chars.peek() { match chars.peek() {
Some(' ') => { Some(' ') => {
chars.next(); chars.next();
count += 1; count += 1;
}, }
Some('\t') => { Some('\t') => {
chars.next(); chars.next();
count += 4; count += 4;
}, }
_ => break, _ => break,
} }
} }
// Return the remaining characters // Return the remaining characters
chars.collect::<String>() chars.collect::<String>()
}) })
@ -82,24 +86,25 @@ pub fn dedent(text: &str) -> String {
.join("\n") .join("\n")
} }
/** /**
* Prefix a multiline string with a specified prefix. * Prefix a multiline string with a specified prefix.
* *
* This function adds the specified prefix to the beginning of each line in the input text. * This function adds the specified prefix to the beginning of each line in the input text.
* *
* # Arguments * # Arguments
* *
* * `text` - The multiline string to prefix * * `text` - The multiline string to prefix
* * `prefix` - The prefix to add to each line * * `prefix` - The prefix to add to each line
* *
* # Returns * # Returns
* *
* * `String` - The prefixed string * * `String` - The prefixed string
* *
* # Examples * # Examples
* *
* ``` * ```
* use sal::text::prefix;
*
* let text = "line 1\nline 2\nline 3"; * let text = "line 1\nline 2\nline 3";
* let prefixed = prefix(text, " "); * let prefixed = prefix(text, " ");
* assert_eq!(prefixed, " line 1\n line 2\n line 3"); * assert_eq!(prefixed, " line 1\n line 2\n line 3");

View File

@ -32,7 +32,7 @@ impl TemplateBuilder {
/// ``` /// ```
pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> { pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
let path_str = template_path.as_ref().to_string_lossy().to_string(); let path_str = template_path.as_ref().to_string_lossy().to_string();
// Verify the template file exists // Verify the template file exists
if !Path::new(&path_str).exists() { if !Path::new(&path_str).exists() {
return Err(io::Error::new( return Err(io::Error::new(
@ -40,14 +40,14 @@ impl TemplateBuilder {
format!("Template file not found: {}", path_str), format!("Template file not found: {}", path_str),
)); ));
} }
Ok(Self { Ok(Self {
template_path: path_str, template_path: path_str,
context: Context::new(), context: Context::new(),
tera: None, tera: None,
}) })
} }
/// Adds a variable to the template context. /// Adds a variable to the template context.
/// ///
/// # Arguments /// # Arguments
@ -61,12 +61,15 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// let builder = TemplateBuilder::open("templates/example.html")? /// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// .add_var("title", "Hello World") /// let builder = TemplateBuilder::open("templates/example.html")?
/// .add_var("username", "John Doe"); /// .add_var("title", "Hello World")
/// .add_var("username", "John Doe");
/// Ok(())
/// }
/// ``` /// ```
pub fn add_var<S, V>(mut self, name: S, value: V) -> Self pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
where where
@ -76,7 +79,7 @@ impl TemplateBuilder {
self.context.insert(name.as_ref(), &value); self.context.insert(name.as_ref(), &value);
self self
} }
/// Adds multiple variables to the template context from a HashMap. /// Adds multiple variables to the template context from a HashMap.
/// ///
/// # Arguments /// # Arguments
@ -89,16 +92,19 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// use std::collections::HashMap; /// use std::collections::HashMap;
/// ///
/// let mut vars = HashMap::new(); /// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// vars.insert("title", "Hello World"); /// let mut vars = HashMap::new();
/// vars.insert("username", "John Doe"); /// vars.insert("title", "Hello World");
/// vars.insert("username", "John Doe");
/// ///
/// let builder = TemplateBuilder::open("templates/example.html")? /// let builder = TemplateBuilder::open("templates/example.html")?
/// .add_vars(vars); /// .add_vars(vars);
/// Ok(())
/// }
/// ``` /// ```
pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
where where
@ -110,7 +116,7 @@ impl TemplateBuilder {
} }
self self
} }
/// Initializes the Tera template engine with the template file. /// Initializes the Tera template engine with the template file.
/// ///
/// This method is called automatically by render() if not called explicitly. /// This method is called automatically by render() if not called explicitly.
@ -122,24 +128,24 @@ impl TemplateBuilder {
if self.tera.is_none() { if self.tera.is_none() {
// Create a new Tera instance with just this template // Create a new Tera instance with just this template
let mut tera = Tera::default(); let mut tera = Tera::default();
// Read the template content // Read the template content
let template_content = fs::read_to_string(&self.template_path) let template_content = fs::read_to_string(&self.template_path)
.map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?; .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?;
// Add the template to Tera // Add the template to Tera
let template_name = Path::new(&self.template_path) let template_name = Path::new(&self.template_path)
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("template"); .unwrap_or("template");
tera.add_raw_template(template_name, &template_content)?; tera.add_raw_template(template_name, &template_content)?;
self.tera = Some(tera); self.tera = Some(tera);
} }
Ok(()) Ok(())
} }
/// Renders the template with the current context. /// Renders the template with the current context.
/// ///
/// # Returns /// # Returns
@ -148,31 +154,34 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// let result = TemplateBuilder::open("templates/example.html")? /// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// .add_var("title", "Hello World") /// let result = TemplateBuilder::open("templates/example.html")?
/// .add_var("username", "John Doe") /// .add_var("title", "Hello World")
/// .render()?; /// .add_var("username", "John Doe")
/// .render()?;
/// ///
/// println!("Rendered template: {}", result); /// println!("Rendered template: {}", result);
/// Ok(())
/// }
/// ``` /// ```
pub fn render(&mut self) -> Result<String, tera::Error> { pub fn render(&mut self) -> Result<String, tera::Error> {
// Initialize Tera if not already done // Initialize Tera if not already done
self.initialize_tera()?; self.initialize_tera()?;
// Get the template name // Get the template name
let template_name = Path::new(&self.template_path) let template_name = Path::new(&self.template_path)
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("template"); .unwrap_or("template");
// Render the template // Render the template
let tera = self.tera.as_ref().unwrap(); let tera = self.tera.as_ref().unwrap();
tera.render(template_name, &self.context) tera.render(template_name, &self.context)
} }
/// Renders the template and writes the result to a file. /// Renders the template and writes the result to a file.
/// ///
/// # Arguments /// # Arguments
@ -185,19 +194,25 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// TemplateBuilder::open("templates/example.html")? /// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// .add_var("title", "Hello World") /// TemplateBuilder::open("templates/example.html")?
/// .add_var("username", "John Doe") /// .add_var("title", "Hello World")
/// .render_to_file("output.html")?; /// .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<()> { pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
let rendered = self.render().map_err(|e| { 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) fs::write(output_path, rendered)
} }
} }
@ -207,70 +222,68 @@ mod tests {
use super::*; use super::*;
use std::io::Write; use std::io::Write;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
#[test] #[test]
fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> { fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file // Create a temporary template file
let temp_file = NamedTempFile::new()?; let temp_file = NamedTempFile::new()?;
let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n"; let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n";
fs::write(temp_file.path(), template_content)?; fs::write(temp_file.path(), template_content)?;
// Create a template builder and add variables // Create a template builder and add variables
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder builder = builder.add_var("name", "John").add_var("place", "Rust");
.add_var("name", "John")
.add_var("place", "Rust");
// Render the template // Render the template
let result = builder.render()?; let result = builder.render()?;
assert_eq!(result, "Hello, John! Welcome to Rust.\n"); assert_eq!(result, "Hello, John! Welcome to Rust.\n");
Ok(()) Ok(())
} }
#[test] #[test]
fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> { fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file // Create a temporary template file
let temp_file = NamedTempFile::new()?; 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"; 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)?; fs::write(temp_file.path(), template_content)?;
// Create a template builder and add variables // Create a template builder and add variables
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
// Add variables including a boolean and a vector // Add variables including a boolean and a vector
builder = builder builder = builder
.add_var("name", "Alice") .add_var("name", "Alice")
.add_var("show_greeting", true) .add_var("show_greeting", true)
.add_var("items", vec!["apple", "banana", "cherry"]); .add_var("items", vec!["apple", "banana", "cherry"]);
// Render the template // Render the template
let result = builder.render()?; let result = builder.render()?;
assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n"); assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n");
Ok(()) Ok(())
} }
#[test] #[test]
fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> { fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file // Create a temporary template file
let mut temp_file = NamedTempFile::new()?; let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?; writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?;
temp_file.flush()?; temp_file.flush()?;
// Create a HashMap of variables // Create a HashMap of variables
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("greeting", "Hi"); vars.insert("greeting", "Hi");
vars.insert("name", "Bob"); vars.insert("name", "Bob");
// Create a template builder and add variables from HashMap // Create a template builder and add variables from HashMap
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder.add_vars(vars); builder = builder.add_vars(vars);
// Render the template // Render the template
let result = builder.render()?; let result = builder.render()?;
assert_eq!(result, "Hi, Bob!\n"); assert_eq!(result, "Hi, Bob!\n");
Ok(()) Ok(())
} }
#[test] #[test]
@ -279,20 +292,19 @@ mod tests {
let temp_file = NamedTempFile::new()?; let temp_file = NamedTempFile::new()?;
let template_content = "{{ message }}\n"; let template_content = "{{ message }}\n";
fs::write(temp_file.path(), template_content)?; fs::write(temp_file.path(), template_content)?;
// Create an output file // Create an output file
let output_file = NamedTempFile::new()?; let output_file = NamedTempFile::new()?;
// Create a template builder, add a variable, and render to file // Create a template builder, add a variable, and render to file
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder.add_var("message", "This is a test"); builder = builder.add_var("message", "This is a test");
builder.render_to_file(output_file.path())?; builder.render_to_file(output_file.path())?;
// Read the output file and verify its contents // Read the output file and verify its contents
let content = fs::read_to_string(output_file.path())?; let content = fs::read_to_string(output_file.path())?;
assert_eq!(content, "This is a test\n"); assert_eq!(content, "This is a test\n");
Ok(()) Ok(())
} }
} }