db/heromodels/docs/prompts/new_rhai_rs_gen.md
2025-06-27 12:11:04 +03:00

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

  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
    • 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

// 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

// 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:

  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