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
- Clean Separation: Your gbapp is self-contained
- Easy Discovery: Visible in
src/analytics/ - Type Safety: Rust compiler checks everything
- Native Performance: Compiles into the main binary
- Familiar Structure: Like the old
.gbapppackages
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:
- Create a module in
src/ - Define keywords for BASIC
- Implement services
- Register with core
- 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!