18 KiB
18 KiB
Search Command Implementation Guide
Overview
The search command needs to be enhanced to use custom package search using libapt-pkg instead of relying on the apt search command, similar to how rpm-ostree implements its own search functionality.
Current Implementation Status
- ✅ Basic search command exists in apt-ostree
- ❌ Currently relies on
apt searchcommand - ❌ Missing custom libapt-pkg integration
- ❌ Missing name and description search
- ❌ Missing proper result formatting
Implementation Requirements
Phase 1: Custom Search Implementation
Files to Modify:
src/main.rs- Add search command optionssrc/apt.rs- Add custom search functionalitysrc/daemon.rs- Add search D-Bus method
Implementation Steps:
1.1 Update CLI Options (src/main.rs)
#[derive(Debug, Parser)]
pub struct SearchOpts {
/// Search query
query: String,
/// Search in package descriptions
#[arg(short = 'd', long)]
description: bool,
/// Search in package names only
#[arg(short = 'n', long)]
name_only: bool,
/// Show package details
#[arg(short = 'v', long)]
verbose: bool,
/// Output JSON format
#[arg(long)]
json: bool,
/// Limit number of results
#[arg(short = 'l', long)]
limit: Option<usize>,
/// Case insensitive search
#[arg(short = 'i', long)]
ignore_case: bool,
/// Search in installed packages only
#[arg(long)]
installed_only: bool,
/// Search in available packages only
#[arg(long)]
available_only: bool,
}
1.2 Add Custom Search Implementation (src/apt.rs)
use rust_apt::cache::Cache;
use rust_apt::package::Package;
use regex::Regex;
impl AptManager {
pub async fn search_packages(&self, query: &str, opts: &SearchOpts) -> Result<Vec<SearchResult>, Box<dyn std::error::Error>> {
// 1. Initialize APT cache
let cache = Cache::new()?;
// 2. Prepare search query
let search_query = if opts.ignore_case {
query.to_lowercase()
} else {
query.to_string()
};
// 3. Compile regex pattern
let pattern = if opts.ignore_case {
Regex::new(&format!("(?i){}", regex::escape(&search_query)))?
} else {
Regex::new(®ex::escape(&search_query))?
};
// 4. Search packages
let mut results = Vec::new();
for package in cache.packages() {
if let Some(pkg) = package {
// Check if package matches search criteria
if self.matches_search_criteria(pkg, &pattern, opts).await? {
let result = self.create_search_result(pkg, opts).await?;
results.push(result);
}
}
}
// 5. Sort results by relevance
results.sort_by(|a, b| {
// Sort by exact name matches first, then by relevance score
let a_exact = a.name == search_query;
let b_exact = b.name == search_query;
match (a_exact, b_exact) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => b.relevance_score.cmp(&a.relevance_score),
}
});
// 6. Apply limit if specified
if let Some(limit) = opts.limit {
results.truncate(limit);
}
Ok(results)
}
async fn matches_search_criteria(&self, package: &Package, pattern: &Regex, opts: &SearchOpts) -> Result<bool, Box<dyn std::error::Error>> {
// Check installed/available filter
if opts.installed_only && !package.is_installed() {
return Ok(false);
}
if opts.available_only && package.is_installed() {
return Ok(false);
}
// Search in package name
let name = package.name();
if pattern.is_match(name) {
return Ok(true);
}
// Search in package description if requested
if opts.description || !opts.name_only {
if let Some(description) = package.long_description() {
if pattern.is_match(description) {
return Ok(true);
}
}
if let Some(short_description) = package.short_description() {
if pattern.is_match(short_description) {
return Ok(true);
}
}
}
Ok(false)
}
async fn create_search_result(&self, package: &Package, opts: &SearchOpts) -> Result<SearchResult, Box<dyn std::error::Error>> {
let name = package.name().to_string();
let version = package.candidate_version()
.map(|v| v.version().to_string())
.unwrap_or_else(|| "unknown".to_string());
let description = if opts.verbose {
package.long_description().unwrap_or_else(|| "No description available".to_string())
} else {
package.short_description().unwrap_or_else(|| "No description available".to_string())
};
let architecture = package.candidate_version()
.and_then(|v| v.architecture())
.unwrap_or_else(|| "unknown".to_string());
let installed_version = if package.is_installed() {
package.installed_version()
.map(|v| v.version().to_string())
} else {
None
};
let size = package.candidate_version()
.and_then(|v| v.size())
.unwrap_or(0);
// Calculate relevance score
let relevance_score = self.calculate_relevance_score(package, opts).await?;
Ok(SearchResult {
name,
version,
description,
architecture,
installed_version,
size,
relevance_score,
is_installed: package.is_installed(),
})
}
async fn calculate_relevance_score(&self, package: &Package, opts: &SearchOpts) -> Result<u32, Box<dyn std::error::Error>> {
let mut score = 0;
let query = opts.query.to_lowercase();
let name = package.name().to_lowercase();
// Exact name match gets highest score
if name == query {
score += 1000;
}
// Name starts with query
if name.starts_with(&query) {
score += 500;
}
// Name contains query
if name.contains(&query) {
score += 100;
}
// Description contains query
if !opts.name_only {
if let Some(description) = package.short_description() {
let desc_lower = description.to_lowercase();
if desc_lower.contains(&query) {
score += 50;
}
}
}
// Installed packages get slight bonus
if package.is_installed() {
score += 10;
}
Ok(score)
}
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub name: String,
pub version: String,
pub description: String,
pub architecture: String,
pub installed_version: Option<String>,
pub size: u64,
pub relevance_score: u32,
pub is_installed: bool,
}
Phase 2: D-Bus Integration
Files to Modify:
src/daemon.rs- Add search D-Bus methodsrc/client.rs- Add search client method
Implementation Steps:
2.1 Add Search D-Bus Method (src/daemon.rs)
#[dbus_interface(name = "org.aptostree.dev")]
impl AptOstreeDaemon {
/// Search for packages
async fn search_packages(&self, query: String, options: HashMap<String, Value>) -> Result<Vec<SearchResult>, Box<dyn std::error::Error>> {
let apt_manager = AptManager::new().await?;
// Convert options to SearchOpts
let opts = SearchOpts {
query,
description: options.get("description").and_then(|v| v.as_bool()).unwrap_or(false),
name_only: options.get("name-only").and_then(|v| v.as_bool()).unwrap_or(false),
verbose: options.get("verbose").and_then(|v| v.as_bool()).unwrap_or(false),
json: options.get("json").and_then(|v| v.as_bool()).unwrap_or(false),
limit: options.get("limit").and_then(|v| v.as_u64()).map(|l| l as usize),
ignore_case: options.get("ignore-case").and_then(|v| v.as_bool()).unwrap_or(false),
installed_only: options.get("installed-only").and_then(|v| v.as_bool()).unwrap_or(false),
available_only: options.get("available-only").and_then(|v| v.as_bool()).unwrap_or(false),
};
let results = apt_manager.search_packages(&opts.query, &opts).await?;
Ok(results)
}
}
2.2 Add Search Client Method (src/client.rs)
impl AptOstreeClient {
pub async fn search_packages(&self, query: &str, opts: &SearchOpts) -> Result<Vec<SearchResult>, Box<dyn std::error::Error>> {
// Try daemon first
if let Ok(results) = self.search_packages_via_daemon(query, opts).await {
return Ok(results);
}
// Fallback to direct APT manager
let apt_manager = AptManager::new().await?;
apt_manager.search_packages(query, opts).await
}
async fn search_packages_via_daemon(&self, query: &str, opts: &SearchOpts) -> Result<Vec<SearchResult>, Box<dyn std::error::Error>> {
let mut options = HashMap::new();
options.insert("description".to_string(), Value::Bool(opts.description));
options.insert("name-only".to_string(), Value::Bool(opts.name_only));
options.insert("verbose".to_string(), Value::Bool(opts.verbose));
options.insert("json".to_string(), Value::Bool(opts.json));
options.insert("ignore-case".to_string(), Value::Bool(opts.ignore_case));
options.insert("installed-only".to_string(), Value::Bool(opts.installed_only));
options.insert("available-only".to_string(), Value::Bool(opts.available_only));
if let Some(limit) = opts.limit {
options.insert("limit".to_string(), Value::U64(limit as u64));
}
// Call daemon method
let proxy = self.get_dbus_proxy().await?;
let results: Vec<SearchResult> = proxy.search_packages(query.to_string(), options).await?;
Ok(results)
}
}
Phase 3: Result Formatting
Files to Modify:
src/formatting.rs- Add search result formattingsrc/main.rs- Add search command formatting
Implementation Steps:
3.1 Add Search Result Formatting (src/formatting.rs)
impl SearchFormatter {
pub fn format_search_results(&self, results: &[SearchResult], opts: &SearchOpts) -> String {
if opts.json {
self.format_json(results)
} else {
self.format_text(results, opts)
}
}
fn format_json(&self, results: &[SearchResult]) -> String {
let json_results: Vec<serde_json::Value> = results
.iter()
.map(|r| {
let mut obj = serde_json::Map::new();
obj.insert("name".to_string(), Value::String(r.name.clone()));
obj.insert("version".to_string(), Value::String(r.version.clone()));
obj.insert("description".to_string(), Value::String(r.description.clone()));
obj.insert("architecture".to_string(), Value::String(r.architecture.clone()));
obj.insert("size".to_string(), Value::Number(r.size.into()));
obj.insert("is_installed".to_string(), Value::Bool(r.is_installed));
obj.insert("relevance_score".to_string(), Value::Number(r.relevance_score.into()));
if let Some(ref installed_version) = r.installed_version {
obj.insert("installed_version".to_string(), Value::String(installed_version.clone()));
}
Value::Object(obj)
})
.collect();
serde_json::to_string_pretty(&Value::Array(json_results)).unwrap()
}
fn format_text(&self, results: &[SearchResult], opts: &SearchOpts) -> String {
let mut output = String::new();
if results.is_empty() {
output.push_str("No packages found matching the search criteria.\n");
return output;
}
// Print header
output.push_str(&format!("Found {} packages:\n\n", results.len()));
// Print results
for result in results {
output.push_str(&self.format_single_result(result, opts));
output.push('\n');
}
output
}
fn format_single_result(&self, result: &SearchResult, opts: &SearchOpts) -> String {
let mut output = String::new();
// Package name and version
let status_indicator = if result.is_installed { "[installed]" } else { "" };
output.push_str(&format!("{}/{} {}\n", result.name, result.architecture, status_indicator));
// Version information
if let Some(ref installed_version) = result.installed_version {
if installed_version != &result.version {
output.push_str(&format!(" Installed: {}\n", installed_version));
output.push_str(&format!(" Available: {}\n", result.version));
} else {
output.push_str(&format!(" Version: {}\n", result.version));
}
} else {
output.push_str(&format!(" Version: {}\n", result.version));
}
// Size information
if result.size > 0 {
let size_mb = result.size as f64 / 1024.0 / 1024.0;
output.push_str(&format!(" Size: {:.1} MB\n", size_mb));
}
// Description
if !opts.name_only {
output.push_str(&format!(" Description: {}\n", result.description));
}
output
}
}
pub struct SearchFormatter;
3.2 Update Main Search Command (src/main.rs)
async fn search_command(opts: SearchOpts) -> Result<(), Box<dyn std::error::Error>> {
// 1. Validate options
if opts.installed_only && opts.available_only {
return Err("Cannot specify both --installed-only and --available-only".into());
}
// 2. Perform search
let client = AptOstreeClient::new().await?;
let results = client.search_packages(&opts.query, &opts).await?;
// 3. Format and display results
let formatter = SearchFormatter;
let output = formatter.format_search_results(&results, &opts);
println!("{}", output);
Ok(())
}
Testing Strategy
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_search_package_matching() {
let apt_manager = AptManager::new().await.unwrap();
let opts = SearchOpts {
query: "test".to_string(),
description: false,
name_only: true,
verbose: false,
json: false,
limit: None,
ignore_case: true,
installed_only: false,
available_only: false,
};
let results = apt_manager.search_packages("test", &opts).await.unwrap();
// Test based on available packages
}
#[tokio::test]
async fn test_search_result_formatting() {
let results = vec![
SearchResult {
name: "test-package".to_string(),
version: "1.0-1".to_string(),
description: "A test package".to_string(),
architecture: "amd64".to_string(),
installed_version: None,
size: 1024,
relevance_score: 100,
is_installed: false,
}
];
let formatter = SearchFormatter;
let opts = SearchOpts {
query: "test".to_string(),
description: false,
name_only: false,
verbose: false,
json: false,
limit: None,
ignore_case: false,
installed_only: false,
available_only: false,
};
let output = formatter.format_search_results(&results, &opts);
assert!(output.contains("test-package"));
assert!(output.contains("A test package"));
}
}
Integration Tests
#[tokio::test]
async fn test_search_command_integration() {
let opts = SearchOpts {
query: "htop".to_string(),
description: false,
name_only: false,
verbose: false,
json: false,
limit: Some(10),
ignore_case: true,
installed_only: false,
available_only: false,
};
let result = search_command(opts).await;
assert!(result.is_ok());
}
Error Handling
Files to Modify:
src/error.rs- Add search-specific errors
Implementation:
#[derive(Debug, thiserror::Error)]
pub enum SearchError {
#[error("Invalid search query: {0}")]
InvalidQuery(String),
#[error("Failed to initialize APT cache: {0}")]
CacheError(String),
#[error("Failed to compile search pattern: {0}")]
PatternError(String),
#[error("No packages found matching criteria")]
NoResults,
#[error("Search requires valid APT configuration")]
AptConfigError,
}
impl From<SearchError> for Box<dyn std::error::Error> {
fn from(err: SearchError) -> Self {
Box::new(err)
}
}
Dependencies to Add
Add to Cargo.toml:
[dependencies]
regex = "1.0"
serde_json = "1.0"
Implementation Checklist
- Add CLI options for search command
- Implement custom search using libapt-pkg
- Add name and description search functionality
- Implement relevance scoring
- Add D-Bus integration for search
- Add result formatting (text and JSON)
- Add filtering options (installed/available)
- Add comprehensive error handling
- Write unit and integration tests
- Update documentation
References
- rpm-ostree search implementation patterns
- libapt-pkg search functionality
- APT package cache management
- Regular expression search patterns