Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Example: Creating a New gbapp Virtual Crate

This guide walks through creating a new gbapp virtual crate called analytics that adds analytics capabilities to BotServer.

Step 1: Create the Module Structure

Create your gbapp directory in src/:

src/analytics/              # analytics.gbapp virtual crate
├── mod.rs                  # Module definition
├── keywords.rs             # BASIC keywords
├── services.rs             # Core functionality
├── models.rs               # Data structures
└── tests.rs                # Unit tests

Step 2: Define the Module

src/analytics/mod.rs

#![allow(unused)]
fn main() {
//! Analytics gbapp - Provides analytics and reporting functionality
//! 
//! This virtual crate adds analytics keywords to BASIC and provides
//! services for tracking and reporting bot interactions.

pub mod keywords;
pub mod services;
pub mod models;

#[cfg(test)]
mod tests;

use crate::shared::state::AppState;
use std::sync::Arc;

/// Initialize the analytics gbapp
pub fn init(state: Arc<AppState>) -> Result<(), Box<dyn std::error::Error>> {
    log::info!("Initializing analytics.gbapp virtual crate");
    
    // Initialize analytics services
    services::init_analytics_service(&state)?;
    
    Ok(())
}
}

Step 3: Add BASIC Keywords

src/analytics/keywords.rs

#![allow(unused)]
fn main() {
use crate::shared::state::AppState;
use rhai::{Engine, Dynamic};
use std::sync::Arc;

/// Register analytics keywords with the BASIC interpreter
pub fn register_keywords(engine: &mut Engine, state: Arc<AppState>) {
    let state_clone = state.clone();
    
    // TRACK EVENT keyword
    engine.register_fn("TRACK EVENT", move |event_name: String, properties: String| -> String {
        let result = tokio::task::block_in_place(|| {
            tokio::runtime::Handle::current().block_on(async {
                crate::analytics::services::track_event(&state_clone, &event_name, &properties).await
            })
        });
        
        match result {
            Ok(_) => format!("Event '{}' tracked", event_name),
            Err(e) => format!("Failed to track event: {}", e),
        }
    });
    
    // GET ANALYTICS keyword
    engine.register_fn("GET ANALYTICS", move |metric: String, timeframe: String| -> Dynamic {
        let result = tokio::task::block_in_place(|| {
            tokio::runtime::Handle::current().block_on(async {
                crate::analytics::services::get_analytics(&metric, &timeframe).await
            })
        });
        
        match result {
            Ok(data) => Dynamic::from(data),
            Err(_) => Dynamic::UNIT,
        }
    });
    
    // GENERATE REPORT keyword
    engine.register_fn("GENERATE REPORT", move |report_type: String| -> String {
        // Use LLM to generate natural language report
        let data = crate::analytics::services::get_report_data(&report_type);
        
        let prompt = format!(
            "Generate a {} report from this data: {}",
            report_type, data
        );
        
        // This would call the LLM service
        format!("Report generated for: {}", report_type)
    });
}
}

Step 4: Implement Services

src/analytics/services.rs

#![allow(unused)]
fn main() {
use crate::shared::state::AppState;
use crate::shared::models::AnalyticsEvent;
use std::sync::Arc;
use anyhow::Result;

/// Initialize analytics service
pub fn init_analytics_service(state: &Arc<AppState>) -> Result<()> {
    // Set up database tables, connections, etc.
    log::debug!("Analytics service initialized");
    Ok(())
}

/// Track an analytics event
pub async fn track_event(
    state: &Arc<AppState>,
    event_name: &str,
    properties: &str,
) -> Result<()> {
    // Store event in database
    let conn = state.conn.get()?;
    
    // Implementation details...
    log::debug!("Tracked event: {}", event_name);
    
    Ok(())
}

/// Get analytics data
pub async fn get_analytics(metric: &str, timeframe: &str) -> Result<String> {
    // Query analytics data
    let results = match metric {
        "user_count" => get_user_count(timeframe).await?,
        "message_volume" => get_message_volume(timeframe).await?,
        "engagement_rate" => get_engagement_rate(timeframe).await?,
        _ => return Err(anyhow::anyhow!("Unknown metric: {}", metric)),
    };
    
    Ok(results)
}

/// Get data for report generation
pub fn get_report_data(report_type: &str) -> String {
    // Gather data based on report type
    match report_type {
        "daily" => get_daily_report_data(),
        "weekly" => get_weekly_report_data(),
        "monthly" => get_monthly_report_data(),
        _ => "{}".to_string(),
    }
}

// Helper functions
async fn get_user_count(timeframe: &str) -> Result<String> {
    // Implementation
    Ok("100".to_string())
}

async fn get_message_volume(timeframe: &str) -> Result<String> {
    // Implementation
    Ok("5000".to_string())
}

async fn get_engagement_rate(timeframe: &str) -> Result<String> {
    // Implementation
    Ok("75%".to_string())
}

fn get_daily_report_data() -> String {
    // Gather daily metrics
    r#"{"users": 100, "messages": 1500, "sessions": 50}"#.to_string()
}

fn get_weekly_report_data() -> String {
    // Gather weekly metrics
    r#"{"users": 500, "messages": 8000, "sessions": 300}"#.to_string()
}

fn get_monthly_report_data() -> String {
    // Gather monthly metrics
    r#"{"users": 2000, "messages": 35000, "sessions": 1200}"#.to_string()
}
}

