Update library components and models

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-23 15:52:20 -03:00
parent 2de330dbe9
commit b430866dbf
8 changed files with 14 additions and 212 deletions

View file

@ -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

View file

@ -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<BrandingConfig> = 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<String>,
/// Domain for URLs and emails
pub domain: Option<String>,
/// Support email
pub support_email: Option<String>,
/// Logo URL (for web UI)
pub logo_url: Option<String>,
/// Favicon URL
pub favicon_url: Option<String>,
/// Primary color (hex)
pub primary_color: Option<String>,
/// Secondary color (hex)
pub secondary_color: Option<String>,
/// Footer text
pub footer_text: Option<String>,
/// Copyright text
pub copyright: Option<String>,
/// Custom CSS URL
pub custom_css: Option<String>,
/// Terms of service URL
pub terms_url: Option<String>,
/// Privacy policy URL
pub privacy_url: Option<String>,
/// Documentation URL
pub docs_url: Option<String>,
/// 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<Self, Box<dyn std::error::Error>> {
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::<ProductFile>(&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<ProductFile> 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())

View file

@ -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<reqwest::Client>,
@ -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<String>) -> 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<String>, 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<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
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<T: Serialize + Send + Sync, R: DeserializeOwned>(
&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<T: Serialize + Send + Sync, R: DeserializeOwned>(
&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<T: Serialize + Send + Sync, R: DeserializeOwned>(
&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<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
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<T: DeserializeOwned>(
&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<T: Serialize + Send + Sync, R: DeserializeOwned>(
&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<T: DeserializeOwned>(
&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::<serde_json::Value>("/health").await {
Ok(_) => true,

View file

@ -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,
};

View file

@ -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);
}

View file

@ -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<Utc>) -> Self {
pub const fn with_expiry(mut self, expires_at: DateTime<Utc>) -> 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()
}
}

View file

@ -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<Duration> },
/// 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<Arc<dyn Fn(&str) -> 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<F>(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 {
//

View file

@ -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<Option<VersionRegistry>> = 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<String>,
/// Whether an update is available
pub update_available: bool,
/// Component status
pub status: ComponentStatus,
/// Last check time
pub last_checked: Option<DateTime<Utc>>,
/// Source/origin of the component
pub source: ComponentSource,
/// Additional metadata
pub metadata: HashMap<String, String>,
}
/// 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<String, ComponentVersion> {
&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<String, serde_json::Error> {
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<VersionRegistry> {
VERSION_REGISTRY.read().ok()?.clone()
}
/// Get mutable version registry
pub fn version_registry_mut(
) -> Option<std::sync::RwLockWriteGuard<'static, Option<VersionRegistry>>> {
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<ComponentVersion> {
VERSION_REGISTRY
@ -284,13 +248,11 @@ pub fn get_component_version(name: &str) -> Option<ComponentVersion> {
.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}")