Update library components and models
This commit is contained in:
parent
2de330dbe9
commit
b430866dbf
8 changed files with 14 additions and 212 deletions
22
Cargo.toml
22
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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
10
src/lib.rs
10
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
//
|
||||
|
|
@ -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}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue