merge branches and cleanup db

This commit is contained in:
timurgordon
2025-06-27 12:11:04 +03:00
parent 5563d7e27e
commit 1f9ec01934
177 changed files with 1202 additions and 174 deletions

View File

@@ -0,0 +1,151 @@
# 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

View File

@@ -0,0 +1,192 @@
## AI Prompt: Generate `rhai.rs` for a new Rust Model
**Objective:**
Create a `rhai.rs` file to expose the Rust model `[YourModelName]` (and any related owned sub-models like `[YourSubModelName]`) to the Rhai scripting engine. This file should allow Rhai scripts to create, manipulate, and retrieve instances of `[YourModelName]`.
**Input Requirements (Provide this information for your specific model):**
1. **Rust Struct Definition(s):**
```rust
// Example for [YourModelName]
#[derive(Clone, Debug, Serialize, Deserialize)] // Ensure necessary derives
pub struct [YourModelName] {
pub base_data: BaseModelData, // Common field for ID
pub unique_id_field: String, // Example: like flow_uuid
pub name: String,
pub status: String,
// If it owns a collection of sub-models:
pub sub_items: Vec<[YourSubModelName]>,
// Other fields...
}
impl [YourModelName] {
// Constructor
pub fn new(id: u32, unique_id_field: String /*, other essential params */) -> Self {
Self {
base_data: BaseModelData::new(id),
unique_id_field,
name: "".to_string(), // Default or passed in
status: "".to_string(), // Default or passed in
sub_items: Vec::new(),
// ...
}
}
// Builder methods
pub fn name(mut self, name: String) -> Self {
self.name = name;
self
}
pub fn status(mut self, status: String) -> Self {
self.status = status;
self
}
// Method to add sub-items
pub fn add_sub_item(mut self, item: [YourSubModelName]) -> Self {
self.sub_items.push(item);
self
}
// Other methods to expose...
}
// Example for [YourSubModelName] (if applicable)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct [YourSubModelName] {
pub id: u32,
pub description: String,
// ...
}
impl [YourSubModelName] {
pub fn new(id: u32, description: String) -> Self {
Self { id, description }
}
// Builder methods for sub-model...
}
```
2. **Key ID fields that need `i64` (Rhai) to `u32` (Rust) conversion:** (e.g., `base_data.id`, `[YourSubModelName].id`, any foreign key IDs).
**Implementation Guidelines for `rhai.rs`:**
1. **File Structure:**
* Start with necessary imports: `rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext, Position}`, `std::sync::Arc`, your model structs, `BaseModelData`, `OurDB`.
* Include the `i64_to_u32` helper function.
* Define `pub fn register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>) { ... }`.
2. **Helper Function for ID Conversion:**
```rust
fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u32, Box<EvalAltResult>> {
val.try_into().map_err(|_e| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Conversion error for {} in {} from i64 to u32", field_name, object_name),
context_pos,
))
})
}
```
3. **Constructors (e.g., `new_[your_model_name]`):**
* Use `NativeCallContext` for manual argument parsing and `i64_to_u32` conversion for ID fields.
* Example:
```rust
engine.register_fn("new_[your_model_name]",
move |context: NativeCallContext, id_i64: i64, unique_id_str: String /*, other_args... */|
-> Result<[YourModelName], Box<EvalAltResult>> {
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_[your_model_name]")?;
Ok([YourModelName]::new(id_u32, unique_id_str /*, ... */))
});
```
* Do the same for `new_[your_sub_model_name]` if applicable.
* **Note on `adapter_macros`**: `adapt_rhai_i64_input_fn!` is generally NOT suitable for constructors with multiple arguments or mixed types (e.g., `u32` and `String`). Prefer `NativeCallContext`.
4. **Builder Methods:**
* Register functions that take ownership of the model, modify it, and return it.
* Example:
```rust
engine.register_fn("name", |model: [YourModelName], name_val: String| -> [YourModelName] { model.name(name_val) });
engine.register_fn("status", |model: [YourModelName], status_val: String| -> [YourModelName] { model.status(status_val) });
// For adding sub-items (if applicable)
engine.register_fn("add_sub_item", |model: [YourModelName], item: [YourSubModelName]| -> [YourModelName] { model.add_sub_item(item) });
```
5. **Getters:**
* Use `engine.register_get("field_name", |model: &mut [YourModelName]| -> Result<FieldType, Box<EvalAltResult>> { Ok(model.field.clone()) });`
* For `base_data.id` (u32), cast to `i64` for Rhai: `Ok(model.base_data.id as i64)`
* **For `Vec<[YourSubModelName]>` fields (e.g., `sub_items`):** Convert to `rhai::Array`.
```rust
engine.register_get("sub_items", |model: &mut [YourModelName]| -> Result<rhai::Array, Box<EvalAltResult>> {
let rhai_array = model.sub_items.iter().cloned().map(Dynamic::from).collect::<rhai::Array>();
Ok(rhai_array)
});
```
(Ensure `[YourSubModelName]` is `Clone` and works with `Dynamic::from`).
6. **Setters (Less common if using a full builder pattern, but can be useful):**
* Use `engine.register_set("field_name", |model: &mut [YourModelName], value: FieldType| { model.field = value; Ok(()) });`
* If a setter method in Rust takes an ID (e.g., `set_related_id(id: u32)`), and you want to call it from Rhai with an `i64`:
* You *could* use `adapter_macros::adapt_rhai_i64_input_method!(YourModelType::set_related_id, u32)` if `YourModelType::set_related_id` takes `&mut self, u32`.
* Or, handle manually with `NativeCallContext` if the method signature is more complex or doesn't fit the macro.
7. **Other Custom Methods:**
* Register any other public methods from your Rust struct that should be callable.
* Example: `engine.register_fn("custom_method_name", |model: &mut [YourModelName]| model.custom_method_in_rust());`
8. **Database Interaction (Using actual `OurDB` methods like `set` and `get_by_id`):**
* The `Arc<OurDB>` instance passed to `register_[your_model_name]_rhai_module` should be cloned and *captured* by the closures for DB functions.
* The Rhai script will call these functions without explicitly passing the DB instance (e.g., `set_my_model(my_model_instance);`, `let m = get_my_model_by_id(123);`).
* Ensure proper error handling, converting DB errors and `Option::None` (for getters) to `Box<EvalAltResult::ErrorRuntime(...)`.
* **Example for `set_[your_model_name]`:**
```rust
// In register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>)
let captured_db_for_set = Arc::clone(&db);
engine.register_fn("set_[your_model_name]",
move |model: [YourModelName]| -> Result<(), Box<EvalAltResult>> {
captured_db_for_set.set(&model).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to set [YourModelName] (ID: {}): {}", model.base_data.id, e).into(),
Position::NONE,
))
})
});
```
* **Example for `get_[your_model_name]_by_id`:**
```rust
// In register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>)
let captured_db_for_get = Arc::clone(&db);
engine.register_fn("get_[your_model_name]_by_id",
move |context: NativeCallContext, id_i64: i64| -> Result<[YourModelName], Box<EvalAltResult>> {
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_[your_model_name]_by_id")?;
captured_db_for_get.get_by_id(id_u32) // Assumes OurDB directly implements Collection<_, [YourModelName]>
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Error getting [YourModelName] (ID: {}): {}", id_u32, e).into(),
Position::NONE,
)))?
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(
format!("[YourModelName] with ID {} not found", id_u32).into(),
Position::NONE,
)))
});
```
**Example Output Snippet (Illustrating a few parts):**
```rust
// #[...]
// pub struct MyItem { /* ... */ }
// impl MyItem { /* ... */ }
// pub fn register_my_item_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// fn i64_to_u32(...) { /* ... */ }
//
// engine.register_fn("new_my_item", |ctx: NativeCallContext, id: i64, name: String| { /* ... */ });
// engine.register_fn("name", |item: MyItem, name: String| -> MyItem { /* ... */ });
// engine.register_get("id", |item: &mut MyItem| Ok(item.base_data.id as i64));
// engine.register_get("sub_elements", |item: &mut MyItem| {
// Ok(item.sub_elements.iter().cloned().map(Dynamic::from).collect::<rhai::Array>())
// });
// // ... etc.
// }
```