more efforts to automate rhai bindings

This commit is contained in:
timurgordon
2025-05-13 02:00:35 +03:00
parent 16ad4f5743
commit ec4769a6b0
14 changed files with 3174 additions and 52 deletions

View File

@@ -0,0 +1,176 @@
use rhai::Engine;
use rhai_wrapper::rust_rhai_wrapper;
use std::error::Error;
use std::fmt;
// Define a custom error type for Result examples
#[derive(Debug, Clone)]
struct MyError(String);
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for MyError {}
impl From<String> for MyError {
fn from(s: String) -> Self {
MyError(s)
}
}
fn main() {
// Create a Rhai engine
let mut engine = Engine::new();
// 1. Basic example: Add two numbers
// Define the original Rust function
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Register it with Rhai
engine.register_fn("add_rhai", add);
// Create a wrapper function that calls Rhai which calls the Rust function
rust_rhai_wrapper!(add_via_rhai, "add_rhai", (i32, i32) -> i32);
// Test the full circle
let result = add_via_rhai(&mut engine, 5, 3);
println!("add_via_rhai(5, 3) = {}", result);
// 2. String manipulation example
fn concat(s1: String, s2: String) -> String {
format!("{} {}", s1, s2)
}
engine.register_fn("concat_rhai", concat);
rust_rhai_wrapper!(concat_via_rhai, "concat_rhai", (String, String) -> String);
let result = concat_via_rhai(&mut engine, "Hello".to_string(), "World".to_string());
println!("concat_via_rhai(\"Hello\", \"World\") = {}", result);
// 3. Function with no arguments
fn get_random() -> i32 {
42 // Not so random, but it's just an example
}
engine.register_fn("get_random_rhai", get_random);
rust_rhai_wrapper!(get_random_via_rhai, "get_random_rhai", () -> i32);
let result = get_random_via_rhai(&mut engine);
println!("get_random_via_rhai() = {}", result);
// 4. Function with more arguments
fn calculate(a: i32, b: i32, c: i32, d: i32) -> i32 {
a + b * c - d
}
engine.register_fn("calculate_rhai", calculate);
rust_rhai_wrapper!(calculate_via_rhai, "calculate_rhai", (i32, i32, i32, i32) -> i32);
let result = calculate_via_rhai(&mut engine, 5, 3, 2, 1);
println!("calculate_via_rhai(5, 3, 2, 1) = {}", result);
// 5. Function that handles errors with a custom return type
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
// Register a safe division function that returns an array with success flag and result/error
engine.register_fn("safe_divide", |a: i32, b: i32| -> rhai::Array {
if b == 0 {
// Return [false, error message]
let mut arr = rhai::Array::new();
arr.push(rhai::Dynamic::from(false));
arr.push(rhai::Dynamic::from("Division by zero"));
arr
} else {
// Return [true, result]
let mut arr = rhai::Array::new();
arr.push(rhai::Dynamic::from(true));
arr.push(rhai::Dynamic::from(a / b));
arr
}
});
// Create a wrapper for the safe_divide function
rust_rhai_wrapper!(safe_divide_via_rhai, "safe_divide", (i32, i32) -> rhai::Array);
// Test success case
let result = safe_divide_via_rhai(&mut engine, 10, 2);
println!("safe_divide_via_rhai(10, 2) = {:?}", result);
// Test error case
let result = safe_divide_via_rhai(&mut engine, 10, 0);
println!("safe_divide_via_rhai(10, 0) = {:?}", result);
// Process the result
let success = result[0].as_bool().unwrap();
if success {
println!("Division result: {}", result[1].as_int().unwrap());
} else {
println!("Division error: {}", result[1].clone().into_string().unwrap());
}
// 6. Complex example: Using a custom type with Rhai
#[derive(Debug, Clone)]
struct Person {
name: String,
age: i32,
}
// Register type and methods with Rhai
engine.register_type::<Person>();
engine.register_fn("new_person", |name: String, age: i32| -> Person {
Person { name, age }
});
engine.register_fn("get_name", |p: &mut Person| -> String {
p.name.clone()
});
engine.register_fn("get_age", |p: &mut Person| -> i32 {
p.age
});
engine.register_fn("is_adult", |p: &mut Person| -> bool {
p.age >= 18
});
// Register a function that creates a person and checks if they're an adult
engine.register_fn("create_and_check_person", |name: String, age: i32| -> rhai::Array {
let person = Person { name, age };
let is_adult = person.age >= 18;
// Create an array with the person and the is_adult flag
let mut arr = rhai::Array::new();
// Convert the person to a map for Rhai
let mut map = rhai::Map::new();
map.insert("name".into(), rhai::Dynamic::from(person.name));
map.insert("age".into(), rhai::Dynamic::from(person.age));
arr.push(rhai::Dynamic::from(map));
arr.push(rhai::Dynamic::from(is_adult));
arr
});
// Create a wrapper for the Rhai function
rust_rhai_wrapper!(create_person_via_rhai, "create_and_check_person", (String, i32) -> rhai::Array);
// Test the wrapper
let result = create_person_via_rhai(&mut engine, "Alice".to_string(), 25);
println!("create_person_via_rhai(\"Alice\", 25) = {:?}", result);
// Extract data from the Rhai array
let person_map = result[0].clone().try_cast::<rhai::Map>().expect("Expected a Map");
let is_adult = result[1].clone().as_bool().expect("Expected a boolean");
println!("Person: {:?}, Is Adult: {}", person_map, is_adult);
println!("All examples completed successfully!");
}

View File

@@ -0,0 +1,312 @@
use rhai::{Engine, INT, CustomType, TypeBuilder};
use rhai_wrapper::{wrap_option_return, wrap_vec_return};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::path::Path;
use chrono;
// --- Mock heromodels_core ---
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct BaseModelData {
pub id: u32,
pub created_at: i64, // Using i64 for timestamp simplicity
pub updated_at: i64,
pub comment_ids: Vec<u32>,
}
impl BaseModelData {
pub fn new(id: u32) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
id,
created_at: now,
updated_at: now,
comment_ids: Vec::new(),
}
}
// No &mut self for getters if struct is Clone and passed by value in Rhai, or if Rhai handles it.
// For CustomType, Rhai typically passes &mut T to getters/setters.
pub fn get_id(&mut self) -> u32 { self.id }
pub fn get_created_at(&mut self) -> i64 { self.created_at }
pub fn get_updated_at(&mut self) -> i64 { self.updated_at }
pub fn get_comment_ids(&mut self) -> Vec<u32> { self.comment_ids.clone() }
pub fn add_comment_internal(&mut self, comment_id: u32) { // Renamed to avoid clash if also exposed
self.comment_ids.push(comment_id);
self.updated_at = chrono::Utc::now().timestamp();
}
}
// --- User Struct and Methods (Adapted for Rhai) ---
/// Represents a user in the system
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct User {
/// Base model data
pub base_data: BaseModelData,
/// User's username
pub username: String,
/// User's email address
pub email: String,
/// User's full name
pub full_name: String,
/// Whether the user is active
pub is_active: bool,
}
impl User {
// This is the "builder" entry point
pub fn user_builder(id: INT) -> Self {
Self {
base_data: BaseModelData::new(id as u32),
username: String::new(),
email: String::new(),
full_name: String::new(),
is_active: true, // Default, can be changed by .is_active(false)
}
}
// Fluent setters returning Self
pub fn username(mut self, username: String) -> Self {
self.username = username;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
pub fn email(mut self, email: String) -> Self {
self.email = email;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
pub fn full_name(mut self, full_name: String) -> Self {
self.full_name = full_name;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
// Naming this 'set_is_active' to distinguish from potential getter 'is_active'
// or the script can use direct field access if setter is also registered for 'is_active'
// For fluent chain .is_active(bool_val)
pub fn is_active(mut self, active_status: bool) -> Self {
self.is_active = active_status;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
// Method to add a comment, distinct from direct field manipulation
pub fn add_comment(mut self, comment_id: INT) -> Self {
self.base_data.add_comment_internal(comment_id as u32);
self
}
// Explicit activate/deactivate methods returning Self for chaining if needed
pub fn activate(mut self) -> Self {
self.is_active = true;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
pub fn deactivate(mut self) -> Self {
self.is_active = false;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
// Getters for direct field access from Rhai: register with .register_get()
// Rhai passes &mut User to these
pub fn get_id_rhai(&mut self) -> INT { self.base_data.id as INT }
pub fn get_username_rhai(&mut self) -> String { self.username.clone() }
pub fn get_email_rhai(&mut self) -> String { self.email.clone() }
pub fn get_full_name_rhai(&mut self) -> String { self.full_name.clone() }
pub fn get_is_active_rhai(&mut self) -> bool { self.is_active }
pub fn get_comment_ids_rhai(&mut self) -> Vec<u32> { self.base_data.comment_ids.clone() }
}
// --- Comment Struct and Methods (Adapted for Rhai) ---
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct Comment {
pub id: INT,
pub user_id: INT, // Assuming comments are linked to users by ID
pub content: String,
}
impl Comment {
pub fn comment_builder(id: INT) -> Self {
Self {
id,
user_id: 0, // Default
content: String::new(),
}
}
// Fluent setters
pub fn user_id(mut self, user_id: INT) -> Self {
self.user_id = user_id;
self
}
pub fn content(mut self, content: String) -> Self {
self.content = content;
self
}
// Getters for Rhai
pub fn get_id_rhai(&mut self) -> INT { self.id }
pub fn get_user_id_rhai(&mut self) -> INT { self.user_id }
pub fn get_content_rhai(&mut self) -> String { self.content.clone() }
}
// --- Mock Database ---
#[derive(Debug, Clone, Default)]
struct DbState {
users: HashMap<INT, User>,
comments: HashMap<INT, Comment>,
}
type OurDB = Arc<Mutex<DbState>>;
fn set_user(db_arc: OurDB, user: User) {
let mut db = db_arc.lock().unwrap();
db.users.insert(user.base_data.id as INT, user);
}
fn get_user_by_id(db_arc: OurDB, id: INT) -> Option<User> {
let db = db_arc.lock().unwrap();
db.users.get(&id).cloned()
}
fn get_all_users(db_arc: OurDB) -> Vec<User> {
let db = db_arc.lock().unwrap();
db.users.values().cloned().collect()
}
fn delete_user_by_id(db_arc: OurDB, id: INT) {
let mut db = db_arc.lock().unwrap();
db.users.remove(&id);
}
fn set_comment(db_arc: OurDB, comment: Comment) {
let mut db = db_arc.lock().unwrap();
db.comments.insert(comment.id, comment);
}
fn get_comment_by_id(db_arc: OurDB, id: INT) -> Option<Comment> {
let db = db_arc.lock().unwrap();
db.comments.get(&id).cloned()
}
fn get_users_by_activity_status_optional(db_arc: OurDB, is_active_filter: bool) -> Option<Vec<User>> {
let db = db_arc.lock().unwrap();
let users: Vec<User> = db.users.values()
.filter(|u| u.is_active == is_active_filter)
.cloned()
.collect();
if users.is_empty() {
None
} else {
Some(users)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut engine = Engine::new();
let db_instance: OurDB = Arc::new(Mutex::new(DbState::default()));
// Register User type and its methods/getters/setters
engine
.register_type_with_name::<User>("User")
// Fluent methods - these are registered as functions that take User and return User
.register_fn("user_builder", User::user_builder)
.register_fn("username", User::username)
.register_fn("email", User::email)
.register_fn("full_name", User::full_name)
.register_fn("is_active", User::is_active) // This is the fluent setter
.register_fn("add_comment", User::add_comment)
.register_fn("activate", User::activate)
.register_fn("deactivate", User::deactivate)
// Getters for direct field-like access: user.id, user.username etc.
.register_get("id", User::get_id_rhai)
.register_get("username", User::get_username_rhai)
.register_get("email", User::get_email_rhai)
.register_get("full_name", User::get_full_name_rhai)
.register_get("is_active", User::get_is_active_rhai) // This is the getter for direct field access
.register_get("comment_ids", User::get_comment_ids_rhai);
// Register Comment type and its methods/getters/setters
engine
.register_type_with_name::<Comment>("Comment")
.register_fn("comment_builder", Comment::comment_builder)
.register_fn("user_id", Comment::user_id)
.register_fn("content", Comment::content)
.register_get("id", Comment::get_id_rhai)
.register_get("user_id", Comment::get_user_id_rhai)
.register_get("content", Comment::get_content_rhai);
// DB functions - now directly registered
let db_clone_for_get_db = db_instance.clone();
engine.register_fn("get_db", move || db_clone_for_get_db.clone());
let db_clone_for_set_user = db_instance.clone();
engine.register_fn("set_user", move |user: User| set_user(db_clone_for_set_user.clone(), user));
let db_clone_for_get_user_by_id = db_instance.clone();
engine.register_fn("get_user_by_id", move |id: INT| {
wrap_option_return!(get_user_by_id, OurDB, INT => User)(db_clone_for_get_user_by_id.clone(), id)
});
let db_clone_for_get_all_users = db_instance.clone();
engine.register_fn(
"get_all_users",
move || {
(wrap_vec_return!(get_all_users, OurDB => User))(db_clone_for_get_all_users.clone())
}
);
let db_clone_for_delete_user = db_instance.clone();
engine.register_fn("delete_user_by_id", move |id: INT| delete_user_by_id(db_clone_for_delete_user.clone(), id));
let db_clone_for_set_comment = db_instance.clone();
engine.register_fn("set_comment", move |comment: Comment| set_comment(db_clone_for_set_comment.clone(), comment));
let db_clone_for_get_comment_by_id = db_instance.clone();
engine.register_fn("get_comment_by_id", move |id: INT| {
wrap_option_return!(get_comment_by_id, OurDB, INT => Comment)(db_clone_for_get_comment_by_id.clone(), id)
});
let db_clone_for_optional_vec = db_instance.clone();
engine.register_fn(
"get_users_by_activity_status_optional",
move |is_active_filter: bool| {
(wrap_option_vec_return!(get_users_by_activity_status_optional, OurDB, bool => User))(
db_clone_for_optional_vec.clone(),
is_active_filter
)
}
);
engine.register_fn("println", |s: &str| println!("{}", s));
engine.register_fn("print", |s: &str| print!("{}", s));
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("user_script.rhai");
println!("Loading Rhai script from: {}", script_path.display());
match engine.eval_file::<()>(script_path) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Error executing script: {}\nAt: {:?}", e, e.position()),
}
Ok(())
}

View File

@@ -0,0 +1,175 @@
// Hero Models - Rhai Example Script
println("Hero Models - Rhai Usage Example");
println("================================");
// Get the DB instance
let db = get_db();
// Create a new user using the builder pattern
println("Creating users...");
// Create user 1
let user1 = user_builder(1)
.username("johndoe")
.email("john.doe@example.com")
.full_name("John Doe")
.is_active(false);
set_user(user1);
// Create user 2
let user2 = user_builder(2)
.username("janesmith")
.email("jane.smith@example.com")
.full_name("Jane Smith")
.is_active(true);
set_user(user2);
// Create user 3
let user3 = user_builder(3)
.username("willism")
.email("willis.masters@example.com")
.full_name("Willis Masters")
.is_active(true);
set_user(user3);
// Create user 4
let user4 = user_builder(4)
.username("carrols")
.email("carrol.smith@example.com")
.full_name("Carrol Smith")
.is_active(false);
set_user(user4);
// Get user by ID
println("\nRetrieving user by ID...");
let retrieved_user = get_user_by_id(1);
if retrieved_user != () { // In Rhai, functions returning Option<T> yield () for None
println("Found user: " + retrieved_user.full_name + " (ID: " + retrieved_user.id + ")");
println("Username: " + retrieved_user.username);
println("Email: " + retrieved_user.email);
println("Active: " + retrieved_user.is_active);
} else {
println("User not found");
}
// Get users by active status
println("\nRetrieving active users...");
let all_users_for_active_check = get_all_users();
let active_users = [];
// Filter active users
for user_item in all_users_for_active_check {
if user_item.is_active == true {
active_users.push(user_item);
}
}
println("Found " + active_users.len() + " active users:");
for user_item in active_users {
println("- " + user_item.full_name + " (ID: " + user_item.id + ")");
}
// Delete a user
println("\nDeleting user...");
delete_user_by_id(2);
// Get active users again
println("\nRetrieving active users after deletion...");
let all_users_after_delete = get_all_users();
let active_users_after_delete = [];
// Filter active users
for user_item in all_users_after_delete {
if user_item.is_active == true {
active_users_after_delete.push(user_item);
}
}
println("Found " + active_users_after_delete.len() + " active users:");
for user_item in active_users_after_delete {
println("- " + user_item.full_name + " (ID: " + user_item.id + ")");
}
// Get inactive users
println("\nRetrieving inactive users...");
let all_users_for_inactive_check = get_all_users();
let inactive_users = [];
// Filter inactive users
for user_item in all_users_for_inactive_check {
if user_item.is_active == false {
inactive_users.push(user_item);
}
}
println("Found " + inactive_users.len() + " inactive users:");
for user_item in inactive_users {
println("- " + user_item.full_name + " (ID: " + user_item.id + ")");
}
// Create a comment for user 1
println("\nCreating a comment...");
let comment1 = comment_builder(5)
.user_id(1)
.content("This is a comment on the user");
set_comment(comment1);
// Get the comment
println("\nRetrieving comment...");
let retrieved_comment = get_comment_by_id(5);
if retrieved_comment != () { // In Rhai, functions returning Option<T> yield () for None
println("Found comment: " + retrieved_comment.content);
println("Comment ID: " + retrieved_comment.id);
println("User ID: " + retrieved_comment.user_id);
} else {
println("Comment not found");
}
println("\nRetrieving optional active users (should be Some(Array) or Unit)...");
let optional_active_users = get_users_by_activity_status_optional(true);
if optional_active_users == () { // Check for unit (None)
println("No active users found (returned unit).");
} else {
println("Found optional active users:");
for user in optional_active_users {
println("- " + user.full_name + " (ID: " + user.id + ")");
}
}
println("\nRetrieving optional inactive users (should be Some(Array) or Unit)...");
let optional_inactive_users = get_users_by_activity_status_optional(false);
if optional_inactive_users == () { // Check for unit (None)
println("No inactive users found (returned unit).");
} else {
println("Found optional inactive users:");
for user in optional_inactive_users {
println("- " + user.full_name + " (ID: " + user.id + ")");
}
}
// To specifically test the None case from our Rust logic (empty vec for a status returns None)
println("\nTesting None case for optional vector retrieval...");
println("Deleting all users to ensure empty states...");
let all_users_before_delete_all = get_all_users();
for user_to_delete in all_users_before_delete_all {
delete_user_by_id(user_to_delete.id);
}
let optional_active_after_delete_all = get_users_by_activity_status_optional(true);
if optional_active_after_delete_all == () {
println("Correctly received unit for active users after deleting all.");
} else {
println("ERROR: Expected unit for active users after deleting all, but got an array.");
print("Value received: " + optional_active_after_delete_all);
}
let optional_inactive_after_delete_all = get_users_by_activity_status_optional(false);
if optional_inactive_after_delete_all == () {
println("Correctly received unit for inactive users after deleting all.");
} else {
println("ERROR: Expected unit for inactive users after deleting all, but got an array.");
print("Value received: " + optional_inactive_after_delete_all);
}
println("\nRhai example completed successfully!");