From 96e78f2eefd9b9e0ad779aa5c1764394176778a9 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Sun, 13 Apr 2025 00:33:55 +0200 Subject: [PATCH] new rhai wrappers wip --- src/git/rhai/src/engine.rs | 49 ++++++++ src/git/rhai/src/generic_wrapper.rs | 132 +++++++++++++++++++++ src/git/rhai/src/mod.rs | 4 + src/git/rhai/src/module.rs | 106 +++++++++++++++++ src/git/rhai/src/wrapper.rs | 172 ++++++++++++++++++++++++++++ 5 files changed, 463 insertions(+) create mode 100644 src/git/rhai/src/engine.rs create mode 100644 src/git/rhai/src/generic_wrapper.rs create mode 100644 src/git/rhai/src/mod.rs create mode 100644 src/git/rhai/src/module.rs create mode 100644 src/git/rhai/src/wrapper.rs diff --git a/src/git/rhai/src/engine.rs b/src/git/rhai/src/engine.rs new file mode 100644 index 0000000..bde52af --- /dev/null +++ b/src/git/rhai/src/engine.rs @@ -0,0 +1,49 @@ +//! Rhai engine creation and registration for Git module +//! +//! This module provides functions to create and configure a Rhai engine with Git functionality. + +use rhai::{Engine, EvalAltResult}; +use crate::git::{GitTree, GitRepo}; +use super::wrapper::{ + create_git_tree, list_repositories, find_repositories, get_repositories, + get_repo_path, has_changes, pull_repository, reset_repository, + commit_changes, push_changes, parse_git_url_wrapper +}; + +/// Create a Rhai engine with Git functionality +pub fn create_rhai_engine() -> Engine { + let mut engine = Engine::new(); + + // Register the Git module + register_git_module(&mut engine).expect("Failed to register Git module"); + + engine +} + +/// Register Git module functions with the Rhai engine +pub fn register_git_module(engine: &mut Engine) -> Result<(), Box> { + // Register GitTree constructor + engine.register_fn("new_git_tree", create_git_tree); + + // Register GitTree methods + engine.register_fn("list_repositories", list_repositories); + engine.register_fn("find_repositories", find_repositories); + engine.register_fn("get_repositories", get_repositories); + + // Register GitRepo methods + engine.register_fn("get_repo_path", get_repo_path); + engine.register_fn("has_changes", has_changes); + engine.register_fn("pull_repository", pull_repository); + engine.register_fn("reset_repository", reset_repository); + engine.register_fn("commit_changes", commit_changes); + engine.register_fn("push_changes", push_changes); + + // Register utility functions + engine.register_fn("parse_git_url", parse_git_url_wrapper); + + // Register types + engine.register_type_with_name::("GitTree"); + engine.register_type_with_name::("GitRepo"); + + Ok(()) +} \ No newline at end of file diff --git a/src/git/rhai/src/generic_wrapper.rs b/src/git/rhai/src/generic_wrapper.rs new file mode 100644 index 0000000..02fb5df --- /dev/null +++ b/src/git/rhai/src/generic_wrapper.rs @@ -0,0 +1,132 @@ +use std::collections::HashMap; +use rhai::{Dynamic, Map, Array}; + +/// Local wrapper trait for sal::rhai::ToRhai to avoid orphan rule violations +pub trait ToRhai { + /// Convert to a Rhai Dynamic value + fn to_rhai(&self) -> Dynamic; +} + +// Implementation of ToRhai for Dynamic +impl ToRhai for Dynamic { + fn to_rhai(&self) -> Dynamic { + self.clone() + } +} + +/// Generic trait for wrapping Rust functions to be used with Rhai +pub trait RhaiWrapper { + /// Wrap a function that takes ownership of self + fn wrap_consuming(self, f: F) -> Dynamic + where + Self: Sized + Clone, + F: FnOnce(Self) -> R, + R: ToRhai; + + /// Wrap a function that takes a mutable reference to self + fn wrap_mut(&mut self, f: F) -> Dynamic + where + Self: Sized + Clone, + F: FnOnce(&mut Self) -> R, + R: ToRhai; + + /// Wrap a function that takes an immutable reference to self + fn wrap(&self, f: F) -> Dynamic + where + Self: Sized + Clone, + F: FnOnce(&Self) -> R, + R: ToRhai; +} + +/// Implementation of RhaiWrapper for any type +impl RhaiWrapper for T { + fn wrap_consuming(self, f: F) -> Dynamic + where + Self: Sized + Clone, + F: FnOnce(Self) -> R, + R: ToRhai, + { + let result = f(self); + result.to_rhai() + } + + fn wrap_mut(&mut self, f: F) -> Dynamic + where + Self: Sized + Clone, + F: FnOnce(&mut Self) -> R, + R: ToRhai, + { + let result = f(self); + result.to_rhai() + } + + fn wrap(&self, f: F) -> Dynamic + where + Self: Sized + Clone, + F: FnOnce(&Self) -> R, + R: ToRhai, + { + let result = f(self); + result.to_rhai() + } +} + +/// Convert a Rhai Map to a Rust HashMap +pub fn map_to_hashmap(map: &Map) -> HashMap { + let mut result = HashMap::new(); + for (key, value) in map.iter() { + let k = key.clone().to_string(); + let v = value.clone().to_string(); + if !k.is_empty() && !v.is_empty() { + result.insert(k, v); + } + } + result +} + +/// Convert a HashMap to a Rhai Map +pub fn hashmap_to_map(map: &HashMap) -> Map { + let mut result = Map::new(); + for (key, value) in map.iter() { + result.insert(key.clone().into(), Dynamic::from(value.clone())); + } + result +} + +/// Convert a Rhai Array to a Vec of strings +pub fn array_to_vec_string(array: &Array) -> Vec { + array.iter() + .filter_map(|item| { + let s = item.clone().to_string(); + if !s.is_empty() { Some(s) } else { None } + }) + .collect() +} + +/// Helper function to convert Dynamic to Option +pub fn dynamic_to_string_option(value: &Dynamic) -> Option { + if value.is_string() { + Some(value.clone().to_string()) + } else { + None + } +} + +/// Helper function to convert Dynamic to Option +pub fn dynamic_to_u32_option(value: &Dynamic) -> Option { + if value.is_int() { + Some(value.as_int().unwrap() as u32) + } else { + None + } +} + +/// Helper function to convert Dynamic to Option<&str> with lifetime management +pub fn dynamic_to_str_option<'a>(value: &Dynamic, storage: &'a mut String) -> Option<&'a str> { + if value.is_string() { + *storage = value.clone().to_string(); + Some(storage.as_str()) + } else { + None + } +} diff --git a/src/git/rhai/src/mod.rs b/src/git/rhai/src/mod.rs new file mode 100644 index 0000000..6e16e6c --- /dev/null +++ b/src/git/rhai/src/mod.rs @@ -0,0 +1,4 @@ +pub mod engine; +pub mod wrapper; +pub mod generic_wrapper; +pub mod module; \ No newline at end of file diff --git a/src/git/rhai/src/module.rs b/src/git/rhai/src/module.rs new file mode 100644 index 0000000..e8b34c3 --- /dev/null +++ b/src/git/rhai/src/module.rs @@ -0,0 +1,106 @@ +use rhai::{Dynamic, Engine, EvalAltResult, Module, NativeCallContext, FuncRegistration, FnNamespace, Identifier}; +use std::sync::Arc; +use crate::git::{GitTree, GitRepo}; +use super::wrapper; + +/// Creates and returns a Rhai module with Git functionality. +/// +/// This module exposes Git-related functions to Rhai scripts. +pub fn create_git_module() -> Module { + let mut module = Module::new(); + + // Register GitTree functions + FuncRegistration::new("create_git_tree") + .with_namespace(FnNamespace::Global) + .with_params_info(["base_path: &str", "GitTree"]) + .with_comments(["/// Create a new GitTree with the specified base path", + "/// - base_path: Base directory path to search for git repositories", + "/// Returns a GitTree instance"]) + .set_into_module(&mut module, wrapper::create_git_tree); + + FuncRegistration::new("list_repositories") + .with_namespace(FnNamespace::Global) + .with_params_info(["tree: &mut GitTree", "Array"]) + .with_comments(["/// List all git repositories under the base path", + "/// - tree: GitTree instance", + "/// Returns an array of GitRepo instances"]) + .set_into_module(&mut module, wrapper::list_repositories); + + FuncRegistration::new("find_repositories") + .with_namespace(FnNamespace::Global) + .with_params_info(["tree: &mut GitTree", "pattern: &str", "Array"]) + .with_comments(["/// Find repositories matching a pattern", + "/// - tree: GitTree instance", + "/// - pattern: Pattern to match repository names", + "/// Returns an array of matching GitRepo instances"]) + .set_into_module(&mut module, wrapper::find_repositories); + + FuncRegistration::new("get_repositories") + .with_namespace(FnNamespace::Global) + .with_params_info(["tree: &mut GitTree", "path_or_url: &str", "Array"]) + .with_comments(["/// Get repositories based on a path pattern or URL", + "/// - tree: GitTree instance", + "/// - path_or_url: Path pattern or URL to match", + "/// Returns an array of matching GitRepo instances"]) + .set_into_module(&mut module, wrapper::get_repositories); + + // Register GitRepo functions + FuncRegistration::new("get_repo_path") + .with_namespace(FnNamespace::Global) + .with_params_info(["repo: &mut GitRepo", "String"]) + .with_comments(["/// Get the path of the repository", + "/// - repo: GitRepo instance", + "/// Returns the path of the repository as a string"]) + .set_into_module(&mut module, wrapper::get_repo_path); + + FuncRegistration::new("has_changes") + .with_namespace(FnNamespace::Global) + .with_params_info(["repo: &mut GitRepo", "bool"]) + .with_comments(["/// Check if the repository has uncommitted changes", + "/// - repo: GitRepo instance", + "/// Returns true if there are uncommitted changes, false otherwise"]) + .set_into_module(&mut module, wrapper::has_changes); + + FuncRegistration::new("pull_repository") + .with_namespace(FnNamespace::Global) + .with_params_info(["repo: &mut GitRepo", "String"]) + .with_comments(["/// Pull the latest changes from the remote repository", + "/// - repo: GitRepo instance", + "/// Returns a success message or throws an error"]) + .set_into_module(&mut module, wrapper::pull_repository); + + FuncRegistration::new("reset_repository") + .with_namespace(FnNamespace::Global) + .with_params_info(["repo: &mut GitRepo", "String"]) + .with_comments(["/// Reset any local changes in the repository", + "/// - repo: GitRepo instance", + "/// Returns a success message or throws an error"]) + .set_into_module(&mut module, wrapper::reset_repository); + + FuncRegistration::new("commit_changes") + .with_namespace(FnNamespace::Global) + .with_params_info(["repo: &mut GitRepo", "message: &str", "String"]) + .with_comments(["/// Commit changes in the repository", + "/// - repo: GitRepo instance", + "/// - message: Commit message", + "/// Returns a success message or throws an error"]) + .set_into_module(&mut module, wrapper::commit_changes); + + FuncRegistration::new("push_changes") + .with_namespace(FnNamespace::Global) + .with_params_info(["repo: &mut GitRepo", "String"]) + .with_comments(["/// Push changes to the remote repository", + "/// - repo: GitRepo instance", + "/// Returns a success message or throws an error"]) + .set_into_module(&mut module, wrapper::push_changes); + + FuncRegistration::new("parse_git_url") + .with_namespace(FnNamespace::Global) + .with_params_info(["url: &str", "Array"]) + .with_comments(["/// Parse a git URL to extract the server, account, and repository name", + "/// - url: Git URL to parse", + "/// Returns an array with [server, account, repo]"]) + .set_into_module(&mut module, wrapper::parse_git_url_wrapper); + + module +} diff --git a/src/git/rhai/src/wrapper.rs b/src/git/rhai/src/wrapper.rs new file mode 100644 index 0000000..2a278c0 --- /dev/null +++ b/src/git/rhai/src/wrapper.rs @@ -0,0 +1,172 @@ +//! Rhai wrappers for Git module functions +//! +//! This module provides Rhai wrappers for the functions in the Git module. + +use rhai::{EvalAltResult, Array, Dynamic, Position}; +use crate::git::{GitTree, GitRepo, GitError, parse_git_url}; +use super::generic_wrapper::{RhaiWrapper, ToRhai}; + +// Helper function to convert GitError to EvalAltResult +fn git_error_to_rhai_error(result: Result) -> Result> { + result.map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Git error: {}", e).into(), + Position::NONE + )) + }) +} + +// +// GitTree Functions +// + +/// Create a new GitTree with the specified base path +pub fn create_git_tree(base_path: &str) -> Result> { + git_error_to_rhai_error(GitTree::new(base_path)) +} + +/// List all git repositories under the base path +pub fn list_repositories(tree: &mut GitTree) -> Result> { + git_error_to_rhai_error(tree.list()).map(|repos| { + let array: Array = repos.into_iter() + .map(|repo| Dynamic::from(repo)) + .collect(); + array + }) +} + +/// Find repositories matching a pattern +pub fn find_repositories(tree: &mut GitTree, pattern: &str) -> Result> { + git_error_to_rhai_error(tree.find(pattern)).map(|repos| { + let array: Array = repos.into_iter() + .map(|repo| Dynamic::from(repo)) + .collect(); + array + }) +} + +/// Get repositories based on a path pattern or URL +pub fn get_repositories(tree: &mut GitTree, path_or_url: &str) -> Result> { + git_error_to_rhai_error(tree.get(path_or_url)).map(|repos| { + let array: Array = repos.into_iter() + .map(Dynamic::from) + .collect(); + array + }) +} + +// +// GitRepo Functions +// + +/// Get the path of the repository +pub fn get_repo_path(repo: &mut GitRepo) -> String { + repo.path().to_string() +} + +/// Check if the repository has uncommitted changes +pub fn has_changes(repo: &mut GitRepo) -> Result> { + git_error_to_rhai_error(repo.has_changes()) +} + +/// Pull the latest changes from the remote repository +pub fn pull_repository(repo: &mut GitRepo) -> Result> { + match repo.pull() { + Ok(_) => Ok(String::from("Successfully pulled changes")), + Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Error pulling changes: {}", err).into(), + Position::NONE + ))) + } +} + +/// Reset any local changes in the repository +pub fn reset_repository(repo: &mut GitRepo) -> Result> { + match repo.reset() { + Ok(_) => Ok(String::from("Successfully reset repository")), + Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Error resetting repository: {}", err).into(), + Position::NONE + ))) + } +} + +/// Commit changes in the repository +pub fn commit_changes(repo: &mut GitRepo, message: &str) -> Result> { + match repo.commit(message) { + Ok(_) => Ok(String::from("Successfully committed changes")), + Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Error committing changes: {}", err).into(), + Position::NONE + ))) + } +} + +/// Push changes to the remote repository +pub fn push_changes(repo: &mut GitRepo) -> Result> { + match repo.push() { + Ok(_) => Ok(String::from("Successfully pushed changes")), + Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Error pushing changes: {}", err).into(), + Position::NONE + ))) + } +} + +/// Parse a git URL to extract the server, account, and repository name +pub fn parse_git_url_wrapper(url: &str) -> Array { + let (server, account, repo) = parse_git_url(url); + let mut array = Array::new(); + array.push(Dynamic::from(server)); + array.push(Dynamic::from(account)); + array.push(Dynamic::from(repo)); + array +} + +// Implementation of ToRhai for GitTree +impl ToRhai for GitTree { + fn to_rhai(&self) -> Dynamic { + Dynamic::from(self.clone()) + } +} + +// Implementation of ToRhai for GitRepo +impl ToRhai for GitRepo { + fn to_rhai(&self) -> Dynamic { + Dynamic::from(self.clone()) + } +} + +// Implementation of ToRhai for String +impl ToRhai for String { + fn to_rhai(&self) -> Dynamic { + Dynamic::from(self.clone()) + } +} + +// Implementation of ToRhai for bool +impl ToRhai for bool { + fn to_rhai(&self) -> Dynamic { + Dynamic::from(*self) + } +} + +// Implementation of ToRhai for Vec +impl ToRhai for Vec { + fn to_rhai(&self) -> Dynamic { + let array: Array = self.iter() + .map(|s| Dynamic::from(s.clone())) + .collect(); + Dynamic::from(array) + } +} + +// Implementation of ToRhai for Option +impl ToRhai for Option { + fn to_rhai(&self) -> Dynamic { + match self { + Some(value) => value.to_rhai(), + None => Dynamic::UNIT + } + } +} \ No newline at end of file