-
Notifications
You must be signed in to change notification settings - Fork 9
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 # Release
1. 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.db
2. 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/Neovim
bkmr/
├── 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 --nocapture
Critical 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=1
Integration Tests (tests/
directory):
# Run integration tests
cargo test --test '*' -- --test-threads=1
Documentation Tests (embedded in code):
# Included in cargo test
# Tests example code in doc comments
TestServiceContainer (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 --quiet
Full test suite:
make test
With 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 database
Format code:
cargo fmt
cargo fmt --check # Check without modifying
Linting:
cargo clippy --all-targets --all-features
cargo clippy --fix # Auto-fix warnings
Build 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 --release
Run 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 revert
Migration 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.log
Test 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.py
Test basic LSP connectivity:
# Echo initialize request
echo '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}' | ./target/debug/bkmr lsp
# Should return JSON with capabilities
Search not working:
# Debug search execution
RUST_LOG=bkmr::application::services::bookmark_service=debug cargo run -- search "test" 2>&1 | grep -i search
Template not interpolating:
# Debug template rendering
RUST_LOG=bkmr::application::services::template_service=debug cargo run -- open <id> 2>&1 | grep -i template
Database 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-fix
2. 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 committing
4. 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 performed
Rust Conventions:
- Follow
rustfmt
formatting (runmake format
) - Use snake_case for functions and variables
- Use PascalCase for types and traits
- Add documentation comments for public APIs
- Use
thiserror
for 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 eliminated
Testing:
// 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 wheels
3. 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