botlib/src/version.rs

364 lines
10 KiB
Rust
Raw Normal View History

2025-12-03 19:45:09 -03:00
//! Version Tracking Module
//!
//! Tracks versions of all components and checks for updates.
use chrono::{DateTime, Utc};
use log::debug;
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,
Stopped,
Error,
Updating,
NotInstalled,
Unknown,
}
impl std::fmt::Display for ComponentStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
2025-12-21 23:40:41 -03:00
Self::Running => write!(f, "[OK] Running"),
Self::Stopped => write!(f, "[STOP] Stopped"),
Self::Error => write!(f, "[ERR] Error"),
Self::Updating => write!(f, "[UPD] Updating"),
Self::NotInstalled => write!(f, "[--] Not Installed"),
Self::Unknown => write!(f, "[?] Unknown"),
2025-12-03 19:45:09 -03:00
}
}
}
/// Component source type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ComponentSource {
Builtin,
Docker,
Lxc,
System,
Binary,
External,
}
impl std::fmt::Display for ComponentSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
2025-12-21 23:40:41 -03:00
Self::Builtin => write!(f, "Built-in"),
Self::Docker => write!(f, "Docker"),
Self::Lxc => write!(f, "LXC"),
Self::System => write!(f, "System"),
Self::Binary => write!(f, "Binary"),
Self::External => write!(f, "External"),
2025-12-03 19:45:09 -03:00
}
}
}
/// Version registry holding all component versions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionRegistry {
pub core_version: String,
pub components: HashMap<String, ComponentVersion>,
pub last_update_check: Option<DateTime<Utc>>,
pub update_url: Option<String>,
}
impl Default for VersionRegistry {
fn default() -> Self {
Self {
core_version: BOTSERVER_VERSION.to_string(),
components: HashMap::new(),
last_update_check: None,
update_url: Some("https://api.generalbots.com/updates".to_string()),
}
}
}
impl VersionRegistry {
/// Create a new version registry
2025-12-21 23:40:41 -03:00
#[must_use]
2025-12-03 19:45:09 -03:00
pub fn new() -> Self {
let mut registry = Self::default();
registry.register_builtin_components();
registry
}
/// Register built-in components
fn register_builtin_components(&mut self) {
self.register_component(ComponentVersion {
name: "botserver".to_string(),
version: BOTSERVER_VERSION.to_string(),
latest_version: None,
update_available: false,
status: ComponentStatus::Running,
last_checked: Some(Utc::now()),
source: ComponentSource::Builtin,
metadata: HashMap::from([
("description".to_string(), "Core bot server".to_string()),
(
"repo".to_string(),
"https://github.com/GeneralBots/botserver".to_string(),
),
]),
});
self.register_component(ComponentVersion {
name: "basic".to_string(),
version: BOTSERVER_VERSION.to_string(),
latest_version: None,
update_available: false,
status: ComponentStatus::Running,
last_checked: Some(Utc::now()),
source: ComponentSource::Builtin,
metadata: HashMap::from([(
"description".to_string(),
"BASIC script interpreter".to_string(),
)]),
});
self.register_component(ComponentVersion {
name: "llm".to_string(),
version: BOTSERVER_VERSION.to_string(),
latest_version: None,
update_available: false,
status: ComponentStatus::Running,
last_checked: Some(Utc::now()),
source: ComponentSource::Builtin,
metadata: HashMap::from([(
"description".to_string(),
"LLM integration (Claude, GPT, etc.)".to_string(),
)]),
});
}
/// Register a component
pub fn register_component(&mut self, component: ComponentVersion) {
debug!(
"Registered component: {} v{}",
component.name, component.version
);
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;
component.last_checked = Some(Utc::now());
}
}
/// Get component by name
2025-12-21 23:40:41 -03:00
#[must_use]
2025-12-03 19:45:09 -03:00
pub fn get_component(&self, name: &str) -> Option<&ComponentVersion> {
self.components.get(name)
}
/// Get all components
2025-12-21 23:40:41 -03:00
#[must_use]
pub const fn get_all_components(&self) -> &HashMap<String, ComponentVersion> {
2025-12-03 19:45:09 -03:00
&self.components
}
/// Get components with available updates
2025-12-21 23:40:41 -03:00
#[must_use]
2025-12-03 19:45:09 -03:00
pub fn get_available_updates(&self) -> Vec<&ComponentVersion> {
self.components
.values()
.filter(|c| c.update_available)
.collect()
}
/// Get summary of all components
2025-12-21 23:40:41 -03:00
#[must_use]
2025-12-03 19:45:09 -03:00
pub fn summary(&self) -> String {
let running = self
.components
.values()
.filter(|c| c.status == ComponentStatus::Running)
.count();
let total = self.components.len();
let updates = self.get_available_updates().len();
format!(
2025-12-21 23:40:41 -03:00
"{BOTSERVER_NAME} v{} | {running}/{total} components running | {updates} updates available",
self.core_version
2025-12-03 19:45:09 -03:00
)
}
/// Get summary as JSON
2025-12-21 23:40:41 -03:00
///
/// # Errors
///
/// Returns an error if JSON serialization fails.
2025-12-03 19:45:09 -03:00
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() {
*guard = Some(registry);
}
}
/// Get version registry (read-only)
2025-12-21 23:40:41 -03:00
#[must_use]
2025-12-03 19:45:09 -03:00
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 {
registry.register_component(component);
}
}
}
/// 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 {
registry.update_status(name, status);
}
}
}
/// Get component version
2025-12-21 23:40:41 -03:00
#[must_use]
2025-12-03 19:45:09 -03:00
pub fn get_component_version(name: &str) -> Option<ComponentVersion> {
VERSION_REGISTRY
.read()
.ok()?
.as_ref()?
.get_component(name)
.cloned()
}
/// Get botserver version
2025-12-21 23:40:41 -03:00
#[must_use]
pub const fn get_botserver_version() -> &'static str {
2025-12-03 19:45:09 -03:00
BOTSERVER_VERSION
}
/// Get version string for display
2025-12-21 23:40:41 -03:00
#[must_use]
2025-12-03 19:45:09 -03:00
pub fn version_string() -> String {
2025-12-21 23:40:41 -03:00
format!("{BOTSERVER_NAME} v{BOTSERVER_VERSION}")
2025-12-03 19:45:09 -03:00
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_creation() {
let registry = VersionRegistry::new();
assert!(!registry.core_version.is_empty());
assert!(registry.components.contains_key("botserver"));
}
#[test]
fn test_component_registration() {
let mut registry = VersionRegistry::new();
registry.register_component(ComponentVersion {
name: "test".to_string(),
version: "1.0.0".to_string(),
latest_version: None,
update_available: false,
status: ComponentStatus::Running,
last_checked: None,
source: ComponentSource::Builtin,
metadata: HashMap::new(),
});
assert!(registry.get_component("test").is_some());
}
#[test]
fn test_status_display() {
assert_eq!(ComponentStatus::Running.to_string(), "[OK] Running");
assert_eq!(ComponentStatus::Error.to_string(), "[ERR] Error");
2025-12-03 19:45:09 -03:00
}
#[test]
fn test_version_string() {
let vs = version_string();
assert!(!vs.is_empty());
assert!(vs.contains('v'));
}
#[test]
fn test_source_display() {
assert_eq!(ComponentSource::Builtin.to_string(), "Built-in");
assert_eq!(ComponentSource::Docker.to_string(), "Docker");
}
#[test]
fn test_update_status() {
let mut registry = VersionRegistry::new();
registry.update_status("botserver", ComponentStatus::Stopped);
2025-12-21 23:40:41 -03:00
let component = registry.get_component("botserver");
assert!(
component.is_some(),
"botserver component should exist in registry"
);
assert_eq!(component.map(|c| c.status), Some(ComponentStatus::Stopped));
2025-12-03 19:45:09 -03:00
}
#[test]
fn test_summary() {
let registry = VersionRegistry::new();
let summary = registry.summary();
assert!(summary.contains("components running"));
}
}