Files
herolib/aiprompts/heromodel_instruct.md
2025-10-12 16:02:34 +04:00

22 KiB

HeroModels Implementation Guide

This guide provides comprehensive instructions for creating new models in the HeroModels system, including best practices for model structure, serialization/deserialization, testing, and integration with the HeroModels factory.

Table of Contents

  1. Model Structure Overview
  2. Creating a New Model
  3. Serialization and Deserialization
  4. Database Operations
  5. API Handler Implementation
  6. Testing Models
  7. Integration with Factory
  8. Advanced Features
  9. Best Practices
  10. Example Implementation

Model Structure Overview

Each model in the HeroModels system consists of several components:

  1. Model Struct: The core data structure inheriting from db.Base
  2. DB Wrapper Struct: Provides database operations for the model
  3. Argument Struct: Used for creating and updating model instances
  4. API Handler Function: Handles RPC calls for the model
  5. List Arguments Struct: Used for filtering when listing instances

Directory Structure

lib/hero/heromodels/
  ├── model_name.v         # Main model file
  ├── model_name_test.v    # Tests for the model
  └── factory.v            # Factory integration

Creating a New Model

1. Define the Model Struct

Create a new file model_name.v in the lib/hero/heromodels directory.

module heromodels

import incubaid.herolib.core.db
import incubaid.herolib.core.encoder
import incubaid.herolib.core.ourtime
import incubaid.herolib.core.jsonrpc { Response }
import json

// Model struct - inherits from db.Base
pub struct ModelName {
pub mut:
    db.Base       // Inherit from db.Base
    name         string
    description  string
    created_at   u64
    updated_at   u64
    // Add additional fields as needed
}

// TypeName returns the type name used for serialization
pub fn (self ModelName) type_name() string {
    return 'heromodels.ModelName'
}

2. Define the Argument Struct for Model Creation/Updates

// Argument struct for creating/updating models with params attribute
@[params]
pub struct ModelNameArg {
pub mut:
    id           u32     // Optional for updates, ignored for creation
    name         string  @[required] // Required field
    description  string
    // Add additional fields as needed
}

3. Define the List Arguments Struct for Filtering

// Arguments for filtering when listing models
@[params]
pub struct ModelNameListArg {
pub mut:
    // Add filter fields (e.g., status, type, etc.)
    limit        int = 100 // Default limit
}

4. Create the DB Wrapper Struct

// DB Wrapper struct for database operations
pub struct DBModelName {
pub mut:
    db &db.DB
}

Serialization and Deserialization

Implement the dump and load methods for serialization/deserialization.

Dump Method (Serialization)

// Dump serializes the model to the encoder
pub fn (self ModelName) dump(mut e encoder.Encoder) ! {
    // Always dump the Base first
    self.Base.dump(mut e)!
    
    // Dump model-specific fields in the same order they will be loaded
    e.add_string(self.name)!
    e.add_string(self.description)!
    e.add_u64(self.created_at)!
    e.add_u64(self.updated_at)!
    // Add more fields in the exact order they should be loaded
}

Load Method (Deserialization)

// Load deserializes the model from the decoder
pub fn (mut self DBModelName) load(mut obj ModelName, mut d encoder.Decoder) ! {
    // Always load the Base first
    obj.Base.load(mut d)!
    
    // Load model-specific fields in the same order they were dumped
    obj.name = d.get_string()!
    obj.description = d.get_string()!
    obj.created_at = d.get_u64()!
    obj.updated_at = d.get_u64()!
    // Add more fields in the exact order they were dumped
}

Database Operations

Implement the standard CRUD operations and additional methods.

New Instance Creation

// Create a new model instance from arguments
pub fn (mut self DBModelName) new(args ModelNameArg) !ModelName {
    mut o := ModelName{
        name: args.name
        description: args.description
        // Initialize other fields
        created_at: ourtime.now().unix()
        updated_at: ourtime.now().unix()
    }
    
    // Additional initialization logic
    
    return o
}

Set (Create or Update)

// Save or update a model instance
pub fn (mut self DBModelName) set(o ModelName) !ModelName {
    return self.db.set[ModelName](o)!
}

Get

// Retrieve a model instance by ID
pub fn (mut self DBModelName) get(id u32) !ModelName {
    mut o, data := self.db.get_data[ModelName](id)!
    mut e_decoder := encoder.decoder_new(data)
    self.load(mut o, mut e_decoder)!
    return o
}

Delete

// Delete a model instance by ID
pub fn (mut self DBModelName) delete(id u32) !bool {
    // Check if the item exists before trying to delete
    if !self.db.exists[ModelName](id)! {
        return false
    }
    self.db.delete[ModelName](id)!
    return true
}

Exist

// Check if a model instance exists by ID
pub fn (mut self DBModelName) exist(id u32) !bool {
    return self.db.exists[ModelName](id)!
}

List with Filtering

// List model instances with optional filtering
pub fn (mut self DBModelName) list(args ModelNameListArg) ![]ModelName {
    // Get all instances
    all_items := self.db.list[ModelName]()!.map(self.get(it)!)
    
    // Apply filters
    mut filtered_items := []ModelName{}
    for item in all_items {
        // Apply your filter conditions here
        // Example:
        // if args.some_filter && item.some_property != args.filter_value {
        //     continue
        // }
        
        filtered_items << item
    }
    
    // Apply limit
    mut limit := args.limit
    if limit > 100 {
        limit = 100
    }
    if filtered_items.len > limit {
        return filtered_items[..limit]
    }
    
    return filtered_items
}

API Handler Implementation

Create the handler function for RPC requests.

// Handler for RPC calls to this model
pub fn model_name_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
    match method {
        'get' {
            id := db.decode_u32(params)!
            res := f.model_name.get(id)!
            return new_response(rpcid, json.encode_pretty(res))
        }
        'set' {
            mut args := db.decode_generic[ModelNameArg](params)!
            mut o := f.model_name.new(args)!
            if args.id != 0 {
                o.id = args.id
            }
            o = f.model_name.set(o)!
            return new_response_int(rpcid, int(o.id))
        }
        'delete' {
            id := db.decode_u32(params)!
            deleted := f.model_name.delete(id)!
            if deleted {
                return new_response_true(rpcid)
            } else {
                return new_error(rpcid,
                    code:    404
                    message: 'ModelName with ID ${id} not found'
                )
            }
        }
        'exist' {
            id := db.decode_u32(params)!
            if f.model_name.exist(id)! {
                return new_response_true(rpcid)
            } else {
                return new_response_false(rpcid)
            }
        }
        'list' {
            args := db.decode_generic[ModelNameListArg](params)!
            res := f.model_name.list(args)!
            return new_response(rpcid, json.encode_pretty(res))
        }
        else {
            return new_error(rpcid,
                code:    32601
                message: 'Method ${method} not found on model_name'
            )
        }
    }
}

Testing Models

Create a model_name_test.v file to test your model.

module heromodels

fn test_model_name_crud() ! {
    // Initialize DB for testing
    mut mydb := db.new_test()!
    mut db_model := DBModelName{
        db: &mydb
    }
    
    // Create
    mut args := ModelNameArg{
        name: 'Test Model'
        description: 'A test model'
    }
    
    mut model := db_model.new(args)!
    model = db_model.set(model)!
    model_id := model.id
    
    // Verify ID assignment
    assert model_id > 0
    
    // Read
    retrieved_model := db_model.get(model_id)!
    assert retrieved_model.name == 'Test Model'
    assert retrieved_model.description == 'A test model'
    
    // Update
    retrieved_model.description = 'Updated description'
    updated_model := db_model.set(retrieved_model)!
    assert updated_model.description == 'Updated description'
    
    // Delete
    deleted := db_model.delete(model_id)!
    assert deleted == true
    
    // Verify deletion
    exists := db_model.exist(model_id)!
    assert exists == false
}

