151 lines
7.1 KiB
Markdown
151 lines
7.1 KiB
Markdown
# 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
|
|
1. **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).
|
|
2. **Database Access**: Database functions are exposed as global functions in a 'db' module namespace (e.g., `db::save_flow`, not `db.save_flow`).
|
|
3. **ID Handling**: New objects should use ID 0 to allow database assignment of real IDs.
|
|
4. **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
|
|
|
|
1. **Module Structure and Imports**:
|
|
- Create a `rhai.rs` file that exports a function named `register_[model]_rhai_module` which takes an Engine and an Arc<OurDB>
|
|
- **IMPORTANT**: Import `heromodels_core::Model` trait for access to methods like `get_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
|
|
|
|
2. **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) or `std::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
|
|
|
|
3. **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`, not `flow.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.)
|
|
|
|
4. **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 ID
|
|
- `db::get_[model]_by_id`: Retrieve a model by ID
|
|
- `db::delete_[model]`: Delete a model from the database
|
|
- `db::list_[model]s`: List all models of this type
|
|
- **IMPORTANT**: Use `db::function_name` syntax in scripts, not `db.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
|
|
|
|
5. **Error Handling**:
|
|
- Use `Box<EvalAltResult>` for error returns
|
|
- Provide detailed error messages with proper position information
|
|
- **IMPORTANT**: Use `context.call_position()` not the deprecated `context.position()`
|
|
- Handle type conversions safely
|
|
- Format error messages with debug format for error objects: `format!("Error: {:?}", err)`
|
|
|
|
6. **Type Conversions**:
|
|
- Create helper functions for converting between Rhai and Rust types
|
|
- Handle special cases like enums with string representations
|
|
|
|
## Implementation Pattern
|
|
|
|
```rust
|
|
// 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
|
|
|
|
1. All models must follow the builder pattern where methods take ownership of `self` and return `Self`
|
|
2. If a model doesn't implement `Default`, use `std::mem::replace` instead of `std::mem::take`
|
|
3. Ensure proper error handling for all operations
|
|
4. Register all functions with appropriate Rhai names
|
|
5. Follow the naming conventions established in existing modules
|
|
6. Ensure all database operations are properly wrapped with error handling
|
|
7. **CRITICAL**: When creating example scripts, use function calls not method chaining
|
|
8. **CRITICAL**: Always use ID 0 for new objects in scripts to allow database assignment
|
|
9. **CRITICAL**: After saving objects, use the returned objects with assigned IDs for further operations
|
|
10. **CRITICAL**: In Rust examples, include the Model trait import and use direct field access
|
|
|
|
## Example Rhai Script
|
|
|
|
```rhai
|
|
// 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
|
|
|
|
```rust
|
|
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:
|
|
1. `heromodels/src/models/calendar/rhai.rs` - Calendar module Rhai bindings
|
|
2. `heromodels/src/models/flow/rhai.rs` - Flow module Rhai bindings
|
|
3. `heromodels_rhai/examples/flow/flow_script.rhai` - Example Rhai script
|
|
4. `heromodels_rhai/examples/flow/example.rs` - Example Rust code |