Adding Dependencies
BotServer is a single-crate Rust application, so all dependencies are managed through the root Cargo.toml file. This guide covers how to add, update, and manage dependencies effectively.
Adding a Dependency
Basic Dependency
To add a new crate, edit Cargo.toml and add it to the [dependencies] section:
[dependencies]
serde = "1.0"
Then update your dependencies:
cargo build
Dependency with Features
Many crates offer optional features that you can enable selectively. The syntax uses curly braces to specify both the version and the features array:
[dependencies]
tokio = { version = "1.41", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
Version-Specific Dependencies
Cargo supports several version constraint formats to control which versions are acceptable. An exact version uses the equals sign prefix, while minimum versions use the greater-than-or-equal operator. The caret symbol indicates compatible versions according to semantic versioning, and wildcards allow any version within a major release:
[dependencies]
# Exact version
diesel = "=2.1.0"
# Minimum version
anyhow = ">=1.0.0"
# Compatible version (caret)
regex = "^1.11"
# Wildcard
uuid = "1.*"
Git Dependencies
You can add dependencies directly from Git repositories when you need unreleased features or custom forks. Specify the repository URL along with an optional branch name:
[dependencies]
rhai = { git = "https://github.com/therealprof/rhai.git", branch = "features/use-web-time" }
For reproducible builds, pin to a specific commit using the rev field:
[dependencies]
my-crate = { git = "https://github.com/user/repo", rev = "abc123" }
You can also reference a tagged release:
[dependencies]
my-crate = { git = "https://github.com/user/repo", tag = "v1.0.0" }
Optional Dependencies
Some dependencies aren’t always needed and can be marked as optional. These won’t be compiled unless explicitly enabled through feature flags:
[dependencies]
qdrant-client = { version = "1.12", optional = true }
imap = { version = "3.0.0-alpha.15", optional = true }
Then define features that enable them:
[features]
vectordb = ["qdrant-client"]
email = ["imap"]
Platform-Specific Dependencies
Certain dependencies are only needed on specific operating systems. Cargo’s target configuration syntax lets you conditionally include dependencies based on the compilation target:
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.dependencies]
winapi = "0.3"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
Existing Dependencies
BotServer relies on a comprehensive set of dependencies organized by functionality.
Web Framework
The HTTP layer is built on axum as the primary web framework, with tower providing middleware and service abstractions. The tower-http crate adds HTTP-specific middleware for CORS, static file serving, and tracing. At the lowest level, hyper handles the HTTP protocol implementation.
Async Runtime
Asynchronous execution is powered by tokio with its full feature set enabled. Supporting crates include tokio-stream for stream utilities, async-trait for async trait definitions, async-stream for async stream macros, and async-lock for asynchronous locking primitives.
Database
Database operations use diesel as the ORM for PostgreSQL, with diesel_migrations handling schema migrations. Connection pooling is managed by r2d2, and the redis crate provides cache client functionality compatible with both Valkey and Redis.
Storage
Cloud storage integration relies on the AWS SDK, with aws-config for configuration and aws-sdk-s3 for S3-compatible storage operations through the drive component. The optional qdrant-client crate enables vector database functionality.
Security
Cryptographic operations use several specialized crates. Password hashing is handled by argon2, encryption by aes-gcm, HMAC authentication by hmac, and SHA-256 hashing by sha2.
Scripting
The BASIC interpreter is powered by rhai, which provides a safe and fast embedded scripting engine.
Data Formats
Serialization and deserialization use serde as the core framework, with serde_json for JSON support. Additional format support comes from csv for CSV parsing and base64 for Base64 encoding.
Document Processing
Document handling includes pdf-extract for PDF text extraction, mailparse for email parsing, and zip for ZIP archive handling.
Communication
Network communication uses reqwest as the HTTP client. Email functionality is split between lettre for SMTP sending and the optional imap crate for reading emails. Video conferencing is provided by the livekit crate.
Desktop (Optional)
Desktop application builds use tauri as the framework, along with tauri-plugin-dialog for native file dialogs and tauri-plugin-opener for opening files and URLs.
Utilities
Common utilities include anyhow for error handling, log and env_logger for logging, tracing for structured logging, chrono for date and time handling, uuid for UUID generation, regex for regular expressions, and rand for random number generation.
Testing
Test support comes from mockito for HTTP mocking and tempfile for temporary file handling.
Adding a New Dependency: Example
This walkthrough demonstrates adding JSON Web Token (JWT) support to the project.
1. Choose a Crate
Search on crates.io to find suitable crates:
cargo search jsonwebtoken
2. Add to Cargo.toml
[dependencies]
jsonwebtoken = "9.2"
3. Update Dependencies
cargo build
4. Import in Code
In your Rust file (e.g., src/auth/mod.rs):
#![allow(unused)] fn main() { use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey}; }
5. Use the Dependency
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, exp: usize, } pub fn create_jwt(user_id: &str) -> Result<String, jsonwebtoken::errors::Error> { let expiration = chrono::Utc::now() .checked_add_signed(chrono::Duration::hours(24)) .unwrap() .timestamp() as usize; let claims = Claims { sub: user_id.to_owned(), exp: expiration, }; let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()); let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(secret.as_ref()), )?; Ok(token) } }
Managing Dependencies
Updating Dependencies
To update all dependencies to their latest compatible versions, run cargo update. For updating a specific dependency, use cargo update -p serde with the package name.
Checking for Outdated Dependencies
The cargo-outdated tool helps identify dependencies that have newer versions available:
cargo install cargo-outdated
cargo outdated
Upgrading to Latest Compatible Versions
The cargo-edit tool provides convenient commands for managing dependencies:
cargo install cargo-edit
cargo upgrade
Auditing for Security Vulnerabilities
Regular security audits are essential for production applications:
cargo install cargo-audit
cargo audit
Viewing the Dependency Tree
Understanding your dependency graph helps identify bloat and conflicts:
cargo tree
To view dependencies for a specific package:
cargo tree -p diesel
Finding Duplicate Dependencies
Different versions of the same crate increase binary size and compile time:
cargo tree --duplicates
Feature Management
BotServer uses feature flags to enable optional functionality, allowing users to compile only what they need.
Current Features
[features]
default = ["desktop"]
vectordb = ["qdrant-client"]
email = ["imap"]
desktop = ["dep:tauri", "dep:tauri-plugin-dialog", "dep:tauri-plugin-opener"]
Adding a New Feature
Start by adding the dependency as optional:
[dependencies]
elasticsearch = { version = "8.5", optional = true }
Then create a feature that enables it:
[features]
search = ["elasticsearch"]
Use conditional compilation in your code to only include the functionality when the feature is enabled:
#![allow(unused)] fn main() { #[cfg(feature = "search")] pub mod search { use elasticsearch::Elasticsearch; pub fn create_client() -> Elasticsearch { // Implementation } } }
Build with the feature enabled:
cargo build --features search
Build Dependencies
Dependencies needed only at build time (used in build.rs) go in a separate section:
[build-dependencies]
tauri-build = { version = "2", features = [] }
Development Dependencies
Dependencies needed only during testing should be placed in the dev-dependencies section. These are not included in release builds:
[dev-dependencies]
mockito = "1.7.0"
tempfile = "3"
Dependency Best Practices
Version Pinning
For production builds, prefer specific versions over ranges to ensure reproducible builds. While serde = "1.0.193" guarantees a specific version, serde = "1" could pull in any 1.x release, potentially introducing unexpected changes.
Minimize Dependencies
Every dependency you add increases build time, binary size, and maintenance burden while introducing potential security risks. Only add dependencies that provide significant value and aren’t easily implemented inline.
Check License Compatibility
All dependencies must have licenses compatible with AGPL-3.0. The cargo-license tool helps audit your dependency licenses:
cargo install cargo-license
cargo license
Prefer Maintained Crates
When choosing between crates that provide similar functionality, evaluate them based on recent release activity, GitHub repository engagement, maintainer responsiveness, and documentation quality.
Review Security Advisories
Make dependency auditing part of your regular development workflow. Running cargo audit regularly helps catch known vulnerabilities before they become problems.
Use Features to Reduce Size
Many crates include features you don’t need. Instead of enabling everything with tokio = "1.41", specify only the features you actually use:
tokio = { version = "1.41", features = ["rt-multi-thread", "net", "sync"] }
Common Issues
Conflicting Versions
When multiple crates require different versions of the same dependency, Cargo will fail to resolve the dependency graph. Use cargo tree to identify which crates are causing the conflict, then update dependencies or look for alternative crates.
Missing System Libraries
Some crates require system libraries to be installed. If you see linker errors mentioning cc, check the crate’s documentation for required system packages and refer to the Building from Source guide.
Feature Not Found
Referencing a non-existent feature will cause a build error. Double-check feature names in the crate’s Cargo.toml on crates.io or in its repository.
Removing Dependencies
To remove a dependency, first delete it from Cargo.toml. Then find and remove all import statements using grep or ripgrep:
rg "use dependency_name" src/
After removing the imports, clean and rebuild:
cargo clean
cargo build
Verify the dependency is completely removed:
cargo tree | grep dependency_name
Alternative Registries
Using a Custom Registry
For private crates or custom registries, configure the registry in your Cargo.toml:
[dependencies]
my-crate = { version = "1.0", registry = "my-registry" }
[registries.my-registry]
index = "https://my-registry.example.com/index"
For private company crates, consider Git dependencies or a private registry like Artifactory or CloudSmith.
Dependency Documentation
Good documentation makes dependencies easier to maintain. Add comments in Cargo.toml explaining why each dependency exists:
[dependencies]
# JWT token generation and validation
jsonwebtoken = "9.2"
Document usage in your code with doc comments that explain the dependency’s role:
#![allow(unused)] fn main() { /// Creates a JWT token for user authentication. /// /// Uses the `jsonwebtoken` crate to encode user claims /// with an expiration time. pub fn create_jwt(user_id: &str) -> Result<String, Error> { // Implementation } }
Next Steps
Review the Module Structure documentation to understand where to use new dependencies within the codebase. The Service Layer guide shows how dependencies integrate into the application architecture. For extending BASIC with new functionality that leverages dependencies, see Creating Custom Keywords.