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"
|
license = "AGPL-3.0"
|
||||||
authors = ["Pragmatismo.com.br", "General Bots Community"]
|
authors = ["Pragmatismo.com.br", "General Bots Community"]
|
||||||
repository = "https://github.com/GeneralBots/BotServer"
|
repository = "https://github.com/GeneralBots/BotServer"
|
||||||
|
keywords = ["bot", "chatbot", "ai", "conversational", "library"]
|
||||||
|
categories = ["api-bindings", "web-programming"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
@ -36,3 +38,24 @@ validator = { version = "0.18", features = ["derive"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.41", features = ["rt", "macros"] }
|
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"
|
||||||
|
|
|
||||||
360
PROMPT.md
360
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
|
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
|
||||||
|
|
||||||
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/**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Official Icons - Reference
|
## ABSOLUTE PROHIBITIONS
|
||||||
|
|
||||||
**BotLib does not contain icons.** Icons are managed in:
|
```
|
||||||
- `botui/ui/suite/assets/icons/` - Runtime UI icons
|
❌ NEVER use #![allow()] or #[allow()] in source code to silence warnings
|
||||||
- `botbook/src/assets/icons/` - Documentation icons
|
❌ NEVER use _ prefix for unused variables - DELETE the variable or USE it
|
||||||
|
❌ NEVER use .unwrap() - use ? or proper error handling
|
||||||
When documenting or referencing UI elements in BotLib:
|
❌ NEVER use .expect() - use ? or proper error handling
|
||||||
- Reference icons by name (e.g., `gb-chat.svg`, `gb-drive.svg`)
|
❌ NEVER use panic!() or unreachable!() - handle all cases
|
||||||
- Never generate or embed icon content
|
❌ NEVER use todo!() or unimplemented!() - write real code
|
||||||
- See `botui/PROMPT.md` for the complete icon list
|
❌ 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**
|
**Approved exceptions:**
|
||||||
```bash
|
- `missing_const_for_fn` - false positives for `mut self`, heap types
|
||||||
cargo outdated
|
- `needless_pass_by_value` - Tauri/framework requirements
|
||||||
cargo audit
|
- `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**
|
## MANDATORY CODE PATTERNS
|
||||||
- Custom implementations that now have crate equivalents
|
|
||||||
- Boilerplate that can be replaced with derive macros
|
|
||||||
- Re-exports that can simplify downstream usage
|
|
||||||
|
|
||||||
4. **Feature Flag Review**
|
### Error Handling - Use `?` Operator
|
||||||
- Check if optional features are still needed
|
|
||||||
- Consolidate similar features
|
|
||||||
- Remove unused feature gates
|
|
||||||
|
|
||||||
### Packages to Watch
|
```rust
|
||||||
|
// ❌ WRONG
|
||||||
|
let value = something.unwrap();
|
||||||
|
let value = something.expect("msg");
|
||||||
|
|
||||||
| Area | Potential Packages | Purpose |
|
// ✅ CORRECT
|
||||||
|------|-------------------|---------|
|
let value = something?;
|
||||||
| Error Handling | `anyhow`, `thiserror` | Consolidate error types |
|
let value = something.ok_or_else(|| Error::NotFound)?;
|
||||||
| Validation | `validator` | Replace manual validation |
|
```
|
||||||
| Serialization | `serde` derives | Reduce boilerplate |
|
|
||||||
| UUID | `uuid` | Consistent ID generation |
|
### 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
|
## 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.
|
BotLib is the shared foundation library for the General Bots workspace.
|
||||||
|
|
||||||
### Workspace Position
|
|
||||||
|
|
||||||
```
|
```
|
||||||
botlib/ # THIS PROJECT - Shared library
|
botlib/ # THIS PROJECT - Shared library
|
||||||
botserver/ # Main server (depends on botlib)
|
botserver/ # Main server (depends on botlib)
|
||||||
botui/ # Web/Desktop UI (depends on botlib)
|
botui/ # Web/Desktop UI (depends on botlib)
|
||||||
botapp/ # Desktop app (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
|
## Module Structure
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
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
|
## Dependencies
|
||||||
|
|
||||||
| Library | Version | Purpose | Optional |
|
| Library | Version | Purpose |
|
||||||
|---------|---------|---------|----------|
|
|---------|---------|---------|
|
||||||
| anyhow | 1.0 | Error handling | No |
|
| anyhow | 1.0 | Error handling |
|
||||||
| thiserror | 2.0 | Error derive | No |
|
| thiserror | 2.0 | Error derive |
|
||||||
| log | 0.4 | Logging facade | No |
|
| chrono | 0.4 | Date/time |
|
||||||
| chrono | 0.4 | Date/time | No |
|
| serde | 1.0 | Serialization |
|
||||||
| serde | 1.0 | Serialization | No |
|
| uuid | 1.11 | UUIDs |
|
||||||
| serde_json | 1.0 | JSON | No |
|
| diesel | 2.1 | Database ORM |
|
||||||
| uuid | 1.11 | UUIDs | No |
|
| reqwest | 0.12 | HTTP client |
|
||||||
| toml | 0.8 | Config parsing | No |
|
|
||||||
| diesel | 2.1 | Database ORM | Yes |
|
|
||||||
| reqwest | 0.12 | HTTP client | Yes |
|
|
||||||
| validator | 0.18 | Validation | Yes |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing
|
## Remember
|
||||||
|
|
||||||
```bash
|
- **ZERO WARNINGS** - Every clippy warning must be fixed
|
||||||
# Test all features
|
- **NO ALLOW IN CODE** - Never use #[allow()] in source files
|
||||||
cargo test --all-features
|
- **CARGO.TOML EXCEPTIONS OK** - Disable lints with false positives in Cargo.toml with comment
|
||||||
|
- **NO DEAD CODE** - Delete unused code, never prefix with _
|
||||||
# Test specific feature
|
- **NO UNWRAP/EXPECT** - Use ? operator or proper error handling
|
||||||
cargo test --features database
|
- **INLINE FORMAT ARGS** - format!("{name}") not format!("{}", name)
|
||||||
|
- **USE SELF** - In impl blocks, use Self not the type name
|
||||||
# Test without optional features
|
- **DERIVE EQ** - Always derive Eq with PartialEq
|
||||||
cargo test
|
- **DISPLAY NOT TOSTRING** - Implement Display, not ToString
|
||||||
```
|
- **USE DIAGNOSTICS** - Use IDE diagnostics tool, never call cargo clippy directly
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
- **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_SHORT: &str = "GB";
|
||||||
const DEFAULT_PLATFORM_DOMAIN: &str = "generalbots.com";
|
const DEFAULT_PLATFORM_DOMAIN: &str = "generalbots.com";
|
||||||
|
|
||||||
/// Branding configuration loaded from .product file
|
/// Branding configuration loaded from `.product` file
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BrandingConfig {
|
pub struct BrandingConfig {
|
||||||
/// Platform name (e.g., "MyCustomPlatform")
|
/// Platform name (e.g., "`MyCustomPlatform`")
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Short name for logs and compact displays (e.g., "MCP")
|
/// Short name for logs and compact displays (e.g., "MCP")
|
||||||
pub short_name: String,
|
pub short_name: String,
|
||||||
|
|
@ -82,6 +82,7 @@ impl Default for BrandingConfig {
|
||||||
|
|
||||||
impl BrandingConfig {
|
impl BrandingConfig {
|
||||||
/// Load branding from .product file if it exists
|
/// Load branding from .product file if it exists
|
||||||
|
#[must_use]
|
||||||
pub fn load() -> Self {
|
pub fn load() -> Self {
|
||||||
let search_paths = [
|
let search_paths = [
|
||||||
".product",
|
".product",
|
||||||
|
|
@ -92,7 +93,7 @@ impl BrandingConfig {
|
||||||
|
|
||||||
for path in &search_paths {
|
for path in &search_paths {
|
||||||
if let Ok(config) = Self::load_from_file(path) {
|
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;
|
return config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,8 +102,8 @@ impl BrandingConfig {
|
||||||
if let Ok(product_file) = std::env::var("PRODUCT_FILE") {
|
if let Ok(product_file) = std::env::var("PRODUCT_FILE") {
|
||||||
if let Ok(config) = Self::load_from_file(&product_file) {
|
if let Ok(config) = Self::load_from_file(&product_file) {
|
||||||
info!(
|
info!(
|
||||||
"Loaded white-label branding from PRODUCT_FILE={}: {}",
|
"Loaded white-label branding from PRODUCT_FILE={product_file}: {}",
|
||||||
product_file, config.name
|
config.name
|
||||||
);
|
);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
@ -149,8 +150,10 @@ impl BrandingConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try parsing as simple key=value format
|
// Try parsing as simple key=value format
|
||||||
let mut config = Self::default();
|
let mut config = Self {
|
||||||
config.is_white_label = true;
|
is_white_label: true,
|
||||||
|
..Self::default()
|
||||||
|
};
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
|
|
@ -261,26 +264,31 @@ pub fn init_branding() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current branding configuration
|
/// Get the current branding configuration
|
||||||
|
#[must_use]
|
||||||
pub fn branding() -> &'static BrandingConfig {
|
pub fn branding() -> &'static BrandingConfig {
|
||||||
BRANDING.get_or_init(BrandingConfig::load)
|
BRANDING.get_or_init(BrandingConfig::load)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the platform name
|
/// Get the platform name
|
||||||
|
#[must_use]
|
||||||
pub fn platform_name() -> &'static str {
|
pub fn platform_name() -> &'static str {
|
||||||
&branding().name
|
&branding().name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the short platform name
|
/// Get the short platform name
|
||||||
|
#[must_use]
|
||||||
pub fn platform_short() -> &'static str {
|
pub fn platform_short() -> &'static str {
|
||||||
&branding().short_name
|
&branding().short_name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this is a white-label deployment
|
/// Check if this is a white-label deployment
|
||||||
|
#[must_use]
|
||||||
pub fn is_white_label() -> bool {
|
pub fn is_white_label() -> bool {
|
||||||
branding().is_white_label
|
branding().is_white_label
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get formatted copyright text
|
/// Get formatted copyright text
|
||||||
|
#[must_use]
|
||||||
pub fn copyright_text() -> String {
|
pub fn copyright_text() -> String {
|
||||||
branding().copyright.clone().unwrap_or_else(|| {
|
branding().copyright.clone().unwrap_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
|
|
@ -292,6 +300,7 @@ pub fn copyright_text() -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get footer text
|
/// Get footer text
|
||||||
|
#[must_use]
|
||||||
pub fn footer_text() -> String {
|
pub fn footer_text() -> String {
|
||||||
branding()
|
branding()
|
||||||
.footer_text
|
.footer_text
|
||||||
|
|
@ -300,6 +309,7 @@ pub fn footer_text() -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a log prefix with platform branding
|
/// Format a log prefix with platform branding
|
||||||
|
#[must_use]
|
||||||
pub fn log_prefix() -> String {
|
pub fn log_prefix() -> String {
|
||||||
format!("[{}]", platform_short())
|
format!("[{}]", platform_short())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
src/error.rs
35
src/error.rs
|
|
@ -88,7 +88,8 @@ impl BotError {
|
||||||
Self::Conflict(msg.into())
|
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 }
|
Self::RateLimited { retry_after_secs }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +97,8 @@ impl BotError {
|
||||||
Self::ServiceUnavailable(msg.into())
|
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 }
|
Self::Timeout { duration_ms }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,26 +106,27 @@ impl BotError {
|
||||||
Self::Internal(msg.into())
|
Self::Internal(msg.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_code(&self) -> u16 {
|
#[must_use]
|
||||||
|
pub const fn status_code(&self) -> u16 {
|
||||||
match self {
|
match self {
|
||||||
Self::Config(_) => 500,
|
|
||||||
Self::Database(_) => 500,
|
|
||||||
Self::Http { status, .. } => *status,
|
Self::Http { status, .. } => *status,
|
||||||
Self::Auth(_) => 401,
|
Self::Auth(_) => 401,
|
||||||
Self::Validation(_) => 400,
|
Self::Validation(_) | Self::Json(_) => 400,
|
||||||
Self::NotFound { .. } => 404,
|
Self::NotFound { .. } => 404,
|
||||||
Self::Conflict(_) => 409,
|
Self::Conflict(_) => 409,
|
||||||
Self::RateLimited { .. } => 429,
|
Self::RateLimited { .. } => 429,
|
||||||
Self::ServiceUnavailable(_) => 503,
|
Self::ServiceUnavailable(_) => 503,
|
||||||
Self::Timeout { .. } => 504,
|
Self::Timeout { .. } => 504,
|
||||||
Self::Internal(_) => 500,
|
Self::Config(_)
|
||||||
Self::Io(_) => 500,
|
| Self::Database(_)
|
||||||
Self::Json(_) => 400,
|
| Self::Internal(_)
|
||||||
Self::Other(_) => 500,
|
| Self::Io(_)
|
||||||
|
| Self::Other(_) => 500,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_retryable(&self) -> bool {
|
#[must_use]
|
||||||
|
pub const fn is_retryable(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::RateLimited { .. } | Self::ServiceUnavailable(_) | Self::Timeout { .. } => true,
|
Self::RateLimited { .. } | Self::ServiceUnavailable(_) | Self::Timeout { .. } => true,
|
||||||
Self::Http { status, .. } => *status >= 500,
|
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();
|
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
|
self.status_code() >= 500
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +167,7 @@ impl From<&str> for BotError {
|
||||||
#[cfg(feature = "http-client")]
|
#[cfg(feature = "http-client")]
|
||||||
impl From<reqwest::Error> for BotError {
|
impl From<reqwest::Error> for BotError {
|
||||||
fn from(err: reqwest::Error) -> Self {
|
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 {
|
Self::Http {
|
||||||
status,
|
status,
|
||||||
message: err.to_string(),
|
message: err.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! HTTP client for botserver communication.
|
||||||
|
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
@ -7,6 +9,7 @@ use std::time::Duration;
|
||||||
const DEFAULT_BOTSERVER_URL: &str = "https://localhost:8088";
|
const DEFAULT_BOTSERVER_URL: &str = "https://localhost:8088";
|
||||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||||
|
|
||||||
|
/// HTTP client for communicating with the botserver.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BotServerClient {
|
pub struct BotServerClient {
|
||||||
client: Arc<reqwest::Client>,
|
client: Arc<reqwest::Client>,
|
||||||
|
|
@ -14,10 +17,20 @@ pub struct BotServerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn new(base_url: Option<String>) -> Self {
|
||||||
Self::with_timeout(base_url, Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
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 {
|
pub fn with_timeout(base_url: Option<String>, timeout: Duration) -> Self {
|
||||||
let url = base_url.unwrap_or_else(|| {
|
let url = base_url.unwrap_or_else(|| {
|
||||||
std::env::var("BOTSERVER_URL").unwrap_or_else(|_| DEFAULT_BOTSERVER_URL.to_string())
|
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")))
|
.user_agent(format!("BotLib/{}", env!("CARGO_PKG_VERSION")))
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to create HTTP client");
|
.unwrap_or_else(|_| reqwest::Client::new());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
client: Arc::new(client),
|
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 {
|
pub fn base_url(&self) -> &str {
|
||||||
&self.base_url
|
&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> {
|
pub async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("GET {}", url);
|
debug!("GET {url}");
|
||||||
|
|
||||||
let response = self.client.get(&url).send().await?;
|
let response = self.client.get(&url).send().await?;
|
||||||
self.handle_response(response).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,
|
&self,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &T,
|
body: &T,
|
||||||
) -> Result<R, BotError> {
|
) -> Result<R, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("POST {}", url);
|
debug!("POST {url}");
|
||||||
|
|
||||||
let response = self.client.post(&url).json(body).send().await?;
|
let response = self.client.post(&url).json(body).send().await?;
|
||||||
self.handle_response(response).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,
|
&self,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &T,
|
body: &T,
|
||||||
) -> Result<R, BotError> {
|
) -> Result<R, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("PUT {}", url);
|
debug!("PUT {url}");
|
||||||
|
|
||||||
let response = self.client.put(&url).json(body).send().await?;
|
let response = self.client.put(&url).json(body).send().await?;
|
||||||
self.handle_response(response).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,
|
&self,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &T,
|
body: &T,
|
||||||
) -> Result<R, BotError> {
|
) -> Result<R, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("PATCH {}", url);
|
debug!("PATCH {url}");
|
||||||
|
|
||||||
let response = self.client.patch(&url).json(body).send().await?;
|
let response = self.client.patch(&url).json(body).send().await?;
|
||||||
self.handle_response(response).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> {
|
pub async fn delete<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("DELETE {}", url);
|
debug!("DELETE {url}");
|
||||||
|
|
||||||
let response = self.client.delete(&url).send().await?;
|
let response = self.client.delete(&url).send().await?;
|
||||||
self.handle_response(response).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>(
|
pub async fn get_authorized<T: DeserializeOwned>(
|
||||||
&self,
|
&self,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<T, BotError> {
|
) -> Result<T, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("GET {} (authorized)", url);
|
debug!("GET {url} (authorized)");
|
||||||
|
|
||||||
let response = self.client.get(&url).bearer_auth(token).send().await?;
|
let response = self.client.get(&url).bearer_auth(token).send().await?;
|
||||||
self.handle_response(response).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,
|
&self,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &T,
|
body: &T,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<R, BotError> {
|
) -> Result<R, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("POST {} (authorized)", url);
|
debug!("POST {url} (authorized)");
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
|
|
@ -123,23 +173,31 @@ impl BotServerClient {
|
||||||
self.handle_response(response).await
|
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>(
|
pub async fn delete_authorized<T: DeserializeOwned>(
|
||||||
&self,
|
&self,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<T, BotError> {
|
) -> Result<T, BotError> {
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
let url = format!("{}{endpoint}", self.base_url);
|
||||||
debug!("DELETE {} (authorized)", url);
|
debug!("DELETE {url} (authorized)");
|
||||||
|
|
||||||
let response = self.client.delete(&url).bearer_auth(token).send().await?;
|
let response = self.client.delete(&url).bearer_auth(token).send().await?;
|
||||||
self.handle_response(response).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 {
|
pub async fn health_check(&self) -> bool {
|
||||||
match self.get::<serde_json::Value>("/health").await {
|
match self.get::<serde_json::Value>("/health").await {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Health check failed: {}", e);
|
error!("Health check failed: {e}");
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,13 +215,13 @@ impl BotServerClient {
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
.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));
|
return Err(BotError::http(status_code, error_text));
|
||||||
}
|
}
|
||||||
|
|
||||||
response.json().await.map_err(|e| {
|
response.json().await.map_err(|e| {
|
||||||
error!("Failed to parse response: {}", e);
|
error!("Failed to parse response: {e}");
|
||||||
BotError::http(status_code, format!("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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("BotServerClient")
|
f.debug_struct("BotServerClient")
|
||||||
.field("base_url", &self.base_url)
|
.field("base_url", &self.base_url)
|
||||||
.finish()
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +270,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_debug() {
|
fn test_client_debug() {
|
||||||
let client = BotServerClient::new(Some("http://debug-test".to_string()));
|
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("BotServerClient"));
|
||||||
assert!(debug_str.contains("http://debug-test"));
|
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
|
//! This crate provides common types, utilities, and abstractions
|
||||||
//! shared between botserver and botui.
|
//! shared between botserver and botui.
|
||||||
|
|
@ -17,7 +17,9 @@ pub mod models;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
||||||
// Re-exports for convenience
|
// 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 error::{BotError, BotResult};
|
||||||
pub use message_types::MessageType;
|
pub use message_types::MessageType;
|
||||||
pub use models::{ApiResponse, BotResponse, Session, Suggestion, UserMessage};
|
pub use models::{ApiResponse, BotResponse, Session, Suggestion, UserMessage};
|
||||||
|
|
|
||||||
|
|
@ -10,28 +10,28 @@ use serde::{Deserialize, Serialize};
|
||||||
pub struct MessageType(pub i32);
|
pub struct MessageType(pub i32);
|
||||||
|
|
||||||
impl MessageType {
|
impl MessageType {
|
||||||
/// Regular message from external systems (WhatsApp, Instagram, etc.)
|
/// Regular message from external systems (`WhatsApp`, Instagram, etc.)
|
||||||
pub const EXTERNAL: MessageType = MessageType(0);
|
pub const EXTERNAL: Self = Self(0);
|
||||||
|
|
||||||
/// User message from web interface
|
/// 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)
|
/// 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
|
/// Continue interrupted response
|
||||||
pub const CONTINUE: MessageType = MessageType(3);
|
pub const CONTINUE: Self = Self(3);
|
||||||
|
|
||||||
/// Suggestion or command message
|
/// Suggestion or command message
|
||||||
pub const SUGGESTION: MessageType = MessageType(4);
|
pub const SUGGESTION: Self = Self(4);
|
||||||
|
|
||||||
/// Context change notification
|
/// Context change notification
|
||||||
pub const CONTEXT_CHANGE: MessageType = MessageType(5);
|
pub const CONTEXT_CHANGE: Self = Self(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<i32> for MessageType {
|
impl From<i32> for MessageType {
|
||||||
fn from(value: i32) -> Self {
|
fn from(value: i32) -> Self {
|
||||||
MessageType(value)
|
Self(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ impl From<MessageType> for i32 {
|
||||||
|
|
||||||
impl Default for MessageType {
|
impl Default for MessageType {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
MessageType::USER
|
Self::USER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ impl std::fmt::Display for MessageType {
|
||||||
5 => "CONTEXT_CHANGE",
|
5 => "CONTEXT_CHANGE",
|
||||||
_ => "UNKNOWN",
|
_ => "UNKNOWN",
|
||||||
};
|
};
|
||||||
write!(f, "{}", name)
|
write!(f, "{name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! API models for bot communication.
|
||||||
|
|
||||||
use crate::message_types::MessageType;
|
use crate::message_types::MessageType;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -17,7 +19,8 @@ pub struct ApiResponse<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ApiResponse<T> {
|
impl<T> ApiResponse<T> {
|
||||||
pub fn success(data: T) -> Self {
|
#[must_use]
|
||||||
|
pub const fn success(data: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
success: true,
|
success: true,
|
||||||
data: Some(data),
|
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 {
|
pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -37,6 +41,7 @@ impl<T> ApiResponse<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn error(message: impl Into<String>) -> Self {
|
pub fn error(message: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
success: false,
|
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 {
|
pub fn error_with_code(message: impl Into<String>, code: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
success: false,
|
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> {
|
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> ApiResponse<U> {
|
||||||
ApiResponse {
|
ApiResponse {
|
||||||
success: self.success,
|
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
|
self.success
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_error(&self) -> bool {
|
#[must_use]
|
||||||
|
pub const fn is_error(&self) -> bool {
|
||||||
!self.success
|
!self.success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +104,7 @@ pub struct Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
|
#[must_use]
|
||||||
pub fn new(user_id: Uuid, bot_id: Uuid, title: impl Into<String>) -> Self {
|
pub fn new(user_id: Uuid, bot_id: Uuid, title: impl Into<String>) -> Self {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -108,19 +118,23 @@ impl Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_expiry(mut self, expires_at: DateTime<Utc>) -> Self {
|
pub fn with_expiry(mut self, expires_at: DateTime<Utc>) -> Self {
|
||||||
self.expires_at = Some(expires_at);
|
self.expires_at = Some(expires_at);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn is_expired(&self) -> bool {
|
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 {
|
pub fn is_active(&self) -> bool {
|
||||||
!self.is_expired()
|
!self.is_expired()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn remaining_time(&self) -> Option<chrono::Duration> {
|
pub fn remaining_time(&self) -> Option<chrono::Duration> {
|
||||||
self.expires_at.map(|exp| exp - Utc::now())
|
self.expires_at.map(|exp| exp - Utc::now())
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +156,7 @@ pub struct UserMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserMessage {
|
impl UserMessage {
|
||||||
|
#[must_use]
|
||||||
pub fn text(
|
pub fn text(
|
||||||
bot_id: impl Into<String>,
|
bot_id: impl Into<String>,
|
||||||
user_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 {
|
pub fn with_media(mut self, url: impl Into<String>) -> Self {
|
||||||
self.media_url = Some(url.into());
|
self.media_url = Some(url.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_context(mut self, context: impl Into<String>) -> Self {
|
pub fn with_context(mut self, context: impl Into<String>) -> Self {
|
||||||
self.context_name = Some(context.into());
|
self.context_name = Some(context.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_media(&self) -> bool {
|
#[must_use]
|
||||||
|
pub const fn has_media(&self) -> bool {
|
||||||
self.media_url.is_some()
|
self.media_url.is_some()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +207,7 @@ pub struct Suggestion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Suggestion {
|
impl Suggestion {
|
||||||
|
#[must_use]
|
||||||
pub fn new(text: impl Into<String>) -> Self {
|
pub fn new(text: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
|
|
@ -198,16 +217,19 @@ impl Suggestion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_context(mut self, context: impl Into<String>) -> Self {
|
pub fn with_context(mut self, context: impl Into<String>) -> Self {
|
||||||
self.context = Some(context.into());
|
self.context = Some(context.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_action(mut self, action: impl Into<String>) -> Self {
|
pub fn with_action(mut self, action: impl Into<String>) -> Self {
|
||||||
self.action = Some(action.into());
|
self.action = Some(action.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
|
pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
|
||||||
self.icon = Some(icon.into());
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
|
|
@ -242,6 +264,7 @@ pub struct BotResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotResponse {
|
impl BotResponse {
|
||||||
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
bot_id: impl Into<String>,
|
bot_id: impl Into<String>,
|
||||||
session_id: impl Into<String>,
|
session_id: impl Into<String>,
|
||||||
|
|
@ -265,6 +288,7 @@ impl BotResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn streaming(
|
pub fn streaming(
|
||||||
bot_id: impl Into<String>,
|
bot_id: impl Into<String>,
|
||||||
session_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
|
pub fn with_suggestions<I, S>(mut self, suggestions: I) -> Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
|
|
@ -297,11 +322,13 @@ impl BotResponse {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn add_suggestion(mut self, suggestion: impl Into<Suggestion>) -> Self {
|
pub fn add_suggestion(mut self, suggestion: impl Into<Suggestion>) -> Self {
|
||||||
self.suggestions.push(suggestion.into());
|
self.suggestions.push(suggestion.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_context(
|
pub fn with_context(
|
||||||
mut self,
|
mut self,
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
|
|
@ -318,15 +345,18 @@ impl BotResponse {
|
||||||
self.content.push_str(chunk);
|
self.content.push_str(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn complete(mut self) -> Self {
|
pub fn complete(mut self) -> Self {
|
||||||
self.is_complete = true;
|
self.is_complete = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_streaming(&self) -> bool {
|
#[must_use]
|
||||||
|
pub const fn is_streaming(&self) -> bool {
|
||||||
self.stream_token.is_some() && !self.is_complete
|
self.stream_token.is_some() && !self.is_complete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn has_suggestions(&self) -> bool {
|
pub fn has_suggestions(&self) -> bool {
|
||||||
!self.suggestions.is_empty()
|
!self.suggestions.is_empty()
|
||||||
}
|
}
|
||||||
|
|
@ -376,6 +406,7 @@ pub enum AttachmentType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Attachment {
|
impl Attachment {
|
||||||
|
#[must_use]
|
||||||
pub fn new(attachment_type: AttachmentType, url: impl Into<String>) -> Self {
|
pub fn new(attachment_type: AttachmentType, url: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
attachment_type,
|
attachment_type,
|
||||||
|
|
@ -387,51 +418,62 @@ impl Attachment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn image(url: impl Into<String>) -> Self {
|
pub fn image(url: impl Into<String>) -> Self {
|
||||||
Self::new(AttachmentType::Image, url)
|
Self::new(AttachmentType::Image, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn audio(url: impl Into<String>) -> Self {
|
pub fn audio(url: impl Into<String>) -> Self {
|
||||||
Self::new(AttachmentType::Audio, url)
|
Self::new(AttachmentType::Audio, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn video(url: impl Into<String>) -> Self {
|
pub fn video(url: impl Into<String>) -> Self {
|
||||||
Self::new(AttachmentType::Video, url)
|
Self::new(AttachmentType::Video, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn document(url: impl Into<String>) -> Self {
|
pub fn document(url: impl Into<String>) -> Self {
|
||||||
Self::new(AttachmentType::Document, url)
|
Self::new(AttachmentType::Document, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn file(url: impl Into<String>) -> Self {
|
pub fn file(url: impl Into<String>) -> Self {
|
||||||
Self::new(AttachmentType::File, url)
|
Self::new(AttachmentType::File, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
|
||||||
self.mime_type = Some(mime_type.into());
|
self.mime_type = Some(mime_type.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
|
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
|
||||||
self.filename = Some(filename.into());
|
self.filename = Some(filename.into());
|
||||||
self
|
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.size = Some(size);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn with_thumbnail(mut self, thumbnail_url: impl Into<String>) -> Self {
|
pub fn with_thumbnail(mut self, thumbnail_url: impl Into<String>) -> Self {
|
||||||
self.thumbnail_url = Some(thumbnail_url.into());
|
self.thumbnail_url = Some(thumbnail_url.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_image(&self) -> bool {
|
#[must_use]
|
||||||
self.attachment_type == AttachmentType::Image
|
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!(
|
matches!(
|
||||||
self.attachment_type,
|
self.attachment_type,
|
||||||
AttachmentType::Image | AttachmentType::Audio | AttachmentType::Video
|
AttachmentType::Image | AttachmentType::Audio | AttachmentType::Video
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,12 @@ pub enum ComponentStatus {
|
||||||
impl std::fmt::Display for ComponentStatus {
|
impl std::fmt::Display for ComponentStatus {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ComponentStatus::Running => write!(f, "[OK] Running"),
|
Self::Running => write!(f, "[OK] Running"),
|
||||||
ComponentStatus::Stopped => write!(f, "[STOP] Stopped"),
|
Self::Stopped => write!(f, "[STOP] Stopped"),
|
||||||
ComponentStatus::Error => write!(f, "[ERR] Error"),
|
Self::Error => write!(f, "[ERR] Error"),
|
||||||
ComponentStatus::Updating => write!(f, "[UPD] Updating"),
|
Self::Updating => write!(f, "[UPD] Updating"),
|
||||||
ComponentStatus::NotInstalled => write!(f, "[--] Not Installed"),
|
Self::NotInstalled => write!(f, "[--] Not Installed"),
|
||||||
ComponentStatus::Unknown => write!(f, "[?] Unknown"),
|
Self::Unknown => write!(f, "[?] Unknown"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,12 +74,12 @@ pub enum ComponentSource {
|
||||||
impl std::fmt::Display for ComponentSource {
|
impl std::fmt::Display for ComponentSource {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ComponentSource::Builtin => write!(f, "Built-in"),
|
Self::Builtin => write!(f, "Built-in"),
|
||||||
ComponentSource::Docker => write!(f, "Docker"),
|
Self::Docker => write!(f, "Docker"),
|
||||||
ComponentSource::Lxc => write!(f, "LXC"),
|
Self::Lxc => write!(f, "LXC"),
|
||||||
ComponentSource::System => write!(f, "System"),
|
Self::System => write!(f, "System"),
|
||||||
ComponentSource::Binary => write!(f, "Binary"),
|
Self::Binary => write!(f, "Binary"),
|
||||||
ComponentSource::External => write!(f, "External"),
|
Self::External => write!(f, "External"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +106,7 @@ impl Default for VersionRegistry {
|
||||||
|
|
||||||
impl VersionRegistry {
|
impl VersionRegistry {
|
||||||
/// Create a new version registry
|
/// Create a new version registry
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut registry = Self::default();
|
let mut registry = Self::default();
|
||||||
registry.register_builtin_components();
|
registry.register_builtin_components();
|
||||||
|
|
@ -185,16 +186,19 @@ impl VersionRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get component by name
|
/// Get component by name
|
||||||
|
#[must_use]
|
||||||
pub fn get_component(&self, name: &str) -> Option<&ComponentVersion> {
|
pub fn get_component(&self, name: &str) -> Option<&ComponentVersion> {
|
||||||
self.components.get(name)
|
self.components.get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all components
|
/// 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
|
&self.components
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get components with available updates
|
/// Get components with available updates
|
||||||
|
#[must_use]
|
||||||
pub fn get_available_updates(&self) -> Vec<&ComponentVersion> {
|
pub fn get_available_updates(&self) -> Vec<&ComponentVersion> {
|
||||||
self.components
|
self.components
|
||||||
.values()
|
.values()
|
||||||
|
|
@ -203,6 +207,7 @@ impl VersionRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get summary of all components
|
/// Get summary of all components
|
||||||
|
#[must_use]
|
||||||
pub fn summary(&self) -> String {
|
pub fn summary(&self) -> String {
|
||||||
let running = self
|
let running = self
|
||||||
.components
|
.components
|
||||||
|
|
@ -213,12 +218,16 @@ impl VersionRegistry {
|
||||||
let updates = self.get_available_updates().len();
|
let updates = self.get_available_updates().len();
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{} v{} | {}/{} components running | {} updates available",
|
"{BOTSERVER_NAME} v{} | {running}/{total} components running | {updates} updates available",
|
||||||
BOTSERVER_NAME, self.core_version, running, total, updates
|
self.core_version
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get summary as JSON
|
/// Get summary as JSON
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if JSON serialization fails.
|
||||||
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
||||||
serde_json::to_string_pretty(self)
|
serde_json::to_string_pretty(self)
|
||||||
}
|
}
|
||||||
|
|
@ -235,6 +244,7 @@ pub fn init_version_registry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get version registry (read-only)
|
/// Get version registry (read-only)
|
||||||
|
#[must_use]
|
||||||
pub fn version_registry() -> Option<VersionRegistry> {
|
pub fn version_registry() -> Option<VersionRegistry> {
|
||||||
VERSION_REGISTRY.read().ok()?.clone()
|
VERSION_REGISTRY.read().ok()?.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -264,6 +274,7 @@ pub fn update_component_status(name: &str, status: ComponentStatus) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get component version
|
/// Get component version
|
||||||
|
#[must_use]
|
||||||
pub fn get_component_version(name: &str) -> Option<ComponentVersion> {
|
pub fn get_component_version(name: &str) -> Option<ComponentVersion> {
|
||||||
VERSION_REGISTRY
|
VERSION_REGISTRY
|
||||||
.read()
|
.read()
|
||||||
|
|
@ -274,13 +285,15 @@ pub fn get_component_version(name: &str) -> Option<ComponentVersion> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get botserver version
|
/// Get botserver version
|
||||||
pub fn get_botserver_version() -> &'static str {
|
#[must_use]
|
||||||
|
pub const fn get_botserver_version() -> &'static str {
|
||||||
BOTSERVER_VERSION
|
BOTSERVER_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get version string for display
|
/// Get version string for display
|
||||||
|
#[must_use]
|
||||||
pub fn version_string() -> String {
|
pub fn version_string() -> String {
|
||||||
format!("{} v{}", BOTSERVER_NAME, BOTSERVER_VERSION)
|
format!("{BOTSERVER_NAME} v{BOTSERVER_VERSION}")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -333,8 +346,12 @@ mod tests {
|
||||||
fn test_update_status() {
|
fn test_update_status() {
|
||||||
let mut registry = VersionRegistry::new();
|
let mut registry = VersionRegistry::new();
|
||||||
registry.update_status("botserver", ComponentStatus::Stopped);
|
registry.update_status("botserver", ComponentStatus::Stopped);
|
||||||
let component = registry.get_component("botserver").unwrap();
|
let component = registry.get_component("botserver");
|
||||||
assert_eq!(component.status, ComponentStatus::Stopped);
|
assert!(
|
||||||
|
component.is_some(),
|
||||||
|
"botserver component should exist in registry"
|
||||||
|
);
|
||||||
|
assert_eq!(component.map(|c| c.status), Some(ComponentStatus::Stopped));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue