-
Notifications
You must be signed in to change notification settings - Fork 10
Development
This guide covers development setup, testing, contributing, and the technical architecture of bkmr for developers who want to contribute or understand the codebase.
bkmr is built with:
- Language: Rust 2021 edition
- Database: SQLite with Diesel ORM
- Search: FTS5 full-text search
- CLI Framework: Clap v4
- LSP: tower-lsp with async tokio runtime
- Testing: Cargo test with shared database strategy
Required:
- Rust 1.70+ (latest stable recommended)
- SQLite 3.35+
- Git
Optional (for full development):
- fzf (for fuzzy finder testing)
- Python 3.8+ (for LSP test scripts)
- jq (for JSON processing in examples)
# Clone repository
git clone https://github.com/sysid/bkmr.git
cd bkmr
# Build debug version (includes all functionality)
cargo build
# Build release version (optimized)
cargo build --release
# Binary location
ls -la target/debug/bkmr # Debug
ls -la target/release/bkmr # Release1. Create test database:
# Create at standard test location
./target/debug/bkmr create-db ../db/bkmr.db
# Set environment variable
export BKMR_DB_URL=../db/bkmr.db2. Verify build:
# Check version
./target/debug/bkmr --version
# Test basic functionality
./target/debug/bkmr add "https://example.com" test
./target/debug/bkmr search "test"3. Configure editor:
# For code navigation and development
# Rust Analyzer recommended for VS Code/Neovimbkmr/
├── bkmr/ # Main Rust project
│ ├── src/
│ │ ├── main.rs # Entry point, CLI routing
│ │ ├── cli/ # CLI commands and handlers
│ │ ├── application/ # Use cases and business logic
│ │ ├── domain/ # Core entities and traits
│ │ ├── infrastructure/ # External systems (DB, HTTP, embeddings)
│ │ └── lsp/ # LSP server implementation
│ ├── migrations/ # Diesel database migrations
│ ├── tests/ # Integration tests
│ └── Cargo.toml # Dependencies and configuration
├── docs/ # Documentation (to be deprecated)
├── bkmr.wiki/ # GitHub wiki (main documentation)
├── scripts/ # Utility scripts
│ └── lsp/ # LSP testing scripts
├── Makefile # Development commands
└── README.md # Project overview
bkmr follows Clean Architecture principles:
Layer Structure:
-
Domain (
src/domain/) - Core business entities and traits- No external dependencies
- Pure business logic
- Entity definitions (Bookmark, Tag, etc.)
-
Application (
src/application/) - Use cases and orchestration- Service traits and implementations
- Depends only on Domain interfaces
- Business workflow coordination
-
Infrastructure (
src/infrastructure/) - External systems- SQLite repository implementations
- OpenAI embeddings integration
- HTTP client for metadata fetching
- Clipboard operations
-
CLI (
src/cli/) - User interface- Command parsing (Clap)
- Command handlers
- Output formatting
-
LSP (
src/lsp/) - Editor integration- tower-lsp protocol implementation
- Async/sync bridging
- Language-aware filtering
Dependency Injection:
- ServiceContainer pattern for dependency wiring
- All services accept dependencies via constructors
- No global state (eliminated in v4.31+)
- Arc for shared service ownership
# Primary test command (recommended)
make test
# Manual execution
cargo test -- --test-threads=1
# With output visible
cargo test -- --test-threads=1 --nocapture
# Specific test
cargo test --lib test_name -- --test-threads=1 --nocaptureCritical reasons:
-
Database contention - Tests share SQLite database file (
../db/bkmr.db) - Lock prevention - Eliminates SQLite lock conflicts during parallel access
- Environment isolation - Prevents race conditions in environment variable manipulation
- Reliable execution - Ensures consistent, deterministic test results
Without --test-threads=1:
- ❌ Random SQLite lock errors
- ❌ Test flakiness
- ❌ Intermittent CI failures
- ❌ Race conditions in environment setup
Unit Tests (cargo test --lib):
# Run only library tests
env RUST_LOG=error BKMR_DB_URL=../db/bkmr.db cargo test --lib --manifest-path bkmr/Cargo.toml -- --test-threads=1Integration Tests (tests/ directory):
# Run integration tests
cargo test --test '*' -- --test-threads=1Documentation Tests (embedded in code):
# Included in cargo test
# Tests example code in doc commentsTestServiceContainer (src/util/test_service_container.rs):
// Modern dependency injection for tests
let test_container = TestServiceContainer::new();
let service = test_container.bookmark_service.clone();
// Use service with explicit dependencies
let bookmarks = service.get_all_bookmarks(None, None)?;Test Conventions:
-
Naming:
given_X_when_Y_then_Z()pattern - Structure: Arrange/Act/Assert
- Environment: Use EnvGuard for variable isolation
- Services: Obtain via TestServiceContainer
Quick tests (library only):
make test-lib
# Or
env RUST_LOG=error BKMR_DB_URL=../db/bkmr.db cargo test --lib --manifest-path bkmr/Cargo.toml -- --test-threads=1 --quietFull test suite:
make testWith debug output:
RUST_LOG=debug cargo test -- --test-threads=1 --nocapture 2>&1 | tee test-output.log# Build
make build # Build release version
make # Same as 'make build'
# Testing
make test # Run all tests (single-threaded)
make test-lib # Run only library tests
# Code Quality
make format # Format code with cargo fmt
make lint # Run clippy linter with auto-fix
# Maintenance
make clean # Clean build artifacts
make install # Install to ~/bin/ with version suffix
# Database
make create-test-db # Create test databaseFormat code:
cargo fmt
cargo fmt --check # Check without modifyingLinting:
cargo clippy --all-targets --all-features
cargo clippy --fix # Auto-fix warningsBuild variations:
# Debug build (fast compile, unoptimized)
cargo build
# Release build (optimized)
cargo build --release
# With specific features (no longer needed, all features always enabled)
cargo build --releaseRun from source:
# Debug binary
./target/debug/bkmr search "test"
# Release binary
./target/release/bkmr search "test"
# Or use cargo run
cargo run -- search "test"
cargo run --release -- search "test"Create new migration:
# Install diesel CLI
cargo install diesel_cli --no-default-features --features sqlite
# Create migration
diesel migration generate add_new_column
# Edit generated files in migrations/
# - up.sql: Schema changes
# - down.sql: Rollback changes
# Apply migration
diesel migration run
# Rollback if needed
diesel migration revertMigration Best Practices:
- Always create backup before schema changes
- Test migrations on copy of production database
- Write both up.sql and down.sql
- Document complex migrations in comments
Reset test database:
# Remove existing
rm -f ../db/bkmr.db
# Create fresh
./target/debug/bkmr create-db ../db/bkmr.db
# Run migrations (automatic on first access)
./target/debug/bkmr search ""Enable debug output:
# Application debug logs
RUST_LOG=debug cargo run -- search "test"
# Trace level (very verbose)
RUST_LOG=trace cargo run -- search "test"
# Specific modules
RUST_LOG=bkmr::application=debug cargo run -- search "test"
RUST_LOG=bkmr::lsp=debug cargo run -- lsp
# Multiple modules
RUST_LOG=bkmr::application=debug,bkmr::infrastructure=debug cargo run -- search "test"Debug flags:
# Single debug flag
cargo run -- -d search "test"
# Double debug flag (more verbose)
cargo run -- -d -d search "test"
# Save logs to file
RUST_LOG=debug cargo run -- search "test" 2>/tmp/bkmr-debug.logTest LSP server:
# Start LSP in debug mode
RUST_LOG=debug ./target/debug/bkmr lsp 2>/tmp/bkmr-lsp.log
# In another terminal, watch logs
tail -f /tmp/bkmr-lsp.log
# Use Python test scripts
python3 scripts/lsp/list_snippets.py --debug
python3 scripts/lsp/get_snippet.py 123
python3 scripts/lsp/test_lsp_client.pyTest basic LSP connectivity:
# Echo initialize request
echo '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}' | ./target/debug/bkmr lsp
# Should return JSON with capabilitiesSearch not working:
# Debug search execution
RUST_LOG=bkmr::application::services::bookmark_service=debug cargo run -- search "test" 2>&1 | grep -i searchTemplate not interpolating:
# Debug template rendering
RUST_LOG=bkmr::application::services::template_service=debug cargo run -- open <id> 2>&1 | grep -i templateDatabase issues:
# Debug database operations
RUST_LOG=bkmr::infrastructure::repository=debug cargo run -- search "test" 2>&1 | grep -i database- Check existing issues: Look for similar feature requests or bug reports
- Open discussion: For significant changes, open an issue first
- Read architecture: Understand Clean Architecture principles
- Run tests: Ensure existing tests pass
1. Create feature branch:
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix2. Make changes:
- Write code following Rust conventions
- Add tests for new functionality
- Update documentation
- Run tests frequently
3. Test thoroughly:
# Format code
make format
# Run linter
make lint
# Run all tests (CRITICAL)
make test
# All tests must pass before committing4. Commit changes:
# Write clear commit messages
git commit -m "Add: New feature description"
git commit -m "Fix: Bug description"
git commit -m "Docs: Documentation update"5. Push and create PR:
git push origin feature/your-feature-name
# Create pull request on GitHub
# Fill in PR template with:
# - Description of changes
# - Related issues
# - Testing performedRust Conventions:
- Follow
rustfmtformatting (runmake format) - Use snake_case for functions and variables
- Use PascalCase for types and traits
- Add documentation comments for public APIs
- Use
thiserrorfor error types - Prefer
Result<T>over panics
Error Handling:
// Good: Propagate errors with context
let bookmark = repository.get_bookmark(id)
.context("Failed to retrieve bookmark")?;
// Good: Descriptive error messages
return Err(DomainError::NotFound(format!("Bookmark {} not found", id)));
// Avoid: Generic errors
return Err(DomainError::Unknown);Dependency Injection:
// Good: Explicit dependencies via constructor
pub struct BookmarkServiceImpl {
repository: Arc<dyn BookmarkRepository>,
embedder: Arc<dyn Embedder>,
}
impl BookmarkServiceImpl {
pub fn new(
repository: Arc<dyn BookmarkRepository>,
embedder: Arc<dyn Embedder>,
) -> Arc<dyn BookmarkService> {
Arc::new(Self { repository, embedder })
}
}
// Avoid: Global state or factory methods
// These patterns have been completely eliminatedTesting:
// Good: Clear test names and structure
#[test]
fn given_valid_url_when_adding_bookmark_then_succeeds() {
// Arrange
let test_container = TestServiceContainer::new();
let service = test_container.bookmark_service.clone();
// Act
let result = service.add_bookmark(
"https://example.com",
&["test"],
Some("Test Bookmark"),
None,
false,
);
// Assert
assert!(result.is_ok());
}Code Documentation:
/// Retrieves a bookmark by its ID.
///
/// # Arguments
/// * `id` - The unique identifier of the bookmark
///
/// # Returns
/// Returns the bookmark if found, or an error if not found or inaccessible.
///
/// # Errors
/// * `DomainError::NotFound` - Bookmark with given ID doesn't exist
/// * `DomainError::DatabaseError` - Database access failed
pub fn get_bookmark(&self, id: i32) -> DomainResult<Bookmark> {
// Implementation
}Wiki Documentation:
- Update wiki pages for user-facing changes
- Add examples for new features
- Update troubleshooting for new issues
- Keep documentation in sync with code
PR Description Should Include:
- Clear description of changes
- Motivation and context
- Related issues (closes #123)
- Breaking changes (if any)
- Testing performed
Before Submitting:
- ✅ All tests pass (
make test) - ✅ Code formatted (
make format) - ✅ No clippy warnings (
make lint) - ✅ Documentation updated
- ✅ Commit messages are clear
Review Process:
- Automated CI checks must pass
- Code review by maintainers
- Address feedback
- Final approval and merge
Follow Semantic Versioning (SemVer):
- Major: Breaking changes (e.g., 4.0.0 → 5.0.0)
- Minor: New features, backward compatible (e.g., 4.31.0 → 4.32.0)
- Patch: Bug fixes (e.g., 4.31.0 → 4.31.1)
1. Prepare release:
- Update CHANGELOG.md
- Update version in Cargo.toml
- Run full test suite
- Test on multiple platforms
2. Create release:
# Tag version
git tag -a v4.32.0 -m "Release 4.32.0"
git push origin v4.32.0
# GitHub Actions handles:
# - Building binaries
# - Publishing to crates.io
# - Creating GitHub release
# - Building Python wheels3. Verify release:
- Check crates.io publication
- Verify GitHub release artifacts
- Test installation from crates.io
Domain Layer (innermost):
// Pure business logic, no external dependencies
pub trait BookmarkRepository: Send + Sync {
fn get_bookmark(&self, id: i32) -> DomainResult<Bookmark>;
fn add_bookmark(&self, bookmark: &Bookmark) -> DomainResult<i32>;
}Application Layer:
// Use cases and orchestration
pub trait BookmarkService: Send + Sync {
fn get_all_bookmarks(&self, limit: Option<i32>, offset: Option<i32>)
-> ApplicationResult<Vec<Bookmark>>;
}Infrastructure Layer:
// External system implementations
pub struct SqliteBookmarkRepository {
pool: Pool<ConnectionManager<SqliteConnection>>,
}
impl BookmarkRepository for SqliteBookmarkRepository {
fn get_bookmark(&self, id: i32) -> DomainResult<Bookmark> {
// SQLite-specific implementation
}
}main.rs
↓
ServiceContainer (composition root)
↓ Creates all services with dependencies
CLI Commands
↓ Receives ServiceContainer + Settings
Application Services
↓ Business logic orchestration
Domain Entities
↓ Pure business logic
Infrastructure
↓ External systems
Layer-Specific Errors:
-
DomainError- Business rule violations -
ApplicationError- Use case failures -
InfrastructureError- External system errors -
CliError- User interface errors
Error Conversion:
// Infrastructure → Domain
impl From<diesel::result::Error> for DomainError {
fn from(err: diesel::result::Error) -> Self {
DomainError::DatabaseError(err.to_string())
}
}
// Domain → Application
impl From<DomainError> for ApplicationError {
fn from(err: DomainError) -> Self {
ApplicationError::DomainError(err)
}
}Architecture:
-
BkmrLspBackend- Main LSP server - tower-lsp for protocol handling
- tokio runtime for async operations
- Sync bkmr services wrapped in
spawn_blocking
Key Files:
-
src/lsp/server.rs- LSP backend implementation -
src/lsp/services/- LSP-specific services -
src/lsp/domain/- Language registry and mappings
Principles:
- Test behavior, not implementation
- Use dependency injection for mockability
- Single-threaded execution for reliability
- Arrange/Act/Assert structure
Test Categories:
- Unit tests for business logic
- Integration tests for CLI commands
- LSP tests with Python scripts
- Documentation tests for examples
Database:
- Connection pooling (r2d2, max_size=15)
- FTS5 for efficient search
- Indexed columns for common queries
Memory:
- Arc for shared ownership
- Lazy loading for large content
- Efficient string handling
Concurrency:
- Single-threaded database access (SQLite limitation)
- Arc for thread-safe sharing
- No global mutable state
Resources:
- GitHub: https://github.com/sysid/bkmr
- Issues: https://github.com/sysid/bkmr/issues
- Discussions: https://github.com/sysid/bkmr/discussions
- Wiki: https://github.com/sysid/bkmr/wiki
Getting Help:
- Check wiki for documentation
- Search existing issues
- Open new issue with details
- Join discussions
- Installation - Installation instructions
- Basic Usage - Using bkmr
- Troubleshooting - Common issues
- Configuration - Configuration details
- Editor Integration - LSP implementation
bkmr Documentation