fn test_model_name_type_name() ! {
    // Initialize DB for testing
    mut mydb := db.new_test()!
    mut db_model := DBModelName{
        db: &mydb
    }
    
    // Create a model
    mut model := db_model.new(
        name: 'Type Test'
        description: 'Testing type_name'
    )!
    
    // Test type_name method
    assert model.type_name() == 'heromodels.ModelName'
}

fn test_model_name_description() ! {
    // Initialize DB for testing
    mut mydb := db.new_test()!
    mut db_model := DBModelName{
        db: &mydb
    }
    
    // Create a model
    mut model := db_model.new(
        name: 'Description Test'
        description: 'Testing description method'
    )!
    
    // Test description method for each methodname
    assert model.description('set') == 'Create or update a model. Returns the ID of the model.'
    assert model.description('get') == 'Retrieve a model by ID. Returns the model object.'
    assert model.description('delete') == 'Delete a model by ID. Returns true if successful.'
    assert model.description('exist') == 'Check if a model exists by ID. Returns true or false.'
    assert model.description('list') == 'List all models. Returns an array of model objects.'
}

fn test_model_name_example() ! {
    // Initialize DB for testing
    mut mydb := db.new_test()!
    mut db_model := DBModelName{
        db: &mydb
    }
    
    // Create a model
    mut model := db_model.new(
        name: 'Example Test'
        description: 'Testing example method'
    )!
    
    // Test example method for each methodname
    set_call, set_result := model.example('set')
    // Assert expected call and result format
    
    get_call, get_result := model.example('get')
    // Assert expected call and result format
    
    delete_call, delete_result := model.example('delete')
    // Assert expected call and result format
    
    exist_call, exist_result := model.example('exist')
    // Assert expected call and result format
    
    list_call, list_result := model.example('list')
    // Assert expected call and result format
}

fn test_model_name_encoding_decoding() ! {
    // Initialize DB for testing
    mut mydb := db.new_test()!
    mut db_model := DBModelName{
        db: &mydb
    }
    
    // Create a model with all fields populated
    mut args := ModelNameArg{
        name: 'Encoding Test'
        description: 'Testing encoding/decoding'
        // Set other fields
    }
    
    mut model := db_model.new(args)!
    
    // Save the model
    model = db_model.set(model)!
    model_id := model.id
    
    // Retrieve and verify all fields were properly encoded/decoded
    retrieved_model := db_model.get(model_id)!
    
    // Verify all fields match the original
    assert retrieved_model.name == 'Encoding Test'
    assert retrieved_model.description == 'Testing encoding/decoding'
    // Check other fields
}

Integration with Factory

Update the factory.v file to include your new model.

1. Add the Model to the Factory Struct

// In factory.v
pub struct ModelsFactory {
pub mut:
    db                &db.DB
    user              DBUser
    group             DBGroup
    // Add your new model
    model_name        DBModelName
    // Other models...
    rpc_handler       &jsonrpc.Handler
}

2. Initialize the Model in the Factory New Method

// In factory.v, in the new() function
pub fn new(args ModelsFactoryArgs) !&ModelsFactory {
    // Existing code...
    
    mut f := ModelsFactory{
        db: &mydb
        user: DBUser{
            db: &mydb
        }
        // Add your new model
        model_name: DBModelName{
            db: &mydb
        }
        // Other models...
        rpc_handler: &h
    }
    
    // Existing code...
}

3. Add Handler Registration to the Factory API Handler

// In factory.v, in the group_api_handler function
pub fn group_api_handler(rpcid int, servercontext map[string]string, actorname string, methodname string, params string) !jsonrpc.Response {
    // Existing code...
    
    match actorname {
        // Existing cases...
        
        'model_name' {
            return model_name_handle(mut f, rpcid, servercontext, userref, methodname, params)!
        }
        
        // Existing cases...
        
        else {
            // Error handling
        }
    }
}

Advanced Features

Custom Methods

You can add custom methods to your model for specific business logic:

// Add a custom method to the model
pub fn (mut self ModelName) custom_operation(param string) !string {
    // Custom business logic
    self.updated_at = ourtime.now().unix()
    return 'Performed ${param} operation'
}

Enhanced RPC Handling

Extend the RPC handler to support your custom methods:

// In the model_name_handle function
match method {
    // Standard CRUD methods...
    
    'custom_operation' {
        id := db.decode_u32(params)!
        mut model := f.model_name.get(id)!
        
        // Extract parameter from JSON
        param_struct := json.decode(struct { param string }, params) or {
            return new_error(rpcid,
                code:    32602
                message: 'Invalid parameters for custom_operation'
            )
        }
        
        result := model.custom_operation(param_struct.param)!
        model = f.model_name.set(model)! // Save changes
        return new_response(rpcid, json.encode(result))
    }
    
    else {
        // Error handling
    }
}

Best Practices

  1. Field Order: Keep field ordering consistent between dump and load methods
  2. Error Handling: Use the ! operator consistently for error propagation
  3. Timestamp Management: Initialize timestamps using ourtime.now().unix()
  4. Required Fields: Mark mandatory fields with @[required] attribute
  5. Limits: Enforce list limits (default 100)
  6. ID Handling: Always check existence before operations like delete
  7. Validation: Add validation in the new and set methods
  8. API Methods: Implement the standard CRUD operations (get, set, delete, exist, list)
  9. Comments: Document all fields and methods
  10. Testing: Create comprehensive tests covering all methods

Example Implementation

Here is a complete example of a simple "Project" model:

module heromodels

import incubaid.herolib.core.db
import incubaid.herolib.core.encoder
import incubaid.herolib.core.ourtime
import incubaid.herolib.core.jsonrpc { Response }
import json

// Project model
pub struct Project {
pub mut:
    db.Base       // Inherit from db.Base
    name         string
    description  string
    status       ProjectStatus
    owner_id     u32
    members      []u32
    created_at   u64
    updated_at   u64
}

// Project status enum
pub enum ProjectStatus {
    active
    completed
    archived
}

// TypeName for serialization
pub fn (self Project) type_name() string {
    return 'heromodels.Project'
}

// Dump serializes the model
pub fn (self Project) dump(mut e encoder.Encoder) ! {
    self.Base.dump(mut e)!
    e.add_string(self.name)!
    e.add_string(self.description)!
    e.add_u8(u8(self.status))!
    e.add_u32(self.owner_id)!
    e.add_array_u32(self.members)!
    e.add_u64(self.created_at)!
    e.add_u64(self.updated_at)!
}

// Project argument struct
@[params]
pub struct ProjectArg {
pub mut:
    id           u32
    name         string  @[required]
    description  string
    status       ProjectStatus = .active
    owner_id     u32     @[required]
    members      []u32
}

// Project list argument struct
@[params]
pub struct ProjectListArg {
pub mut:
    status      ProjectStatus
    owner_id    u32
    limit       int = 100
}

// DB wrapper struct
pub struct DBProject {
pub mut:
    db &db.DB
}

// Load deserializes the model
pub fn (mut self DBProject) load(mut obj Project, mut d encoder.Decoder) ! {
    obj.Base.load(mut d)!
    obj.name = d.get_string()!
    obj.description = d.get_string()!
    obj.status = unsafe { ProjectStatus(d.get_u8()!) }
    obj.owner_id = d.get_u32()!
    obj.members = d.get_array_u32()!
    obj.created_at = d.get_u64()!
    obj.updated_at = d.get_u64()!
}

// Create a new Project
pub fn (mut self DBProject) new(args ProjectArg) !Project {
    mut o := Project{
        name: args.name
        description: args.description
        status: args.status
        owner_id: args.owner_id
        members: args.members
        created_at: ourtime.now().unix()
        updated_at: ourtime.now().unix()
    }
    
    return o
}

// Save or update a Project
pub fn (mut self DBProject) set(o Project) !Project {
    return self.db.set[Project](o)!
}

