feat: Add Rhai scripting support
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run

- Add new `sal-rhai` crate for Rhai scripting integration
- Integrate Rhai with existing SAL modules
- Improve error handling for Rhai scripts and SAL functions
- Add comprehensive unit and integration tests for `sal-rhai`
This commit is contained in:
Mahmoud-Emad
2025-06-23 16:23:51 +03:00
parent 6dead402a2
commit 8012a66250
19 changed files with 2109 additions and 38 deletions

View File

@@ -43,7 +43,7 @@ pub use sal_os as os;
pub use sal_postgresclient as postgresclient;
pub use sal_process as process;
pub use sal_redisclient as redisclient;
pub mod rhai;
pub use sal_rhai as rhai;
pub use sal_text as text;
pub use sal_vault as vault;
pub use sal_virt as virt;

View File

@@ -1,53 +0,0 @@
//! Rhai wrappers for core engine functions
//!
//! This module provides Rhai wrappers for functions that interact with the Rhai engine itself.
use super::error::ToRhaiError;
use rhai::{Engine, EvalAltResult, NativeCallContext};
use sal_os as os;
/// Register core module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_core_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
engine.register_fn("exec", exec);
Ok(())
}
/// Execute a Rhai script from a URL, file, or string
///
/// # Arguments
///
/// * `context` - The native call context, used to access the Rhai engine
/// * `source` - The source of the script to execute. Can be a URL, a file path, or a string of code.
///
/// # Returns
///
/// * `Result<rhai::Dynamic, Box<EvalAltResult>>` - The result of the script execution
pub fn exec(context: NativeCallContext, source: &str) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
let content = if source.starts_with("http://") || source.starts_with("https://") {
// If the source is a URL, download it to a temporary file
let temp_dir = std::env::temp_dir();
let file_name = source.split('/').last().unwrap_or("script.rhai");
let dest_path = temp_dir.join(format!("{}-{}", uuid::Uuid::new_v4(), file_name));
let dest_str = dest_path.to_str().unwrap();
os::download_file(source, dest_str, 0).to_rhai_error()?;
os::file_read(dest_str).to_rhai_error()?
} else if os::exist(source) {
// If the source is an existing file, read it
os::file_read(source).to_rhai_error()?
} else {
// Otherwise, treat the source as the script content itself
source.to_string()
};
// Execute the script content
context.engine().eval(&content)
}

View File

@@ -1,62 +0,0 @@
use rhai::{Engine, EvalAltResult, Position};
use thiserror::Error;
#[derive(Debug, Error, Clone)]
pub enum SalError {
#[error("File system error: {0}")]
FsError(String),
#[error("Download error: {0}")]
DownloadError(String),
#[error("Package error: {0}")]
PackageError(String),
#[error("{0}: {1}")]
Generic(String, String),
}
impl SalError {
pub fn new(kind: &str, message: &str) -> Self {
SalError::Generic(kind.to_string(), message.to_string())
}
}
impl From<SalError> for Box<EvalAltResult> {
fn from(err: SalError) -> Self {
let err_msg = err.to_string();
Box::new(EvalAltResult::ErrorRuntime(
err_msg.into(),
Position::NONE,
))
}
}
/// A trait for converting a Result to a Rhai-compatible error
pub trait ToRhaiError<T> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>>;
}
impl<T, E: std::error::Error> ToRhaiError<T> for Result<T, E> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
self.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.to_string().into(),
Position::NONE,
))
})
}
}
/// Register all the SalError variants with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the error types with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_error_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
engine.register_type_with_name::<SalError>("SalError")
.register_fn("to_string", |err: &mut SalError| err.to_string());
Ok(())
}

View File

