From b430866dbf2be0fcd5e0d146fe4637407b651873 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Tue, 23 Dec 2025 15:52:20 -0300 Subject: [PATCH] Update library components and models --- Cargo.toml | 22 ++---------------- src/branding.rs | 40 -------------------------------- src/http_client.rs | 54 +++++++------------------------------------- src/lib.rs | 10 -------- src/message_types.rs | 10 -------- src/models.rs | 7 +++--- src/resilience.rs | 43 ----------------------------------- src/version.rs | 40 +------------------------------- 8 files changed, 14 insertions(+), 212 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2317891..c10caec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,23 +39,5 @@ validator = { version = "0.18", features = ["derive"], optional = true } [dev-dependencies] tokio = { version = "1.41", features = ["rt", "macros"] } -[lints.rust] -unused_imports = "warn" -unused_variables = "warn" -unused_mut = "warn" -unsafe_code = "deny" -missing_debug_implementations = "warn" - -[lints.clippy] -all = "warn" -pedantic = "warn" -nursery = "warn" -cargo = "warn" -unwrap_used = "warn" -expect_used = "warn" -panic = "warn" -todo = "warn" -# Disabled: has false positives for functions with mut self, heap types (Vec, String) -missing_const_for_fn = "allow" -# Disabled: transitive dependencies we cannot control -multiple_crate_versions = "allow" +[lints] +workspace = true diff --git a/src/branding.rs b/src/branding.rs index e2c8cf0..6c82d4c 100644 --- a/src/branding.rs +++ b/src/branding.rs @@ -1,56 +1,32 @@ -//! White-Label Branding Module -//! -//! Allows complete customization of platform identity. -//! When a .product file exists with name=MyCustomPlatform, -//! "General Bots" never appears in logs, display, messages, footer - nothing. use log::info; use serde::{Deserialize, Serialize}; use std::path::Path; use std::sync::OnceLock; -/// Global branding configuration - loaded once at startup static BRANDING: OnceLock = OnceLock::new(); -/// Default platform name const DEFAULT_PLATFORM_NAME: &str = "General Bots"; const DEFAULT_PLATFORM_SHORT: &str = "GB"; const DEFAULT_PLATFORM_DOMAIN: &str = "generalbots.com"; -/// Branding configuration loaded from `.product` file #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BrandingConfig { - /// Platform name (e.g., "`MyCustomPlatform`") pub name: String, - /// Short name for logs and compact displays (e.g., "MCP") pub short_name: String, - /// Company/organization name pub company: Option, - /// Domain for URLs and emails pub domain: Option, - /// Support email pub support_email: Option, - /// Logo URL (for web UI) pub logo_url: Option, - /// Favicon URL pub favicon_url: Option, - /// Primary color (hex) pub primary_color: Option, - /// Secondary color (hex) pub secondary_color: Option, - /// Footer text pub footer_text: Option, - /// Copyright text pub copyright: Option, - /// Custom CSS URL pub custom_css: Option, - /// Terms of service URL pub terms_url: Option, - /// Privacy policy URL pub privacy_url: Option, - /// Documentation URL pub docs_url: Option, - /// Whether this is a white-label deployment pub is_white_label: bool, } @@ -81,7 +57,6 @@ impl Default for BrandingConfig { } impl BrandingConfig { - /// Load branding from .product file if it exists #[must_use] pub fn load() -> Self { let search_paths = [ @@ -98,7 +73,6 @@ impl BrandingConfig { } } - // Check environment variable if let Ok(product_file) = std::env::var("PRODUCT_FILE") { if let Ok(config) = Self::load_from_file(&product_file) { info!( @@ -109,7 +83,6 @@ impl BrandingConfig { } } - // Check for individual environment overrides let mut config = Self::default(); if let Ok(name) = std::env::var("PLATFORM_NAME") { @@ -135,7 +108,6 @@ impl BrandingConfig { config } - /// Load from a specific file path fn load_from_file(path: &str) -> Result> { let path = Path::new(path); if !path.exists() { @@ -144,12 +116,10 @@ impl BrandingConfig { let content = std::fs::read_to_string(path)?; - // Try parsing as TOML first if let Ok(config) = toml::from_str::(&content) { return Ok(config.into()); } - // Try parsing as simple key=value format let mut config = Self { is_white_label: true, ..Self::default() @@ -190,7 +160,6 @@ impl BrandingConfig { } } -/// TOML format for .product file #[derive(Debug, Deserialize)] struct ProductFile { name: String, @@ -255,39 +224,32 @@ impl From for BrandingConfig { } } -// Global Access Functions -/// Initialize branding at application startup pub fn init_branding() { let config = BrandingConfig::load(); let _ = BRANDING.set(config); } -/// Get the current branding configuration #[must_use] pub fn branding() -> &'static BrandingConfig { BRANDING.get_or_init(BrandingConfig::load) } -/// Get the platform name #[must_use] pub fn platform_name() -> &'static str { &branding().name } -/// Get the short platform name #[must_use] pub fn platform_short() -> &'static str { &branding().short_name } -/// Check if this is a white-label deployment #[must_use] pub fn is_white_label() -> bool { branding().is_white_label } -/// Get formatted copyright text #[must_use] pub fn copyright_text() -> String { branding().copyright.clone().unwrap_or_else(|| { @@ -299,7 +261,6 @@ pub fn copyright_text() -> String { }) } -/// Get footer text #[must_use] pub fn footer_text() -> String { branding() @@ -308,7 +269,6 @@ pub fn footer_text() -> String { .unwrap_or_else(|| format!("Powered by {}", platform_name())) } -/// Format a log prefix with platform branding #[must_use] pub fn log_prefix() -> String { format!("[{}]", platform_short()) diff --git a/src/http_client.rs b/src/http_client.rs index 5cc00fb..33f5dbc 100644 --- a/src/http_client.rs +++ b/src/http_client.rs @@ -1,4 +1,3 @@ -//! HTTP client for botserver communication. use crate::error::BotError; use log::{debug, error}; @@ -9,7 +8,6 @@ use std::time::Duration; const DEFAULT_BOTSERVER_URL: &str = "https://localhost:8088"; const DEFAULT_TIMEOUT_SECS: u64 = 30; -/// HTTP client for communicating with the botserver. #[derive(Clone)] pub struct BotServerClient { client: Arc, @@ -17,19 +15,11 @@ pub struct BotServerClient { } impl BotServerClient { - /// Creates a new client with an optional base URL. - /// - /// Uses `BOTSERVER_URL` environment variable if no URL is provided, - /// or falls back to the default localhost URL. #[must_use] pub fn new(base_url: Option) -> Self { Self::with_timeout(base_url, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) } - /// Creates a new client with a custom timeout. - /// - /// Uses `BOTSERVER_URL` environment variable if no URL is provided, - /// or falls back to the default localhost URL. #[must_use] pub fn with_timeout(base_url: Option, timeout: Duration) -> Self { let url = base_url.unwrap_or_else(|| { @@ -49,17 +39,13 @@ impl BotServerClient { } } - /// Returns the base URL of the client. #[must_use] pub fn base_url(&self) -> &str { &self.base_url } - /// Performs a GET request to the specified endpoint. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn get(&self, endpoint: &str) -> Result { let url = format!("{}{endpoint}", self.base_url); debug!("GET {url}"); @@ -68,11 +54,8 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs a POST request with a JSON body. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn post( &self, endpoint: &str, @@ -85,11 +68,8 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs a PUT request with a JSON body. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn put( &self, endpoint: &str, @@ -102,11 +82,8 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs a PATCH request with a JSON body. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn patch( &self, endpoint: &str, @@ -119,11 +96,8 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs a DELETE request to the specified endpoint. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn delete(&self, endpoint: &str) -> Result { let url = format!("{}{endpoint}", self.base_url); debug!("DELETE {url}"); @@ -132,11 +106,8 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs an authorized GET request with a bearer token. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn get_authorized( &self, endpoint: &str, @@ -149,11 +120,8 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs an authorized POST request with a bearer token and JSON body. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn post_authorized( &self, endpoint: &str, @@ -173,11 +141,8 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs an authorized DELETE request with a bearer token. - /// /// # Errors - /// - /// Returns an error if the request fails or the response cannot be parsed. + /// Returns `BotError` if the HTTP request fails or response parsing fails. pub async fn delete_authorized( &self, endpoint: &str, @@ -190,9 +155,6 @@ impl BotServerClient { self.handle_response(response).await } - /// Performs a health check against the server. - /// - /// Returns `true` if the server is healthy, `false` otherwise. pub async fn health_check(&self) -> bool { match self.get::("/health").await { Ok(_) => true, diff --git a/src/lib.rs b/src/lib.rs index c8d9a13..ef41f92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,3 @@ -//! `BotLib` - Shared library for General Bots -//! -//! This crate provides common types, utilities, and abstractions -//! shared between botserver and botui. -//! -//! # Features -//! - `database` - Database connection utilities (diesel) -//! - `http-client` - HTTP client for API calls -//! - `validation` - Request validation derive macros pub mod branding; pub mod error; @@ -16,7 +7,6 @@ pub mod message_types; pub mod models; pub mod version; -// Re-exports for convenience pub use branding::{ branding, init_branding, is_white_label, platform_name, platform_short, BrandingConfig, }; diff --git a/src/message_types.rs b/src/message_types.rs index f9d0ebd..72e06e9 100644 --- a/src/message_types.rs +++ b/src/message_types.rs @@ -1,31 +1,21 @@ -//! Message type definitions -//! -//! Defines the different types of messages in the bot system. use serde::{Deserialize, Serialize}; -/// Enum representing different types of messages in the bot system #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct MessageType(pub i32); impl MessageType { - /// Regular message from external systems (`WhatsApp`, Instagram, etc.) pub const EXTERNAL: Self = Self(0); - /// User message from web interface pub const USER: Self = Self(1); - /// Bot response (can be regular content or event) pub const BOT_RESPONSE: Self = Self(2); - /// Continue interrupted response pub const CONTINUE: Self = Self(3); - /// Suggestion or command message pub const SUGGESTION: Self = Self(4); - /// Context change notification pub const CONTEXT_CHANGE: Self = Self(5); } diff --git a/src/models.rs b/src/models.rs index 805a60b..32f3af1 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,3 @@ -//! API models for bot communication. use crate::message_types::MessageType; use chrono::{DateTime, Utc}; @@ -119,7 +118,7 @@ impl Session { } #[must_use] - pub fn with_expiry(mut self, expires_at: DateTime) -> Self { + pub const fn with_expiry(mut self, expires_at: DateTime) -> Self { self.expires_at = Some(expires_at); self } @@ -346,7 +345,7 @@ impl BotResponse { } #[must_use] - pub fn complete(mut self) -> Self { + pub const fn complete(mut self) -> Self { self.is_complete = true; self } @@ -357,7 +356,7 @@ impl BotResponse { } #[must_use] - pub fn has_suggestions(&self) -> bool { + pub const fn has_suggestions(&self) -> bool { !self.suggestions.is_empty() } } diff --git a/src/resilience.rs b/src/resilience.rs index 9f34290..6fec0ef 100644 --- a/src/resilience.rs +++ b/src/resilience.rs @@ -1,17 +1,3 @@ -//! Resilience Module - Production-grade fault tolerance primitives -//! -//! This module provides battle-tested resilience patterns: -//! - Retry with exponential backoff and jitter -//! - Circuit breaker with half-open state -//! - Timeout wrappers -//! - Bulkhead isolation -//! - Fallback chains -//! -//! # Design Principles -//! - Zero-cost abstractions where possible -//! - No panics - all errors are recoverable -//! - Composable patterns -//! - Observable state for metrics use std::future::Future; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; @@ -20,21 +6,15 @@ use std::time::{Duration, Instant}; use tokio::sync::{RwLock, Semaphore, SemaphorePermit}; use tokio::time::{sleep, timeout}; -/// Errors that can occur during resilient operations #[derive(Debug, Clone)] pub enum ResilienceError { - /// Operation timed out Timeout { duration: Duration }, - /// Circuit breaker is open, rejecting requests CircuitOpen { until: Option }, - /// All retry attempts exhausted RetriesExhausted { attempts: u32, last_error: String, }, - /// Bulkhead rejected request (too many concurrent) BulkheadFull { max_concurrent: usize }, - /// Wrapped error from the underlying operation Operation(String), } @@ -75,24 +55,14 @@ impl std::fmt::Display for ResilienceError { impl std::error::Error for ResilienceError {} -// ============================================================================ -// Retry Configuration and Execution -// ============================================================================ -/// Retry strategy configuration #[derive(Debug, Clone)] pub struct RetryConfig { - /// Maximum number of attempts (including the first one) pub max_attempts: u32, - /// Initial delay between retries pub initial_delay: Duration, - /// Maximum delay between retries pub max_delay: Duration, - /// Multiplier for exponential backoff (typically 2.0) pub backoff_multiplier: f64, - /// Add random jitter to prevent thundering herd (0.0 to 1.0) pub jitter_factor: f64, - /// Predicate to determine if error is retryable retryable: Option bool + Send + Sync>>, } @@ -110,37 +80,31 @@ impl Default for RetryConfig { } impl RetryConfig { - /// Create a new retry config with custom max attempts pub fn with_max_attempts(mut self, attempts: u32) -> Self { self.max_attempts = attempts.max(1); self } - /// Set initial delay pub fn with_initial_delay(mut self, delay: Duration) -> Self { self.initial_delay = delay; self } - /// Set maximum delay cap pub fn with_max_delay(mut self, delay: Duration) -> Self { self.max_delay = delay; self } - /// Set backoff multiplier pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self { self.backoff_multiplier = multiplier.max(1.0); self } - /// Set jitter factor (0.0 to 1.0) pub fn with_jitter(mut self, jitter: f64) -> Self { self.jitter_factor = jitter.clamp(0.0, 1.0); self } - /// Set custom retryable predicate pub fn with_retryable(mut self, predicate: F) -> Self where F: Fn(&str) -> bool + Send + Sync + 'static, @@ -149,7 +113,6 @@ impl RetryConfig { self } - /// Aggressive retry for critical operations pub fn aggressive() -> Self { Self { max_attempts: 5, @@ -161,7 +124,6 @@ impl RetryConfig { } } - /// Conservative retry for non-critical operations pub fn conservative() -> Self { Self { max_attempts: 2, @@ -173,17 +135,14 @@ impl RetryConfig { } } - /// Calculate delay for a given attempt number fn calculate_delay(&self, attempt: u32) -> Duration { let base_delay = self.initial_delay.as_secs_f64() * self.backoff_multiplier.powi(attempt.saturating_sub(1) as i32); let capped_delay = base_delay.min(self.max_delay.as_secs_f64()); - // Add jitter let jitter = if self.jitter_factor > 0.0 { let jitter_range = capped_delay * self.jitter_factor; - // Simple deterministic "random" based on attempt number let pseudo_random = ((attempt as f64 * 1.618033988749895) % 1.0) * 2.0 - 1.0; jitter_range * pseudo_random } else { @@ -193,9 +152,7 @@ impl RetryConfig { Duration::from_secs_f64((capped_delay + jitter).max(0.001)) } - /// Check if an error is retryable fn is_retryable(&self, error: &str) -> bool { if let Some(ref predicate) = self.retryable { predicate(error) } else { - // \ No newline at end of file diff --git a/src/version.rs b/src/version.rs index 241c81e..4578d94 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,6 +1,3 @@ -//! Version Tracking Module -//! -//! Tracks versions of all components and checks for updates. use chrono::{DateTime, Utc}; use log::debug; @@ -8,35 +5,23 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::RwLock; -/// Global version registry static VERSION_REGISTRY: RwLock> = RwLock::new(None); -/// Current botserver version from Cargo.toml pub const BOTSERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const BOTSERVER_NAME: &str = env!("CARGO_PKG_NAME"); -/// Component version information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComponentVersion { - /// Component name pub name: String, - /// Current installed version pub version: String, - /// Latest available version (if known) pub latest_version: Option, - /// Whether an update is available pub update_available: bool, - /// Component status pub status: ComponentStatus, - /// Last check time pub last_checked: Option>, - /// Source/origin of the component pub source: ComponentSource, - /// Additional metadata pub metadata: HashMap, } -/// Component status #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum ComponentStatus { Running, @@ -60,7 +45,6 @@ impl std::fmt::Display for ComponentStatus { } } -/// Component source type #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum ComponentSource { Builtin, @@ -84,7 +68,6 @@ impl std::fmt::Display for ComponentSource { } } -/// Version registry holding all component versions #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VersionRegistry { pub core_version: String, @@ -105,7 +88,6 @@ impl Default for VersionRegistry { } impl VersionRegistry { - /// Create a new version registry #[must_use] pub fn new() -> Self { let mut registry = Self::default(); @@ -113,7 +95,6 @@ impl VersionRegistry { registry } - /// Register built-in components fn register_builtin_components(&mut self) { self.register_component(ComponentVersion { name: "botserver".to_string(), @@ -161,7 +142,6 @@ impl VersionRegistry { }); } - /// Register a component pub fn register_component(&mut self, component: ComponentVersion) { debug!( "Registered component: {} v{}", @@ -170,14 +150,12 @@ impl VersionRegistry { self.components.insert(component.name.clone(), component); } - /// Update component status pub fn update_status(&mut self, name: &str, status: ComponentStatus) { if let Some(component) = self.components.get_mut(name) { component.status = status; } } - /// Update component version pub fn update_version(&mut self, name: &str, version: String) { if let Some(component) = self.components.get_mut(name) { component.version = version; @@ -185,19 +163,16 @@ impl VersionRegistry { } } - /// Get component by name #[must_use] pub fn get_component(&self, name: &str) -> Option<&ComponentVersion> { self.components.get(name) } - /// Get all components #[must_use] pub const fn get_all_components(&self) -> &HashMap { &self.components } - /// Get components with available updates #[must_use] pub fn get_available_updates(&self) -> Vec<&ComponentVersion> { self.components @@ -206,7 +181,6 @@ impl VersionRegistry { .collect() } - /// Get summary of all components #[must_use] pub fn summary(&self) -> String { let running = self @@ -223,19 +197,14 @@ impl VersionRegistry { ) } - /// Get summary as JSON - /// /// # Errors - /// - /// Returns an error if JSON serialization fails. + /// Returns `serde_json::Error` if serialization fails. pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self) } } -// Global Access Functions -/// Initialize version registry at startup pub fn init_version_registry() { let registry = VersionRegistry::new(); if let Ok(mut guard) = VERSION_REGISTRY.write() { @@ -243,19 +212,16 @@ pub fn init_version_registry() { } } -/// Get version registry (read-only) #[must_use] pub fn version_registry() -> Option { VERSION_REGISTRY.read().ok()?.clone() } -/// Get mutable version registry pub fn version_registry_mut( ) -> Option>> { VERSION_REGISTRY.write().ok() } -/// Register a component pub fn register_component(component: ComponentVersion) { if let Ok(mut guard) = VERSION_REGISTRY.write() { if let Some(ref mut registry) = *guard { @@ -264,7 +230,6 @@ pub fn register_component(component: ComponentVersion) { } } -/// Update component status pub fn update_component_status(name: &str, status: ComponentStatus) { if let Ok(mut guard) = VERSION_REGISTRY.write() { if let Some(ref mut registry) = *guard { @@ -273,7 +238,6 @@ pub fn update_component_status(name: &str, status: ComponentStatus) { } } -/// Get component version #[must_use] pub fn get_component_version(name: &str) -> Option { VERSION_REGISTRY @@ -284,13 +248,11 @@ pub fn get_component_version(name: &str) -> Option { .cloned() } -/// Get botserver version #[must_use] pub const fn get_botserver_version() -> &'static str { BOTSERVER_VERSION } -/// Get version string for display #[must_use] pub fn version_string() -> String { format!("{BOTSERVER_NAME} v{BOTSERVER_VERSION}")