obsidian_cli_inspector/
machine_contract.rs1use crate::{config::Config, db::Database, query};
2use anyhow::{Context, Result};
3use serde_json::Value;
4
5pub struct ResultDataBuilder;
6
7impl ResultDataBuilder {
8 fn empty_query_result() -> Value {
9 serde_json::json!({ "total": 0, "items": [] })
10 }
11
12 fn query_result(items: Vec<Value>) -> Value {
13 serde_json::json!({ "total": items.len(), "items": items })
14 }
15
16 pub fn build_query_result_data(
17 config: &Config,
18 command: &str,
19 params: &Value,
20 ) -> Result<Value> {
21 let db_path = config.database_path();
22 if !db_path.exists() {
23 anyhow::bail!(
24 "Database not found at: {}\nRun 'obsidian-cli-inspector index' to create and index the database first",
25 db_path.display()
26 );
27 }
28
29 let db = Database::open(&db_path)
30 .with_context(|| format!("Failed to open database at: {}", db_path.display()))?;
31
32 let stats = db.get_stats().context("Failed to get database stats")?;
34 if stats.note_count == 0 {
35 anyhow::bail!(
36 "Database is empty. Run 'obsidian-cli-inspector index' to index your vault first"
37 );
38 }
39
40 match command {
41 "search.notes" => {
42 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
43 let limit = params.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
44
45 let results = db
46 .conn()
47 .execute_query(|conn| query::search_chunks(conn, query, limit))
48 .context("Failed to execute search query")?;
49
50 let items = results
51 .iter()
52 .map(|result| {
53 serde_json::json!({
54 "chunk_id": result.chunk_id,
55 "note_id": result.note_id,
56 "note_path": result.note_path,
57 "note_title": result.note_title,
58 "heading_path": result.heading_path,
59 "chunk_text": result.chunk_text,
60 "rank": result.rank
61 })
62 })
63 .collect();
64
65 Ok(Self::query_result(items))
66 }
67 "search.backlinks" => {
68 let note = params.get("note").and_then(|v| v.as_str()).unwrap_or("");
69
70 let results = db
71 .conn()
72 .execute_query(|conn| query::get_backlinks(conn, note))
73 .context("Failed to get backlinks")?;
74
75 let items = results
76 .iter()
77 .map(|result| {
78 serde_json::json!({
79 "note_id": result.note_id,
80 "note_path": result.note_path,
81 "note_title": result.note_title,
82 "is_embed": result.is_embed,
83 "alias": result.alias,
84 "heading_ref": result.heading_ref,
85 "block_ref": result.block_ref
86 })
87 })
88 .collect();
89
90 Ok(Self::query_result(items))
91 }
92 "search.links" => {
93 let note = params.get("note").and_then(|v| v.as_str()).unwrap_or("");
94
95 let results = db
96 .conn()
97 .execute_query(|conn| query::get_forward_links(conn, note))
98 .context("Failed to get forward links")?;
99
100 let items = results
101 .iter()
102 .map(|result| {
103 serde_json::json!({
104 "note_id": result.note_id,
105 "note_path": result.note_path,
106 "note_title": result.note_title,
107 "is_embed": result.is_embed,
108 "alias": result.alias,
109 "heading_ref": result.heading_ref,
110 "block_ref": result.block_ref
111 })
112 })
113 .collect();
114
115 Ok(Self::query_result(items))
116 }
117 "search.unresolved" => {
118 let results = db
119 .conn()
120 .execute_query(query::get_unresolved_links)
121 .context("Failed to get unresolved links")?;
122
123 let items = results
124 .iter()
125 .map(|result| {
126 serde_json::json!({
127 "note_id": result.note_id,
128 "note_path": result.note_path,
129 "note_title": result.note_title,
130 "is_embed": result.is_embed,
131 "alias": result.alias,
132 "heading_ref": result.heading_ref,
133 "block_ref": result.block_ref
134 })
135 })
136 .collect();
137
138 Ok(Self::query_result(items))
139 }
140 "search.tags" => {
141 let list_all = params
142 .get("list")
143 .and_then(|v| v.as_bool())
144 .unwrap_or(false);
145 let tag = params.get("tag").and_then(|v| v.as_str());
146
147 if list_all || tag.is_none() {
148 let tags = db
149 .conn()
150 .execute_query(query::list_tags)
151 .context("Failed to list tags")?;
152
153 let items = tags
154 .iter()
155 .map(|tag_name| serde_json::json!({ "tag": tag_name }))
156 .collect();
157
158 Ok(Self::query_result(items))
159 } else if let Some(tag_name) = tag {
160 let results = db
161 .conn()
162 .execute_query(|conn| query::get_notes_by_tag(conn, tag_name))
163 .context("Failed to get notes by tag")?;
164
165 let items = results
166 .iter()
167 .map(|result| {
168 serde_json::json!({
169 "note_id": result.note_id,
170 "note_path": result.note_path,
171 "note_title": result.note_title,
172 "tags": result.tags
173 })
174 })
175 .collect();
176
177 Ok(Self::query_result(items))
178 } else {
179 Ok(Self::empty_query_result())
180 }
181 }
182 _ => Ok(Self::empty_query_result()),
183 }
184 }
185
186 pub fn build_view_stats_result_data(config: &Config) -> Value {
187 let db_path = config.database_path();
188 if !db_path.exists() {
189 return serde_json::json!({ "status": "success" });
190 }
191
192 let db = match Database::open(&db_path) {
193 Ok(db) => db,
194 Err(_) => return serde_json::json!({ "status": "success" }),
195 };
196
197 match db.get_stats() {
198 Ok(stats) => serde_json::json!({
199 "notes": stats.note_count,
200 "links": stats.link_count,
201 "tags": stats.tag_count,
202 "chunks": stats.chunk_count,
203 "unresolved_links": stats.unresolved_links
204 }),
205 Err(_) => serde_json::json!({ "status": "success" }),
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use tempfile::TempDir;
214
215 #[test]
216 fn test_empty_query_result() {
217 let result = ResultDataBuilder::empty_query_result();
218 assert_eq!(result.get("total").unwrap(), 0);
219 assert!(result.get("items").unwrap().is_array());
220 }
221
222 #[test]
223 fn test_query_result_with_items() {
224 let items = vec![serde_json::json!({"id": 1}), serde_json::json!({"id": 2})];
225 let result = ResultDataBuilder::query_result(items);
226 assert_eq!(result.get("total").unwrap(), 2);
227 assert!(result.get("items").unwrap().is_array());
228 }
229
230 #[test]
231 fn test_query_result_empty_items() {
232 let items: Vec<Value> = vec![];
233 let result = ResultDataBuilder::query_result(items);
234 assert_eq!(result.get("total").unwrap(), 0);
235 }
236
237 #[test]
238 fn test_build_query_result_data_no_database() {
239 let temp_dir = TempDir::new().unwrap();
240 let config = Config {
241 vault_path: temp_dir.path().to_path_buf(),
242 database_path: Some(temp_dir.path().join("nonexistent.db")),
243 log_path: None,
244 exclude: Default::default(),
245 search: Default::default(),
246 graph: Default::default(),
247 llm: None,
248 };
249
250 let params = serde_json::json!({});
251 let result = ResultDataBuilder::build_query_result_data(&config, "search.notes", ¶ms);
252 assert!(
253 result.is_err(),
254 "Should return error when database doesn't exist"
255 );
256 let err_msg = result.unwrap_err().to_string();
257 assert!(
258 err_msg.contains("Database not found"),
259 "Error should mention database not found"
260 );
261 }
262
263 #[test]
264 fn test_build_query_result_data_unknown_command() {
265 let temp_dir = TempDir::new().unwrap();
266 let config = Config {
267 vault_path: temp_dir.path().to_path_buf(),
268 database_path: Some(temp_dir.path().join("nonexistent.db")),
269 log_path: None,
270 exclude: Default::default(),
271 search: Default::default(),
272 graph: Default::default(),
273 llm: None,
274 };
275
276 let params = serde_json::json!({});
277 let result =
278 ResultDataBuilder::build_query_result_data(&config, "unknown.command", ¶ms);
279 assert!(
281 result.is_err(),
282 "Should return error when database doesn't exist"
283 );
284 }
285
286 #[test]
287 fn test_build_view_stats_result_data_no_database() {
288 let temp_dir = TempDir::new().unwrap();
289 let config = Config {
290 vault_path: temp_dir.path().to_path_buf(),
291 database_path: Some(temp_dir.path().join("nonexistent.db")),
292 log_path: None,
293 exclude: Default::default(),
294 search: Default::default(),
295 graph: Default::default(),
296 llm: None,
297 };
298
299 let result = ResultDataBuilder::build_view_stats_result_data(&config);
300 assert_eq!(result.get("status").unwrap(), "success");
301 }
302}