@@ -1,177 +0,0 @@
//! Rhai scripting integration for the SAL library
//!
//! This module provides integration with the Rhai scripting language,
//! allowing SAL functions to be called from Rhai scripts.
mod core;
pub mod error;
// OS module is now provided by sal-os package
// Platform module is now provided by sal-os package
// PostgreSQL module is now provided by sal-postgresclient package
// Virt modules (buildah, nerdctl, rfs) are now provided by sal-virt package
// vault module is now provided by sal-vault package
// zinit module is now in sal-zinit-client package
#[cfg(test)]
mod tests;
// Re-export common Rhai types for convenience
pub use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
// Re-export error module
pub use error::*;
// Re-export specific functions from sal-os package
pub use sal_os::rhai::{
delete,
// Download functions
download,
download_install,
// File system functions
exist,
file_size,
find_dir,
find_dirs,
find_file,
find_files,
mkdir,
register_os_module,
rsync,
};
// Re-export Redis client module registration function
pub use sal_redisclient::rhai::register_redisclient_module;
// Re-export PostgreSQL client module registration function
pub use sal_postgresclient::rhai::register_postgresclient_module;
pub use sal_process::rhai::{
kill,
process_get,
process_list,
register_process_module,
// Run functions
// Process management functions
which,
};
// Re-export virt functions from sal-virt package
pub use sal_virt::rhai::nerdctl::{
nerdctl_copy,
nerdctl_exec,
nerdctl_image_build,
nerdctl_image_commit,
nerdctl_image_pull,
nerdctl_image_push,
nerdctl_image_remove,
nerdctl_image_tag,
// Image functions
nerdctl_images,
nerdctl_list,
nerdctl_remove,
// Container functions
nerdctl_run,
nerdctl_run_with_name,
nerdctl_run_with_port,
nerdctl_stop,
};
pub use sal_virt::rhai::{
bah_new, register_bah_module, register_nerdctl_module, register_rfs_module,
};
// Re-export git module from sal-git package
pub use sal_git::rhai::register_git_module;
pub use sal_git::{GitRepo, GitTree};
// Re-export zinit module from sal-zinit-client package
pub use sal_zinit_client::rhai::register_zinit_module;
// Re-export mycelium module
pub use sal_mycelium::rhai::register_mycelium_module;
// Re-export text module
pub use sal_text::rhai::register_text_module;
// Re-export net module
pub use sal_net::rhai::register_net_module;
// Re-export crypto module
pub use sal_vault::rhai::register_crypto_module;
// Rename copy functions to avoid conflicts
pub use sal_os::rhai::copy as os_copy;
/// Register all SAL modules with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the modules with
///
/// # Example
///
/// ```ignore
/// use rhai::Engine;
/// use sal::rhai;
///
/// let mut engine = Engine::new();
/// rhai::register(&mut engine);
///
/// // Now you can use SAL functions in Rhai scripts
/// // 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 Core module functions
core::register_core_module(engine)?;
// Register OS module functions
sal_os::rhai::register_os_module(engine)?;
// Register Process module functions
sal_process::rhai::register_process_module(engine)?;
// Register Virt module functions (Buildah, Nerdctl, RFS)
sal_virt::rhai::register_virt_module(engine)?;
// Register Git module functions
sal_git::rhai::register_git_module(engine)?;
// Register Zinit module functions
sal_zinit_client::rhai::register_zinit_module(engine)?;
// Register Mycelium module functions
sal_mycelium::rhai::register_mycelium_module(engine)?;
// Register Text module functions
sal_text::rhai::register_text_module(engine)?;
// Register Net module functions
sal_net::rhai::register_net_module(engine)?;
// RFS module functions are now registered as part of sal_virt above
// Register Crypto module functions
register_crypto_module(engine)?;
// Register Redis client module functions
sal_redisclient::rhai::register_redisclient_module(engine)?;
// Register PostgreSQL client module functions
sal_postgresclient::rhai::register_postgresclient_module(engine)?;
// Platform functions are now registered by sal-os package
// Screen module functions are now part of sal-process package
// Register utility functions
engine.register_fn("is_def_fn", |_name: &str| -> bool {
// This is a utility function to check if a function is defined in the engine
// For testing purposes, we'll just return true
true
});
// Future modules can be registered here
Ok(())
}

View File

@@ -1,212 +0,0 @@
//! Tests for Rhai integration
//!
//! This module contains tests for the Rhai integration.
#[cfg(test)]
mod tests {
use super::super::register;
use rhai::Engine;
use std::fs;
use std::path::Path;
#[test]
fn test_register() {
let mut engine = Engine::new();
assert!(register(&mut engine).is_ok());
}
// OS Module Tests
#[test]
fn test_exist_function() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test with a file that definitely exists
let result = engine.eval::<bool>(r#"exist("Cargo.toml")"#).unwrap();
assert!(result);
// Test with a file that definitely doesn't exist
let result = engine
.eval::<bool>(r#"exist("non_existent_file.xyz")"#)
.unwrap();
assert!(!result);
}
#[test]
fn test_mkdir_and_delete() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
let test_dir = "test_rhai_dir";
// Clean up from previous test runs if necessary
if Path::new(test_dir).exists() {
fs::remove_dir_all(test_dir).unwrap();
}
// Create directory using Rhai
let script = format!(r#"mkdir("{}")"#, test_dir);
let result = engine.eval::<String>(&script).unwrap();
assert!(result.contains("Successfully created directory"));
assert!(Path::new(test_dir).exists());
// Delete directory using Rhai
let script = format!(r#"delete("{}")"#, test_dir);
let result = engine.eval::<String>(&script).unwrap();
assert!(result.contains("Successfully deleted directory"));
assert!(!Path::new(test_dir).exists());
}
#[test]
fn test_file_size() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Create a test file
let test_file = "test_rhai_file.txt";
let test_content = "Hello, Rhai!";
fs::write(test_file, test_content).unwrap();
// Get file size using Rhai
let script = format!(r#"file_size("{}")"#, test_file);
let size = engine.eval::<i64>(&script).unwrap();
assert_eq!(size, test_content.len() as i64);
// Clean up
fs::remove_file(test_file).unwrap();
}
#[test]
fn test_error_handling() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Try to get the size of a non-existent file
let result = engine.eval::<i64>(r#"file_size("non_existent_file.xyz")"#);
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
println!("Error string: {}", err_str);
// The actual error message is "No files found matching..."
assert!(
err_str.contains("No files found matching")
|| err_str.contains("File not found")
|| err_str.contains("File system error")
);
}
// Process Module Tests
#[test]
fn test_which_function() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test with a command that definitely exists (like "ls" on Unix or "cmd" on Windows)
#[cfg(target_os = "windows")]
let cmd = "cmd";
#[cfg(any(target_os = "macos", target_os = "linux"))]
let cmd = "ls";
let script = format!(r#"which("{}")"#, cmd);
let result = engine.eval::<String>(&script).unwrap();
assert!(!result.is_empty());
// Test with a command that definitely doesn't exist
let script = r#"which("non_existent_command_xyz123")"#;
let result = engine.eval::<()>(&script).unwrap();
assert_eq!(result, ());
}
#[test]
fn test_run_with_options() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test running a command with custom options
#[cfg(target_os = "windows")]
let script = r#"
let options = new_run_options();
options["die"] = true;
options["silent"] = false;
options["log"] = true;
let result = run("echo Hello World", options);
result.success && result.stdout.contains("Hello World")
"#;
#[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#"
let options = new_run_options();
options["die"] = true;
options["silent"] = false;
options["log"] = true;
let result = run("echo 'Hello World'", options);
result.success && result.stdout.contains("Hello World")
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
#[test]
fn test_run_command() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test a simple echo command
#[cfg(target_os = "windows")]
let script = r#"
let result = run_command("echo Hello World");
result.success && result.stdout.contains("Hello World")
"#;
#[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#"
let result = run_command("echo 'Hello World'");
result.success && result.stdout.contains("Hello World")
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
#[test]
fn test_run_silent() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test a simple echo command with silent execution
#[cfg(target_os = "windows")]
let script = r#"
let result = run_silent("echo Hello World");
result.success && result.stdout.contains("Hello World")
"#;
#[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#"
let result = run_silent("echo 'Hello World'");
result.success && result.stdout.contains("Hello World")
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
#[test]
fn test_process_list() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test listing processes (should return a non-empty array)
let script = r#"
let processes = process_list("");
processes.len() > 0
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
}