metis_docs_cli/commands/
search.rs1use crate::commands::list::OutputFormat;
2use crate::workspace;
3use anyhow::Result;
4use clap::Args;
5use metis_core::dal::database::models::Document;
6use metis_core::{Application, Database};
7use serde::Serialize;
8
9#[derive(Args)]
10pub struct SearchCommand {
11 pub query: String,
13
14 #[arg(short = 'l', long, default_value = "20")]
16 pub limit: usize,
17
18 #[arg(short = 'f', long, value_enum, default_value = "table")]
20 pub format: OutputFormat,
21}
22
23#[derive(Serialize)]
25struct SearchResultOutput {
26 code: String,
27 title: String,
28 #[serde(rename = "type")]
29 doc_type: String,
30}
31
32impl SearchCommand {
33 pub async fn execute(&self) -> Result<()> {
34 let (workspace_exists, metis_dir) = workspace::has_metis_vault();
36 if !workspace_exists {
37 anyhow::bail!("Not in a Metis workspace. Run 'metis init' to create one.");
38 }
39 let metis_dir = metis_dir.unwrap();
40
41 let db_path = metis_dir.join("metis.db");
43 let database = Database::new(db_path.to_str().unwrap())
44 .map_err(|e| anyhow::anyhow!("Failed to open database for sync: {}", e))?;
45 let app = Application::new(database);
46 app.sync_directory(&metis_dir)
47 .await
48 .map_err(|e| anyhow::anyhow!("Failed to sync workspace: {}", e))?;
49
50 let database = Database::new(db_path.to_str().unwrap())
52 .map_err(|e| anyhow::anyhow!("Failed to open database: {}", e))?;
53 let mut app = Application::new(database);
54
55 let results = self.perform_search(&mut app, &self.query)?;
57
58 let limited_results: Vec<_> = results.into_iter().take(self.limit).collect();
60
61 if limited_results.is_empty() {
63 match self.format {
64 OutputFormat::Json => println!("[]"),
65 _ => println!("No documents found for query: \"{}\"", self.query),
66 }
67 return Ok(());
68 }
69
70 match self.format {
71 OutputFormat::Table => self.display_table(&limited_results),
72 OutputFormat::Compact => self.display_compact(&limited_results),
73 OutputFormat::Json => self.display_json(&limited_results),
74 }
75
76 Ok(())
77 }
78
79 fn perform_search(&self, app: &mut Application, query: &str) -> Result<Vec<Document>> {
80 app.with_database(|db_service| db_service.search_documents(query))
81 .map_err(|e| anyhow::anyhow!("Search failed: {}", e))
82 }
83
84 fn display_table(&self, documents: &[Document]) {
87 println!(
88 "\n{:<14} {:<60} {:<12}",
89 "Code", "Title", "Type"
90 );
91 println!("{}", "-".repeat(88));
92
93 for doc in documents {
94 println!(
95 "{:<14} {:<60} {:<12}",
96 doc.short_code,
97 truncate(&doc.title, 58),
98 doc.document_type
99 );
100 }
101
102 println!(
103 "\nFound {} document(s) for \"{}\"",
104 documents.len(),
105 self.query
106 );
107 }
108
109 fn display_compact(&self, documents: &[Document]) {
112 for doc in documents {
113 println!("{} {} {}", doc.short_code, doc.document_type, doc.title);
114 }
115 }
116
117 fn display_json(&self, documents: &[Document]) {
119 let output: Vec<SearchResultOutput> = documents
120 .iter()
121 .map(|doc| SearchResultOutput {
122 code: doc.short_code.clone(),
123 title: doc.title.clone(),
124 doc_type: doc.document_type.clone(),
125 })
126 .collect();
127
128 match serde_json::to_string_pretty(&output) {
129 Ok(json) => println!("{}", json),
130 Err(e) => eprintln!("Error serializing to JSON: {}", e),
131 }
132 }
133}
134
135fn truncate(s: &str, max_len: usize) -> String {
137 if s.len() <= max_len {
138 s.to_string()
139 } else {
140 format!("{}...", &s[..max_len.saturating_sub(3)])
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_truncate() {
150 assert_eq!(truncate("short", 10), "short");
151 assert_eq!(truncate("this is a very long string", 10), "this is...");
152 assert_eq!(truncate("exactly_10", 10), "exactly_10");
153 }
154}