db/model_trait_unification_plan.md
2025-04-22 12:53:17 +04:00

7.1 KiB

Model Trait Unification Plan

Introduction

This document outlines the plan to unify the BaseModel and ModelBuilder traits in the heromodels crate into a single Model trait. The goal is to simplify the codebase, reduce boilerplate, and make the API more intuitive while maintaining all existing functionality.

Current Structure

Currently, the codebase has two separate traits:

  1. BaseModel - Provides database-related functionality:

    • db_prefix() - Returns a string prefix for database operations
    • db_keys() - Returns index keys for the model
    • get_id() - Returns the model's unique ID
  2. ModelBuilder - Provides builder pattern functionality:

    • base_data_mut() - Gets a mutable reference to the base data
    • id() - Sets the ID for the model
    • build() - Finalizes the model by updating timestamps

Each model (like User and Comment) implements both traits separately, and there are two separate macros for implementing these traits:

// For BaseModel
impl_base_model!(User, "user");

// For ModelBuilder
impl_model_builder!(User);

This leads to duplication and cognitive overhead when working with models.

Proposed Structure

We will create a unified Model trait that combines all the functionality from both existing traits:

classDiagram
    class Model {
        <<trait>>
        +db_prefix() static str
        +db_keys() Vec~IndexKey~
        +get_id() u32
        +base_data_mut() &mut BaseModelData
        +id(u32) Self
        +build() Self
    }
    
    class User {
        +base_data BaseModelData
        +username String
        +email String
        +full_name String
        +is_active bool
    }
    
    class Comment {
        +base_data BaseModelData
        +user_id u32
        +content String
    }
    
    Model <|-- User
    Model <|-- Comment

Implementation Steps

1. Update model.rs

Create the new Model trait that combines all methods from both existing traits:

/// Unified trait for all models
pub trait Model: Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {
    /// Get the database prefix for this model type
    fn db_prefix() -> &'static str where Self: Sized;
    
    /// Returns a list of index keys for this model instance
    /// These keys will be used to create additional indexes in the TST
    fn db_keys(&self) -> Vec<IndexKey> {
        Vec::new()
    }
    
    /// Get the unique ID for this model
    fn get_id(&self) -> u32;
    
    /// Get a mutable reference to the base_data field
    fn base_data_mut(&mut self) -> &mut BaseModelData;
    
    /// Set the ID for this model
    fn id(mut self, id: u32) -> Self where Self: Sized {
        self.base_data_mut().id = id;
        self
    }
    
    /// Build the model, updating the modified timestamp
    fn build(mut self) -> Self where Self: Sized {
        self.base_data_mut().update_modified();
        self
    }
}

Create a new implementation macro that implements all required methods:

/// Macro to implement Model for a struct that contains a base_data field of type BaseModelData
#[macro_export]
macro_rules! impl_model {
    ($type:ty, $prefix:expr) => {
        impl $crate::core::model::Model for $type {
            fn db_prefix() -> &'static str {
                $prefix
            }
            
            fn get_id(&self) -> u32 {
                self.base_data.id
            }
            
            fn base_data_mut(&mut self) -> &mut $crate::core::model::BaseModelData {
                &mut self.base_data
            }
            
            // Other methods have default implementations
        }
    };
}

Mark the old traits and macros as deprecated (or remove them entirely):

#[deprecated(
    since = "0.2.0",
    note = "Use the unified Model trait instead"
)]
pub trait BaseModel { /* ... */ }

#[deprecated(
    since = "0.2.0",
    note = "Use the unified Model trait instead"
)]
pub trait ModelBuilder { /* ... */ }

#[deprecated(
    since = "0.2.0",
    note = "Use impl_model instead"
)]
#[macro_export]
macro_rules! impl_base_model { /* ... */ }

#[deprecated(
    since = "0.2.0",
    note = "Use impl_model instead"
)]
#[macro_export]
macro_rules! impl_model_builder { /* ... */ }

2. Update User and Comment Implementations

For User:

// Implement Model for User
impl_model!(User, "user");

// Custom implementation of db_keys
impl Model for User {
    fn db_keys(&self) -> Vec<IndexKey> {
        vec![
            IndexKey {
                name: "username",
                value: self.username.clone(),
            },
            IndexKey {
                name: "email",
                value: self.email.clone(),
            },
            IndexKey {
                name: "is_active",
                value: self.is_active.to_string(),
            },
        ]
    }
}

For Comment:

// Implement Model for Comment
impl_model!(Comment, "comment");

// Custom implementation of db_keys
impl Model for Comment {
    fn db_keys(&self) -> Vec<IndexKey> {
        vec![
            IndexKey {
                name: "user_id",
                value: self.user_id.to_string(),
            },
        ]
    }
}

3. Update Imports and References

Update the lib.rs file to export the new trait and macro:

// Export core module
pub mod core;

// Export userexample module
pub mod userexample;

// Re-export key types for convenience
pub use core::model::{Model, BaseModelData, IndexKey};
pub use core::Comment;
pub use userexample::User;

// Re-export macros
pub use crate::impl_model;

Update the example code to use the new trait:

use heromodels::{Model, Comment, User};

fn main() {
    println!("Hero Models - Basic Usage Example");
    println!("================================");
    
    // Create a new user using the fluent interface
    let user = User::new(1)
        .username("johndoe")
        .email("john.doe@example.com")
        .full_name("John Doe")
        .build();
        
    println!("Created user: {:?}", user);
    println!("User ID: {}", user.get_id());
    println!("User DB Prefix: {}", User::db_prefix());
    
    // Create a comment for the user
    let comment = Comment::new(1)
        .user_id(2) // commenter's user ID
        .content("This is a comment on the user")
        .build();
        
    println!("\nCreated comment: {:?}", comment);
    println!("Comment ID: {}", comment.get_id());
    println!("Comment DB Prefix: {}", Comment::db_prefix());
}

4. Testing

Run the example to ensure it works as expected:

cargo run --example basic_user_example

Verify that all functionality is preserved and that the output matches the expected output.

Benefits of This Approach

  1. Simplified Code Structure: One trait instead of two means less cognitive overhead
  2. Reduced Boilerplate: A single macro implementation reduces repetitive code
  3. Improved Usability: No need to import multiple traits or worry about which trait provides which method
  4. Maintained Functionality: All existing functionality is preserved
  5. Better Encapsulation: The model concept is more cohesive as a single trait