From 2de330dbe91ff03142c197c4be8980e5222677f0 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 21 Dec 2025 23:40:41 -0300 Subject: [PATCH] Update botlib --- Cargo.toml | 23 +++ PROMPT.md | 362 +++++++++++++++---------------------------- src/branding.rs | 24 ++- src/error.rs | 35 +++-- src/http_client.rs | 112 +++++++++---- src/lib.rs | 6 +- src/message_types.rs | 20 +-- src/models.rs | 62 ++++++-- src/version.rs | 55 ++++--- 9 files changed, 368 insertions(+), 331 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af0e81c..2317891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ description = "Shared library for General Bots - common types, utilities, and HT license = "AGPL-3.0" authors = ["Pragmatismo.com.br", "General Bots Community"] repository = "https://github.com/GeneralBots/BotServer" +keywords = ["bot", "chatbot", "ai", "conversational", "library"] +categories = ["api-bindings", "web-programming"] [features] default = [] @@ -36,3 +38,24 @@ 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" diff --git a/PROMPT.md b/PROMPT.md index c37e32a..737794e 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -5,135 +5,137 @@ --- -## Version Management - CRITICAL +## ZERO TOLERANCE POLICY -**Current version is 6.1.0 - DO NOT CHANGE without explicit approval!** +**This project has the strictest code quality requirements possible.** -### Rules - -1. **Version is 6.1.0 across ALL workspace crates** -2. **NEVER change version without explicit user approval** -3. **All workspace crates share version 6.1.0** -4. **BotLib does not have migrations - all migrations are in botserver/** +**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.** --- -## Official Icons - Reference +## ABSOLUTE PROHIBITIONS -**BotLib does not contain icons.** Icons are managed in: -- `botui/ui/suite/assets/icons/` - Runtime UI icons -- `botbook/src/assets/icons/` - Documentation icons - -When documenting or referencing UI elements in BotLib: -- Reference icons by name (e.g., `gb-chat.svg`, `gb-drive.svg`) -- Never generate or embed icon content -- See `botui/PROMPT.md` for the complete icon list +``` +❌ NEVER use #![allow()] or #[allow()] in source code to silence warnings +❌ NEVER use _ prefix for unused variables - DELETE the variable or USE it +❌ NEVER use .unwrap() - use ? or proper error handling +❌ NEVER use .expect() - use ? or proper error handling +❌ NEVER use panic!() or unreachable!() - handle all cases +❌ NEVER use todo!() or unimplemented!() - write real code +❌ NEVER leave unused imports - DELETE them +❌ NEVER leave dead code - DELETE it or IMPLEMENT it +❌ NEVER use approximate constants (3.14159) - use std::f64::consts::PI +❌ NEVER silence clippy in code - FIX THE CODE or configure in Cargo.toml +❌ NEVER add comments explaining what code does - code must be self-documenting +``` --- -## Weekly Maintenance - EVERY MONDAY +## CARGO.TOML LINT EXCEPTIONS -### Package Review Checklist +When a clippy lint has **technical false positives** that cannot be fixed in code, +disable it in `Cargo.toml` with a comment explaining why: -**Every Monday, review the following:** +```toml +[lints.clippy] +# Disabled: has false positives for functions with mut self, heap types (Vec, String) +missing_const_for_fn = "allow" +# Disabled: Tauri commands require owned types (Window) that cannot be passed by reference +needless_pass_by_value = "allow" +# Disabled: transitive dependencies we cannot control +multiple_crate_versions = "allow" +``` -1. **Dependency Updates** - ```bash - cargo outdated - cargo audit - ``` +**Approved exceptions:** +- `missing_const_for_fn` - false positives for `mut self`, heap types +- `needless_pass_by_value` - Tauri/framework requirements +- `multiple_crate_versions` - transitive dependencies +- `future_not_send` - when async traits require non-Send futures -2. **Package Consolidation Opportunities** - - Check if new crates can replace custom code - - Look for crates that combine multiple dependencies - - Review `Cargo.toml` for redundant dependencies +--- -3. **Code Reduction Candidates** - - Custom implementations that now have crate equivalents - - Boilerplate that can be replaced with derive macros - - Re-exports that can simplify downstream usage +## MANDATORY CODE PATTERNS -4. **Feature Flag Review** - - Check if optional features are still needed - - Consolidate similar features - - Remove unused feature gates +### Error Handling - Use `?` Operator -### Packages to Watch +```rust +// ❌ WRONG +let value = something.unwrap(); +let value = something.expect("msg"); -| Area | Potential Packages | Purpose | -|------|-------------------|---------| -| Error Handling | `anyhow`, `thiserror` | Consolidate error types | -| Validation | `validator` | Replace manual validation | -| Serialization | `serde` derives | Reduce boilerplate | -| UUID | `uuid` | Consistent ID generation | +// ✅ CORRECT +let value = something?; +let value = something.ok_or_else(|| Error::NotFound)?; +``` + +### Self Usage in Impl Blocks + +```rust +// ❌ WRONG +impl MyStruct { + fn new() -> MyStruct { MyStruct { } } +} + +// ✅ CORRECT +impl MyStruct { + fn new() -> Self { Self { } } +} +``` + +### Format Strings - Inline Variables + +```rust +// ❌ WRONG +format!("Hello {}", name) + +// ✅ CORRECT +format!("Hello {name}") +``` + +### Display vs ToString + +```rust +// ❌ WRONG +impl ToString for MyType { } + +// ✅ CORRECT +impl std::fmt::Display for MyType { } +``` + +### Derive Eq with PartialEq + +```rust +// ❌ WRONG +#[derive(PartialEq)] +struct MyStruct { } + +// ✅ CORRECT +#[derive(PartialEq, Eq)] +struct MyStruct { } +``` + +--- + +## Version Management + +**Version is 6.1.0 - NEVER CHANGE without explicit approval** --- ## Project Overview -BotLib is the shared foundation library for the General Bots workspace. It provides common types, utilities, error handling, and optional integrations that are consumed by botserver, botui, and botapp. - -### Workspace Position +BotLib is the shared foundation library for the General Bots workspace. ``` botlib/ # THIS PROJECT - Shared library botserver/ # Main server (depends on botlib) botui/ # Web/Desktop UI (depends on botlib) botapp/ # Desktop app (depends on botlib) -botbook/ # Documentation -``` - -### What BotLib Provides - -- **Error Types**: Common error handling with anyhow/thiserror -- **Models**: Shared data structures and types -- **HTTP Client**: Optional reqwest wrapper -- **Database**: Optional diesel integration -- **Validation**: Optional input validation -- **Branding**: Version and branding constants - ---- - -## Feature Flags - -```toml -[features] -default = [] -full = ["database", "http-client", "validation"] -database = ["dep:diesel"] -http-client = ["dep:reqwest"] -validation = ["dep:validator"] -``` - -### Usage in Dependent Crates - -```toml -# botserver/Cargo.toml -[dependencies.botlib] -path = "../botlib" -features = ["database"] - -# botui/Cargo.toml -[dependencies.botlib] -path = "../botlib" -features = ["http-client"] ``` --- -## Code Generation Rules - -### CRITICAL REQUIREMENTS - -``` -- Library code must be generic and reusable -- No hardcoded values or project-specific logic -- All public APIs must be well-documented -- Feature gates for optional dependencies -- Zero warnings - clean compilation required -``` - -### Module Structure +## Module Structure ``` src/ @@ -148,153 +150,31 @@ src/ --- -## Adding New Features - -### Adding a Shared Type - -```rust -// src/models.rs -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SharedEntity { - pub id: Uuid, - pub name: String, - pub created_at: DateTime, -} -``` - -### Adding a Feature-Gated Module - -```rust -// src/lib.rs -#[cfg(feature = "my-feature")] -pub mod my_module; - -#[cfg(feature = "my-feature")] -pub use my_module::MyType; -``` - -```toml -# Cargo.toml -[features] -my-feature = ["dep:some-crate"] - -[dependencies] -some-crate = { version = "1.0", optional = true } -``` - -### Adding Error Types - -```rust -// src/error.rs -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum BotLibError { - #[error("Configuration error: {0}")] - Config(String), - - #[error("HTTP error: {0}")] - Http(#[from] reqwest::Error), - - #[error("Database error: {0}")] - Database(String), -} - -pub type Result = std::result::Result; -``` - ---- - -## Re-exports Strategy - -BotLib should re-export common dependencies to ensure version consistency: - -```rust -// src/lib.rs -pub use anyhow; -pub use chrono; -pub use serde; -pub use serde_json; -pub use thiserror; -pub use uuid; - -#[cfg(feature = "database")] -pub use diesel; - -#[cfg(feature = "http-client")] -pub use reqwest; -``` - -Consumers then use: - -```rust -use botlib::uuid::Uuid; -use botlib::chrono::Utc; -``` - ---- - ## Dependencies -| Library | Version | Purpose | Optional | -|---------|---------|---------|----------| -| anyhow | 1.0 | Error handling | No | -| thiserror | 2.0 | Error derive | No | -| log | 0.4 | Logging facade | No | -| chrono | 0.4 | Date/time | No | -| serde | 1.0 | Serialization | No | -| serde_json | 1.0 | JSON | No | -| uuid | 1.11 | UUIDs | No | -| toml | 0.8 | Config parsing | No | -| diesel | 2.1 | Database ORM | Yes | -| reqwest | 0.12 | HTTP client | Yes | -| validator | 0.18 | Validation | Yes | +| Library | Version | Purpose | +|---------|---------|---------| +| anyhow | 1.0 | Error handling | +| thiserror | 2.0 | Error derive | +| chrono | 0.4 | Date/time | +| serde | 1.0 | Serialization | +| uuid | 1.11 | UUIDs | +| diesel | 2.1 | Database ORM | +| reqwest | 0.12 | HTTP client | --- -## Testing +## Remember -```bash -# Test all features -cargo test --all-features - -# Test specific feature -cargo test --features database - -# Test without optional features -cargo test -``` - ---- - -## Final Checks Before Commit - -```bash -# Verify version is 6.1.0 -grep "^version" Cargo.toml | grep "6.1.0" - -# Build with all features -cargo build --all-features - -# Check for warnings -cargo check --all-features 2>&1 | grep warning - -# Run tests -cargo test --all-features -``` - ---- - -## Rules - -- Keep botlib minimal and focused -- No business logic - only utilities and types -- Feature gate all optional dependencies -- Maintain backward compatibility -- Document all public APIs -- Target zero warnings -- **Version**: Always 6.1.0 - do not change without approval \ No newline at end of file +- **ZERO WARNINGS** - Every clippy warning must be fixed +- **NO ALLOW IN CODE** - Never use #[allow()] in source files +- **CARGO.TOML EXCEPTIONS OK** - Disable lints with false positives in Cargo.toml with comment +- **NO DEAD CODE** - Delete unused code, never prefix with _ +- **NO UNWRAP/EXPECT** - Use ? operator or proper error handling +- **INLINE FORMAT ARGS** - format!("{name}") not format!("{}", name) +- **USE SELF** - In impl blocks, use Self not the type name +- **DERIVE EQ** - Always derive Eq with PartialEq +- **DISPLAY NOT TOSTRING** - Implement Display, not ToString +- **USE DIAGNOSTICS** - Use IDE diagnostics tool, never call cargo clippy directly +- **Version**: Always 6.1.0 - do not change without approval +- **Session Continuation**: When running out of context, create detailed summary: (1) what was done, (2) what remains, (3) specific files and line numbers, (4) exact next steps. \ No newline at end of file diff --git a/src/branding.rs b/src/branding.rs index 7b64bec..e2c8cf0 100644 --- a/src/branding.rs +++ b/src/branding.rs @@ -17,10 +17,10 @@ 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 +/// Branding configuration loaded from `.product` file #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BrandingConfig { - /// Platform name (e.g., "MyCustomPlatform") + /// Platform name (e.g., "`MyCustomPlatform`") pub name: String, /// Short name for logs and compact displays (e.g., "MCP") pub short_name: String, @@ -82,6 +82,7 @@ impl Default for BrandingConfig { impl BrandingConfig { /// Load branding from .product file if it exists + #[must_use] pub fn load() -> Self { let search_paths = [ ".product", @@ -92,7 +93,7 @@ impl BrandingConfig { for path in &search_paths { if let Ok(config) = Self::load_from_file(path) { - info!("Loaded white-label branding from {}: {}", path, config.name); + info!("Loaded white-label branding from {path}: {}", config.name); return config; } } @@ -101,8 +102,8 @@ impl BrandingConfig { if let Ok(product_file) = std::env::var("PRODUCT_FILE") { if let Ok(config) = Self::load_from_file(&product_file) { info!( - "Loaded white-label branding from PRODUCT_FILE={}: {}", - product_file, config.name + "Loaded white-label branding from PRODUCT_FILE={product_file}: {}", + config.name ); return config; } @@ -149,8 +150,10 @@ impl BrandingConfig { } // Try parsing as simple key=value format - let mut config = Self::default(); - config.is_white_label = true; + let mut config = Self { + is_white_label: true, + ..Self::default() + }; for line in content.lines() { let line = line.trim(); @@ -261,26 +264,31 @@ pub fn init_branding() { } /// 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(|| { format!( @@ -292,6 +300,7 @@ pub fn copyright_text() -> String { } /// Get footer text +#[must_use] pub fn footer_text() -> String { branding() .footer_text @@ -300,6 +309,7 @@ pub fn footer_text() -> String { } /// Format a log prefix with platform branding +#[must_use] pub fn log_prefix() -> String { format!("[{}]", platform_short()) } diff --git a/src/error.rs b/src/error.rs index 594ddd6..3c0ef61 100644 --- a/src/error.rs +++ b/src/error.rs @@ -88,7 +88,8 @@ impl BotError { Self::Conflict(msg.into()) } - pub fn rate_limited(retry_after_secs: u64) -> Self { + #[must_use] + pub const fn rate_limited(retry_after_secs: u64) -> Self { Self::RateLimited { retry_after_secs } } @@ -96,7 +97,8 @@ impl BotError { Self::ServiceUnavailable(msg.into()) } - pub fn timeout(duration_ms: u64) -> Self { + #[must_use] + pub const fn timeout(duration_ms: u64) -> Self { Self::Timeout { duration_ms } } @@ -104,26 +106,27 @@ impl BotError { Self::Internal(msg.into()) } - pub fn status_code(&self) -> u16 { + #[must_use] + pub const fn status_code(&self) -> u16 { match self { - Self::Config(_) => 500, - Self::Database(_) => 500, Self::Http { status, .. } => *status, Self::Auth(_) => 401, - Self::Validation(_) => 400, + Self::Validation(_) | Self::Json(_) => 400, Self::NotFound { .. } => 404, Self::Conflict(_) => 409, Self::RateLimited { .. } => 429, Self::ServiceUnavailable(_) => 503, Self::Timeout { .. } => 504, - Self::Internal(_) => 500, - Self::Io(_) => 500, - Self::Json(_) => 400, - Self::Other(_) => 500, + Self::Config(_) + | Self::Database(_) + | Self::Internal(_) + | Self::Io(_) + | Self::Other(_) => 500, } } - pub fn is_retryable(&self) -> bool { + #[must_use] + pub const fn is_retryable(&self) -> bool { match self { Self::RateLimited { .. } | Self::ServiceUnavailable(_) | Self::Timeout { .. } => true, Self::Http { status, .. } => *status >= 500, @@ -131,12 +134,14 @@ impl BotError { } } - pub fn is_client_error(&self) -> bool { + #[must_use] + pub const fn is_client_error(&self) -> bool { let code = self.status_code(); - (400..500).contains(&code) + code >= 400 && code < 500 } - pub fn is_server_error(&self) -> bool { + #[must_use] + pub const fn is_server_error(&self) -> bool { self.status_code() >= 500 } } @@ -162,7 +167,7 @@ impl From<&str> for BotError { #[cfg(feature = "http-client")] impl From for BotError { fn from(err: reqwest::Error) -> Self { - let status = err.status().map(|s| s.as_u16()).unwrap_or(500); + let status = err.status().map_or(500, |s| s.as_u16()); Self::Http { status, message: err.to_string(), diff --git a/src/http_client.rs b/src/http_client.rs index 95dff21..5cc00fb 100644 --- a/src/http_client.rs +++ b/src/http_client.rs @@ -1,3 +1,5 @@ +//! HTTP client for botserver communication. + use crate::error::BotError; use log::{debug, error}; use serde::{de::DeserializeOwned, Serialize}; @@ -7,6 +9,7 @@ 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, @@ -14,10 +17,20 @@ 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(|| { std::env::var("BOTSERVER_URL").unwrap_or_else(|_| DEFAULT_BOTSERVER_URL.to_string()) @@ -28,7 +41,7 @@ impl BotServerClient { .user_agent(format!("BotLib/{}", env!("CARGO_PKG_VERSION"))) .danger_accept_invalid_certs(true) .build() - .expect("Failed to create HTTP client"); + .unwrap_or_else(|_| reqwest::Client::new()); Self { client: Arc::new(client), @@ -36,82 +49,119 @@ 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. pub async fn get(&self, endpoint: &str) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("GET {}", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("GET {url}"); let response = self.client.get(&url).send().await?; self.handle_response(response).await } - pub async fn post( + /// Performs a POST request with a JSON body. + /// + /// # Errors + /// + /// Returns an error if the request fails or the response cannot be parsed. + pub async fn post( &self, endpoint: &str, body: &T, ) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("POST {}", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("POST {url}"); let response = self.client.post(&url).json(body).send().await?; self.handle_response(response).await } - pub async fn put( + /// Performs a PUT request with a JSON body. + /// + /// # Errors + /// + /// Returns an error if the request fails or the response cannot be parsed. + pub async fn put( &self, endpoint: &str, body: &T, ) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("PUT {}", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("PUT {url}"); let response = self.client.put(&url).json(body).send().await?; self.handle_response(response).await } - pub async fn patch( + /// Performs a PATCH request with a JSON body. + /// + /// # Errors + /// + /// Returns an error if the request fails or the response cannot be parsed. + pub async fn patch( &self, endpoint: &str, body: &T, ) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("PATCH {}", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("PATCH {url}"); let response = self.client.patch(&url).json(body).send().await?; 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. pub async fn delete(&self, endpoint: &str) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("DELETE {}", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("DELETE {url}"); let response = self.client.delete(&url).send().await?; 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. pub async fn get_authorized( &self, endpoint: &str, token: &str, ) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("GET {} (authorized)", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("GET {url} (authorized)"); let response = self.client.get(&url).bearer_auth(token).send().await?; self.handle_response(response).await } - pub async fn post_authorized( + /// 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. + pub async fn post_authorized( &self, endpoint: &str, body: &T, token: &str, ) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("POST {} (authorized)", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("POST {url} (authorized)"); let response = self .client @@ -123,23 +173,31 @@ 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. pub async fn delete_authorized( &self, endpoint: &str, token: &str, ) -> Result { - let url = format!("{}{}", self.base_url, endpoint); - debug!("DELETE {} (authorized)", url); + let url = format!("{}{endpoint}", self.base_url); + debug!("DELETE {url} (authorized)"); let response = self.client.delete(&url).bearer_auth(token).send().await?; 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, Err(e) => { - error!("Health check failed: {}", e); + error!("Health check failed: {e}"); false } } @@ -157,13 +215,13 @@ impl BotServerClient { .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); - error!("HTTP {} error: {}", status_code, error_text); + error!("HTTP {status_code} error: {error_text}"); return Err(BotError::http(status_code, error_text)); } response.json().await.map_err(|e| { - error!("Failed to parse response: {}", e); - BotError::http(status_code, format!("Failed to parse response: {}", e)) + error!("Failed to parse response: {e}"); + BotError::http(status_code, format!("Failed to parse response: {e}")) }) } } @@ -172,7 +230,7 @@ impl std::fmt::Debug for BotServerClient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BotServerClient") .field("base_url", &self.base_url) - .finish() + .finish_non_exhaustive() } } @@ -212,7 +270,7 @@ mod tests { #[test] fn test_client_debug() { let client = BotServerClient::new(Some("http://debug-test".to_string())); - let debug_str = format!("{:?}", client); + let debug_str = format!("{client:?}"); assert!(debug_str.contains("BotServerClient")); assert!(debug_str.contains("http://debug-test")); } diff --git a/src/lib.rs b/src/lib.rs index 8a83a32..c8d9a13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! BotLib - Shared library for General Bots +//! `BotLib` - Shared library for General Bots //! //! This crate provides common types, utilities, and abstractions //! shared between botserver and botui. @@ -17,7 +17,9 @@ pub mod models; pub mod version; // Re-exports for convenience -pub use branding::{branding, init_branding, is_white_label, platform_name, platform_short, BrandingConfig}; +pub use branding::{ + branding, init_branding, is_white_label, platform_name, platform_short, BrandingConfig, +}; pub use error::{BotError, BotResult}; pub use message_types::MessageType; pub use models::{ApiResponse, BotResponse, Session, Suggestion, UserMessage}; diff --git a/src/message_types.rs b/src/message_types.rs index 23d9d45..f9d0ebd 100644 --- a/src/message_types.rs +++ b/src/message_types.rs @@ -10,28 +10,28 @@ use serde::{Deserialize, Serialize}; pub struct MessageType(pub i32); impl MessageType { - /// Regular message from external systems (WhatsApp, Instagram, etc.) - pub const EXTERNAL: MessageType = MessageType(0); + /// Regular message from external systems (`WhatsApp`, Instagram, etc.) + pub const EXTERNAL: Self = Self(0); /// User message from web interface - pub const USER: MessageType = MessageType(1); + pub const USER: Self = Self(1); /// Bot response (can be regular content or event) - pub const BOT_RESPONSE: MessageType = MessageType(2); + pub const BOT_RESPONSE: Self = Self(2); /// Continue interrupted response - pub const CONTINUE: MessageType = MessageType(3); + pub const CONTINUE: Self = Self(3); /// Suggestion or command message - pub const SUGGESTION: MessageType = MessageType(4); + pub const SUGGESTION: Self = Self(4); /// Context change notification - pub const CONTEXT_CHANGE: MessageType = MessageType(5); + pub const CONTEXT_CHANGE: Self = Self(5); } impl From for MessageType { fn from(value: i32) -> Self { - MessageType(value) + Self(value) } } @@ -43,7 +43,7 @@ impl From for i32 { impl Default for MessageType { fn default() -> Self { - MessageType::USER + Self::USER } } @@ -58,7 +58,7 @@ impl std::fmt::Display for MessageType { 5 => "CONTEXT_CHANGE", _ => "UNKNOWN", }; - write!(f, "{}", name) + write!(f, "{name}") } } diff --git a/src/models.rs b/src/models.rs index 54842cb..805a60b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,3 +1,5 @@ +//! API models for bot communication. + use crate::message_types::MessageType; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -17,7 +19,8 @@ pub struct ApiResponse { } impl ApiResponse { - pub fn success(data: T) -> Self { + #[must_use] + pub const fn success(data: T) -> Self { Self { success: true, data: Some(data), @@ -27,6 +30,7 @@ impl ApiResponse { } } + #[must_use] pub fn success_with_message(data: T, message: impl Into) -> Self { Self { success: true, @@ -37,6 +41,7 @@ impl ApiResponse { } } + #[must_use] pub fn error(message: impl Into) -> Self { Self { success: false, @@ -47,6 +52,7 @@ impl ApiResponse { } } + #[must_use] pub fn error_with_code(message: impl Into, code: impl Into) -> Self { Self { success: false, @@ -57,6 +63,7 @@ impl ApiResponse { } } + #[must_use] pub fn map U>(self, f: F) -> ApiResponse { ApiResponse { success: self.success, @@ -67,11 +74,13 @@ impl ApiResponse { } } - pub fn is_success(&self) -> bool { + #[must_use] + pub const fn is_success(&self) -> bool { self.success } - pub fn is_error(&self) -> bool { + #[must_use] + pub const fn is_error(&self) -> bool { !self.success } } @@ -95,6 +104,7 @@ pub struct Session { } impl Session { + #[must_use] pub fn new(user_id: Uuid, bot_id: Uuid, title: impl Into) -> Self { let now = Utc::now(); Self { @@ -108,19 +118,23 @@ impl Session { } } + #[must_use] pub fn with_expiry(mut self, expires_at: DateTime) -> Self { self.expires_at = Some(expires_at); self } + #[must_use] pub fn is_expired(&self) -> bool { - self.expires_at.map(|exp| Utc::now() > exp).unwrap_or(false) + self.expires_at.is_some_and(|exp| Utc::now() > exp) } + #[must_use] pub fn is_active(&self) -> bool { !self.is_expired() } + #[must_use] pub fn remaining_time(&self) -> Option { self.expires_at.map(|exp| exp - Utc::now()) } @@ -142,6 +156,7 @@ pub struct UserMessage { } impl UserMessage { + #[must_use] pub fn text( bot_id: impl Into, user_id: impl Into, @@ -162,17 +177,20 @@ impl UserMessage { } } + #[must_use] pub fn with_media(mut self, url: impl Into) -> Self { self.media_url = Some(url.into()); self } + #[must_use] pub fn with_context(mut self, context: impl Into) -> Self { self.context_name = Some(context.into()); self } - pub fn has_media(&self) -> bool { + #[must_use] + pub const fn has_media(&self) -> bool { self.media_url.is_some() } } @@ -189,6 +207,7 @@ pub struct Suggestion { } impl Suggestion { + #[must_use] pub fn new(text: impl Into) -> Self { Self { text: text.into(), @@ -198,16 +217,19 @@ impl Suggestion { } } + #[must_use] pub fn with_context(mut self, context: impl Into) -> Self { self.context = Some(context.into()); self } + #[must_use] pub fn with_action(mut self, action: impl Into) -> Self { self.action = Some(action.into()); self } + #[must_use] pub fn with_icon(mut self, icon: impl Into) -> Self { self.icon = Some(icon.into()); self @@ -242,6 +264,7 @@ pub struct BotResponse { } impl BotResponse { + #[must_use] pub fn new( bot_id: impl Into, session_id: impl Into, @@ -265,6 +288,7 @@ impl BotResponse { } } + #[must_use] pub fn streaming( bot_id: impl Into, session_id: impl Into, @@ -288,6 +312,7 @@ impl BotResponse { } } + #[must_use] pub fn with_suggestions(mut self, suggestions: I) -> Self where I: IntoIterator, @@ -297,11 +322,13 @@ impl BotResponse { self } + #[must_use] pub fn add_suggestion(mut self, suggestion: impl Into) -> Self { self.suggestions.push(suggestion.into()); self } + #[must_use] pub fn with_context( mut self, name: impl Into, @@ -318,15 +345,18 @@ impl BotResponse { self.content.push_str(chunk); } + #[must_use] pub fn complete(mut self) -> Self { self.is_complete = true; self } - pub fn is_streaming(&self) -> bool { + #[must_use] + pub const fn is_streaming(&self) -> bool { self.stream_token.is_some() && !self.is_complete } + #[must_use] pub fn has_suggestions(&self) -> bool { !self.suggestions.is_empty() } @@ -376,6 +406,7 @@ pub enum AttachmentType { } impl Attachment { + #[must_use] pub fn new(attachment_type: AttachmentType, url: impl Into) -> Self { Self { attachment_type, @@ -387,51 +418,62 @@ impl Attachment { } } + #[must_use] pub fn image(url: impl Into) -> Self { Self::new(AttachmentType::Image, url) } + #[must_use] pub fn audio(url: impl Into) -> Self { Self::new(AttachmentType::Audio, url) } + #[must_use] pub fn video(url: impl Into) -> Self { Self::new(AttachmentType::Video, url) } + #[must_use] pub fn document(url: impl Into) -> Self { Self::new(AttachmentType::Document, url) } + #[must_use] pub fn file(url: impl Into) -> Self { Self::new(AttachmentType::File, url) } + #[must_use] pub fn with_mime_type(mut self, mime_type: impl Into) -> Self { self.mime_type = Some(mime_type.into()); self } + #[must_use] pub fn with_filename(mut self, filename: impl Into) -> Self { self.filename = Some(filename.into()); self } - pub fn with_size(mut self, size: u64) -> Self { + #[must_use] + pub const fn with_size(mut self, size: u64) -> Self { self.size = Some(size); self } + #[must_use] pub fn with_thumbnail(mut self, thumbnail_url: impl Into) -> Self { self.thumbnail_url = Some(thumbnail_url.into()); self } - pub fn is_image(&self) -> bool { - self.attachment_type == AttachmentType::Image + #[must_use] + pub const fn is_image(&self) -> bool { + matches!(self.attachment_type, AttachmentType::Image) } - pub fn is_media(&self) -> bool { + #[must_use] + pub const fn is_media(&self) -> bool { matches!( self.attachment_type, AttachmentType::Image | AttachmentType::Audio | AttachmentType::Video diff --git a/src/version.rs b/src/version.rs index 53f08df..241c81e 100644 --- a/src/version.rs +++ b/src/version.rs @@ -50,12 +50,12 @@ pub enum ComponentStatus { impl std::fmt::Display for ComponentStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ComponentStatus::Running => write!(f, "[OK] Running"), - ComponentStatus::Stopped => write!(f, "[STOP] Stopped"), - ComponentStatus::Error => write!(f, "[ERR] Error"), - ComponentStatus::Updating => write!(f, "[UPD] Updating"), - ComponentStatus::NotInstalled => write!(f, "[--] Not Installed"), - ComponentStatus::Unknown => write!(f, "[?] Unknown"), + 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"), } } } @@ -74,12 +74,12 @@ pub enum ComponentSource { impl std::fmt::Display for ComponentSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ComponentSource::Builtin => write!(f, "Built-in"), - ComponentSource::Docker => write!(f, "Docker"), - ComponentSource::Lxc => write!(f, "LXC"), - ComponentSource::System => write!(f, "System"), - ComponentSource::Binary => write!(f, "Binary"), - ComponentSource::External => write!(f, "External"), + 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"), } } } @@ -106,6 +106,7 @@ impl Default for VersionRegistry { impl VersionRegistry { /// Create a new version registry + #[must_use] pub fn new() -> Self { let mut registry = Self::default(); registry.register_builtin_components(); @@ -185,16 +186,19 @@ impl VersionRegistry { } /// Get component by name + #[must_use] pub fn get_component(&self, name: &str) -> Option<&ComponentVersion> { self.components.get(name) } /// Get all components - pub fn get_all_components(&self) -> &HashMap { + #[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 .values() @@ -203,6 +207,7 @@ impl VersionRegistry { } /// Get summary of all components + #[must_use] pub fn summary(&self) -> String { let running = self .components @@ -213,12 +218,16 @@ impl VersionRegistry { let updates = self.get_available_updates().len(); format!( - "{} v{} | {}/{} components running | {} updates available", - BOTSERVER_NAME, self.core_version, running, total, updates + "{BOTSERVER_NAME} v{} | {running}/{total} components running | {updates} updates available", + self.core_version ) } /// Get summary as JSON + /// + /// # Errors + /// + /// Returns an error if JSON serialization fails. pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self) } @@ -235,6 +244,7 @@ pub fn init_version_registry() { } /// Get version registry (read-only) +#[must_use] pub fn version_registry() -> Option { VERSION_REGISTRY.read().ok()?.clone() } @@ -264,6 +274,7 @@ pub fn update_component_status(name: &str, status: ComponentStatus) { } /// Get component version +#[must_use] pub fn get_component_version(name: &str) -> Option { VERSION_REGISTRY .read() @@ -274,13 +285,15 @@ pub fn get_component_version(name: &str) -> Option { } /// Get botserver version -pub fn get_botserver_version() -> &'static str { +#[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!("{} v{}", BOTSERVER_NAME, BOTSERVER_VERSION) + format!("{BOTSERVER_NAME} v{BOTSERVER_VERSION}") } #[cfg(test)] @@ -333,8 +346,12 @@ mod tests { fn test_update_status() { let mut registry = VersionRegistry::new(); registry.update_status("botserver", ComponentStatus::Stopped); - let component = registry.get_component("botserver").unwrap(); - assert_eq!(component.status, ComponentStatus::Stopped); + 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)); } #[test]