feat: Add Rhai scripting support
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
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:
@@ -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;
|
||||
|
@@ -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)
|
||||
}
|
@@ -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(())
|
||||
}
|
177
src/rhai/mod.rs
177
src/rhai/mod.rs
@@ -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(())
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user