Skip to main content

obsidian_cli_inspector/
machine_contract.rs

1use 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        // Check if database has been indexed
33        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", &params);
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", &params);
279        // Should still error because database doesn't exist
280        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}