7.1 KiB
7.1 KiB
AI Prompt: Generate Rhai Module Integration for Rust Models
Context
You are tasked with creating a Rhai scripting integration for Rust models in the heromodels crate. The integration should follow the builder pattern and provide a comprehensive set of functions for interacting with the models through Rhai scripts.
Key Concepts to Understand
- Builder Pattern vs. Rhai Mutation: Rust models use builder pattern (methods consume self and return Self), but Rhai requires functions that mutate in place (&mut self).
- Database Access: Database functions are exposed as global functions in a 'db' module namespace (e.g.,
db::save_flow
, notdb.save_flow
). - ID Handling: New objects should use ID 0 to allow database assignment of real IDs.
- Function vs. Method Calls: Rhai scripts must use function calls (e.g.,
status(flow, STATUS_DRAFT)
) not method chaining (e.g.,flow.status(STATUS_DRAFT)
).
Requirements
-
Module Structure and Imports:
- Create a
rhai.rs
file that exports a function namedregister_[model]_rhai_module
which takes an Engine and an Arc - IMPORTANT: Import
heromodels_core::Model
trait for access to methods likeget_id
- Use the
#[export_module]
macro to define the Rhai module - Register the module globally with the engine using the correct module registration syntax
- Create separate database functions that capture the database reference
- Create a
-
Builder Pattern Integration:
- For each model struct marked with
#[model]
, create constructor functions (e.g.,new_[model]
) that accept ID 0 for new objects - For each builder method in the model, create a corresponding Rhai function that:
- Takes a mutable reference to the model as first parameter
- Uses
std::mem::take
to get ownership (if the model implements Default) orstd::mem::replace
(if it doesn't) - Calls the builder method on the owned object
- Assigns the result back to the mutable reference
- Returns a clone of the modified object
- For each model struct marked with
-
Field Access:
- IMPORTANT: Do NOT create getter methods for fields that can be accessed directly
- Use direct field access in example code (e.g.,
flow.name
, notflow.get_name()
) - Only create getter functions for computed properties or when special conversion is needed
- Handle special types appropriately (e.g., convert timestamps, IDs, etc.)
-
Database Functions:
- For each model, implement the following database functions in the
db
module namespace:db::save_[model]
: Save the model to the database and return the saved model with assigned IDdb::get_[model]_by_id
: Retrieve a model by IDdb::delete_[model]
: Delete a model from the databasedb::list_[model]s
: List all models of this type
- IMPORTANT: Use
db::function_name
syntax in scripts, notdb.function_name
- Handle errors properly with detailed error messages using debug format
{:?}
for error objects - Convert between Rhai's i64 and Rust's u32/u64 types for IDs and timestamps
- Return saved objects from save functions to ensure proper ID handling
- For each model, implement the following database functions in the
-
Error Handling:
- Use
Box<EvalAltResult>
for error returns - Provide detailed error messages with proper position information
- IMPORTANT: Use
context.call_position()
not the deprecatedcontext.position()
- Handle type conversions safely
- Format error messages with debug format for error objects:
format!("Error: {:?}", err)
- Use
-
Type Conversions:
- Create helper functions for converting between Rhai and Rust types
- Handle special cases like enums with string representations
Implementation Pattern
// Helper functions for type conversion
fn i64_to_u32(val: i64, position: Position, param_name: &str, function_name: &str) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(val).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("{} must be a positive integer less than 2^32 in {}", param_name, function_name).into(),
position
))
})
}
// Model constructor
#[rhai_fn(name = "new_[model]")]
pub fn new_model() -> Model {
Model::new()
}
// Builder method integration
#[rhai_fn(name = "[method_name]", return_raw, global, pure)]
pub fn model_method(model: &mut Model, param: Type) -> Result<Model, Box<EvalAltResult>> {
let owned_model = std::mem::take(model); // or replace if Default not implemented
*model = owned_model.method_name(param);
Ok(model.clone())
}
// Getter method
#[rhai_fn(name = "[field]", global)]
pub fn get_model_field(model: &mut Model) -> Type {
model.field.clone()
}
// Database function
db_module.set_native_fn("save_[model]", move |model: Model| -> Result<Model, Box<EvalAltResult>> {
db_clone.set(&model)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error: {}", e).into(), Position::NONE)))
.map(|(_, model)| model)
});
Important Notes
- All models must follow the builder pattern where methods take ownership of
self
and returnSelf
- If a model doesn't implement
Default
, usestd::mem::replace
instead ofstd::mem::take
- Ensure proper error handling for all operations
- Register all functions with appropriate Rhai names
- Follow the naming conventions established in existing modules
- Ensure all database operations are properly wrapped with error handling
- CRITICAL: When creating example scripts, use function calls not method chaining
- CRITICAL: Always use ID 0 for new objects in scripts to allow database assignment
- CRITICAL: After saving objects, use the returned objects with assigned IDs for further operations
- CRITICAL: In Rust examples, include the Model trait import and use direct field access
Example Rhai Script
// Create a new flow with ID 0 (database will assign real ID)
let flow = new_flow(0, "12345678-1234-1234-1234-123456789012");
name(flow, "Document Approval Flow");
status(flow, STATUS_DRAFT);
// Save flow to database and get saved object with assigned ID
let saved_flow = db::save_flow(flow);
print(`Flow saved to database with ID: ${get_flow_id(saved_flow)}`);
// Retrieve flow from database using assigned ID
let retrieved_flow = db::get_flow_by_id(get_flow_id(saved_flow));
Example Rust Code
use heromodels_core::Model; // Import Model trait for get_id()
// Create flow using Rhai
let flow: Flow = engine.eval("new_flow(0, \"12345678-1234-1234-1234-123456789012\")").unwrap();
// Access fields directly
println!("Flow name: {}", flow.name);
// Save to database using Rhai function
let save_script = "fn save_it(f) { return db::save_flow(f); }";
let save_ast = engine.compile(save_script).unwrap();
let saved_flow: Flow = engine.call_fn(&mut scope, &save_ast, "save_it", (flow,)).unwrap();
println!("Saved flow ID: {}", saved_flow.get_id());
Reference Implementations
See the existing implementations for examples:
heromodels/src/models/calendar/rhai.rs
- Calendar module Rhai bindingsheromodels/src/models/flow/rhai.rs
- Flow module Rhai bindingsheromodels_rhai/examples/flow/flow_script.rhai
- Example Rhai scriptheromodels_rhai/examples/flow/example.rs
- Example Rust code