merge branches and cleanup db
This commit is contained in:
151
heromodels/docs/prompts/new_rhai_rs_gen.md
Normal file
151
heromodels/docs/prompts/new_rhai_rs_gen.md
Normal 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
|
192
heromodels/docs/prompts/rhai_rs_generation_prompt.md
Normal file
192
heromodels/docs/prompts/rhai_rs_generation_prompt.md
Normal 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.
|
||||
// }
|
||||
```
|
Reference in New Issue
Block a user