Step 5: Define Data Models

src/analytics/models.rs

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize)]
pub struct AnalyticsEvent {
    pub id: uuid::Uuid,
    pub event_name: String,
    pub properties: serde_json::Value,
    pub user_id: Option<String>,
    pub session_id: String,
    pub timestamp: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct MetricSnapshot {
    pub metric_name: String,
    pub value: f64,
    pub timestamp: DateTime<Utc>,
    pub dimensions: serde_json::Value,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Report {
    pub report_type: String,
    pub generated_at: DateTime<Utc>,
    pub data: serde_json::Value,
    pub summary: String,
}
}

Step 6: Register with Core

Update src/basic/keywords/mod.rs to include your gbapp:

#![allow(unused)]
fn main() {
use crate::analytics;

pub fn register_all_keywords(engine: &mut Engine, state: Arc<AppState>) {
    // ... existing keywords
    
    // Register analytics.gbapp keywords
    analytics::keywords::register_keywords(engine, state.clone());
}
}

Update src/main.rs or initialization code:

#![allow(unused)]
fn main() {
// Initialize analytics gbapp
analytics::init(state.clone())?;
}

Step 7: Add Tests

src/analytics/tests.rs

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_track_event() {
        // Test event tracking
        let event_name = "user_login";
        let properties = r#"{"user_id": "123"}"#;
        
        // Test implementation
        assert!(true);
    }
    
    #[tokio::test]
    async fn test_get_analytics() {
        // Test analytics retrieval
        let metric = "user_count";
        let timeframe = "daily";
        
        // Test implementation
        assert!(true);
    }
}
}

Step 8: Use in BASIC Scripts

Now your gbapp keywords are available in BASIC:

' Track user actions
TRACK EVENT "button_clicked", "button=submit"

' Get metrics
daily_users = GET ANALYTICS "user_count", "daily"
TALK "Daily active users: " + daily_users

' Generate AI-powered report
report = GENERATE REPORT "weekly"
TALK report

' Combine with LLM for insights
metrics = GET ANALYTICS "all", "monthly"
insights = LLM "Analyze these metrics and provide insights: " + metrics
TALK insights

Step 9: Add Feature Flag (Optional)

If your gbapp should be optional, add it to Cargo.toml:

[features]
analytics = []

# Include in default features if always needed
default = ["ui-server", "chat", "analytics"]

Then conditionally compile:

#![allow(unused)]
fn main() {
#[cfg(feature = "analytics")]
pub mod analytics;

#[cfg(feature = "analytics")]
analytics::keywords::register_keywords(engine, state.clone());
}

Benefits of This Approach

  1. Clean Separation: Your gbapp is self-contained
  2. Easy Discovery: Visible in src/analytics/
  3. Type Safety: Rust compiler checks everything
  4. Native Performance: Compiles into the main binary
  5. Familiar Structure: Like the old .gbapp packages

Best Practices

DO:

  • Keep your gbapp focused on one domain
  • Provide clear BASIC keywords
  • Use LLM for complex logic
  • Write comprehensive tests
  • Document your keywords

DON’T:

  • Create overly complex implementations
  • Duplicate existing functionality
  • Skip error handling
  • Forget about async/await
  • Ignore the BASIC-first philosophy

Summary

Creating a gbapp virtual crate is straightforward:

  1. Create a module in src/
  2. Define keywords for BASIC
  3. Implement services
  4. Register with core
  5. Use in BASIC scripts

Your gbapp becomes part of BotServer’s compiled binary, providing native performance while maintaining the conceptual clarity of the package system. Most importantly, remember that the implementation should be minimal - let BASIC + LLM handle the complexity!