// Get a Project by ID
pub fn (mut self DBProject) get(id u32) !Project {
    mut o, data := self.db.get_data[Project](id)!
    mut e_decoder := encoder.decoder_new(data)
    self.load(mut o, mut e_decoder)!
    return o
}

// Delete a Project by ID
pub fn (mut self DBProject) delete(id u32) !bool {
    if !self.db.exists[Project](id)! {
        return false
    }
    self.db.delete[Project](id)!
    return true
}

// Check if a Project exists
pub fn (mut self DBProject) exist(id u32) !bool {
    return self.db.exists[Project](id)!
}

// List Projects with filtering
pub fn (mut self DBProject) list(args ProjectListArg) ![]Project {
    all_projects := self.db.list[Project]()!.map(self.get(it)!)
    
    mut filtered_projects := []Project{}
    for project in all_projects {
        // Filter by status if provided
        if args.status != .active && project.status != args.status {
            continue
        }
        
        // Filter by owner_id if provided
        if args.owner_id != 0 && project.owner_id != args.owner_id {
            continue
        }
        
        filtered_projects << project
    }
    
    mut limit := args.limit
    if limit > 100 {
        limit = 100
    }
    if filtered_projects.len > limit {
        return filtered_projects[..limit]
    }
    
    return filtered_projects
}

// API description method
pub fn (self Project) description(methodname string) string {
    match methodname {
        'set' { return 'Create or update a project. Returns the ID of the project.' }
        'get' { return 'Retrieve a project by ID. Returns the project object.' }
        'delete' { return 'Delete a project by ID. Returns true if successful.' }
        'exist' { return 'Check if a project exists by ID. Returns true or false.' }
        'list' { return 'List all projects. Returns an array of project objects.' }
        else { return 'This is generic method for the root object, TODO fill in, ...' }
    }
}

// API example method
pub fn (self Project) example(methodname string) (string, string) {
    match methodname {
        'set' {
            return '{"project": {"name": "Website Redesign", "description": "Redesign company website", "status": "active", "owner_id": 1, "members": [2, 3]}}', '1'
        }
        'get' {
            return '{"id": 1}', '{"name": "Website Redesign", "description": "Redesign company website", "status": "active", "owner_id": 1, "members": [2, 3]}'
        }
        'delete' {
            return '{"id": 1}', 'true'
        }
        'exist' {
            return '{"id": 1}', 'true'
        }
        'list' {
            return '{}', '[{"name": "Website Redesign", "description": "Redesign company website", "status": "active", "owner_id": 1, "members": [2, 3]}]'
        }
        else {
            return '{}', '{}'
        }
    }
}

// API handler function
pub fn project_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
    match method {
        'get' {
            id := db.decode_u32(params)!
            res := f.project.get(id)!
            return new_response(rpcid, json.encode_pretty(res))
        }
        'set' {
            mut args := db.decode_generic[ProjectArg](params)!
            mut o := f.project.new(args)!
            if args.id != 0 {
                o.id = args.id
            }
            o = f.project.set(o)!
            return new_response_int(rpcid, int(o.id))
        }
        'delete' {
            id := db.decode_u32(params)!
            deleted := f.project.delete(id)!
            if deleted {
                return new_response_true(rpcid)
            } else {
                return new_error(rpcid,
                    code:    404
                    message: 'Project with ID ${id} not found'
                )
            }
        }
        'exist' {
            id := db.decode_u32(params)!
            if f.project.exist(id)! {
                return new_response_true(rpcid)
            } else {
                return new_response_false(rpcid)
            }
        }
        'list' {
            args := db.decode_generic[ProjectListArg](params)!
            res := f.project.list(args)!
            return new_response(rpcid, json.encode_pretty(res))
        }
        else {
            return new_error(rpcid,
                code:    32601
                message: 'Method ${method} not found on project'
            )
        }
    }
}

This complete guide should provide all the necessary information to create and maintain models in the HeroModels system following the established patterns and best practices.