Update botlib
This commit is contained in:
parent
7928c0ef14
commit
2de330dbe9
9 changed files with 368 additions and 331 deletions
23
Cargo.toml
23
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"
|
||||
|
|
|
|||
362
PROMPT.md
362
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<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
### 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<T> = std::result::Result<T, BotLibError>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- **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.
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
35
src/error.rs
35
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<reqwest::Error> 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(),
|
||||
|
|
|
|||
|
|
@ -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<reqwest::Client>,
|
||||
|
|
@ -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<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(|| {
|
||||
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<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
|
||||
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<T: Serialize, R: DeserializeOwned>(
|
||||
/// 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<T: Serialize + Send + Sync, R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
body: &T,
|
||||
) -> Result<R, BotError> {
|
||||
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<T: Serialize, R: DeserializeOwned>(
|
||||
/// 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<T: Serialize + Send + Sync, R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
body: &T,
|
||||
) -> Result<R, BotError> {
|
||||
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<T: Serialize, R: DeserializeOwned>(
|
||||
/// 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<T: Serialize + Send + Sync, R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
body: &T,
|
||||
) -> Result<R, BotError> {
|
||||
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<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
|
||||
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<T: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
token: &str,
|
||||
) -> Result<T, BotError> {
|
||||
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<T: Serialize, R: DeserializeOwned>(
|
||||
/// 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<T: Serialize + Send + Sync, R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
body: &T,
|
||||
token: &str,
|
||||
) -> Result<R, BotError> {
|
||||
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<T: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
token: &str,
|
||||
) -> Result<T, BotError> {
|
||||
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::<serde_json::Value>("/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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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<i32> for MessageType {
|
||||
fn from(value: i32) -> Self {
|
||||
MessageType(value)
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ impl From<MessageType> 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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<T> {
|
|||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
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<T> ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
|
|
@ -37,6 +41,7 @@ impl<T> ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
|
|
@ -47,6 +52,7 @@ impl<T> ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_with_code(message: impl Into<String>, code: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
|
|
@ -57,6 +63,7 @@ impl<T> ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> ApiResponse<U> {
|
||||
ApiResponse {
|
||||
success: self.success,
|
||||
|
|
@ -67,11 +74,13 @@ impl<T> ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
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<String>) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
|
|
@ -108,19 +118,23 @@ impl Session {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_expiry(mut self, expires_at: DateTime<Utc>) -> 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<chrono::Duration> {
|
||||
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<String>,
|
||||
user_id: impl Into<String>,
|
||||
|
|
@ -162,17 +177,20 @@ impl UserMessage {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_media(mut self, url: impl Into<String>) -> Self {
|
||||
self.media_url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_context(mut self, context: impl Into<String>) -> 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<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
|
|
@ -198,16 +217,19 @@ impl Suggestion {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_context(mut self, context: impl Into<String>) -> Self {
|
||||
self.context = Some(context.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_action(mut self, action: impl Into<String>) -> Self {
|
||||
self.action = Some(action.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_icon(mut self, icon: impl Into<String>) -> 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<String>,
|
||||
session_id: impl Into<String>,
|
||||
|
|
@ -265,6 +288,7 @@ impl BotResponse {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn streaming(
|
||||
bot_id: impl Into<String>,
|
||||
session_id: impl Into<String>,
|
||||
|
|
@ -288,6 +312,7 @@ impl BotResponse {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_suggestions<I, S>(mut self, suggestions: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
|
|
@ -297,11 +322,13 @@ impl BotResponse {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn add_suggestion(mut self, suggestion: impl Into<Suggestion>) -> Self {
|
||||
self.suggestions.push(suggestion.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_context(
|
||||
mut self,
|
||||
name: impl Into<String>,
|
||||
|
|
@ -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<String>) -> Self {
|
||||
Self {
|
||||
attachment_type,
|
||||
|
|
@ -387,51 +418,62 @@ impl Attachment {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn image(url: impl Into<String>) -> Self {
|
||||
Self::new(AttachmentType::Image, url)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn audio(url: impl Into<String>) -> Self {
|
||||
Self::new(AttachmentType::Audio, url)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn video(url: impl Into<String>) -> Self {
|
||||
Self::new(AttachmentType::Video, url)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn document(url: impl Into<String>) -> Self {
|
||||
Self::new(AttachmentType::Document, url)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn file(url: impl Into<String>) -> Self {
|
||||
Self::new(AttachmentType::File, url)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
||||
self.mime_type = Some(mime_type.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_filename(mut self, filename: impl Into<String>) -> 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<String>) -> 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
|
||||
|
|
|
|||
|
|
@ -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<String, ComponentVersion> {
|
||||
#[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
|
||||
.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<String, serde_json::Error> {
|
||||
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<VersionRegistry> {
|
||||
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<ComponentVersion> {
|
||||
VERSION_REGISTRY
|
||||
.read()
|
||||
|
|
@ -274,13 +285,15 @@ pub fn get_component_version(name: &str) -> Option<ComponentVersion> {
|
|||
}
|
||||
|
||||
/// 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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue