turbovault_tools/
metadata_tools.rs1use serde_json::{Value, json};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::sync::Arc;
7use turbovault_core::prelude::*;
8use turbovault_vault::VaultManager;
9
10#[derive(Debug, Clone)]
12pub enum QueryFilter {
13 Equals(String, Value),
14 GreaterThan(String, f64),
15 LessThan(String, f64),
16 Contains(String, String),
17 And(Vec<QueryFilter>),
18 Or(Vec<QueryFilter>),
19}
20
21impl QueryFilter {
22 fn matches(&self, metadata: &HashMap<String, Value>) -> bool {
24 match self {
25 QueryFilter::Equals(key, expected) => metadata.get(key) == Some(expected),
26 QueryFilter::GreaterThan(key, threshold) => {
27 if let Some(Value::Number(num)) = metadata.get(key)
28 && let Some(n) = num.as_f64()
29 {
30 return n > *threshold;
31 }
32 false
33 }
34 QueryFilter::LessThan(key, threshold) => {
35 if let Some(Value::Number(num)) = metadata.get(key)
36 && let Some(n) = num.as_f64()
37 {
38 return n < *threshold;
39 }
40 false
41 }
42 QueryFilter::Contains(key, substring) => metadata
43 .get(key)
44 .and_then(|v| v.as_str())
45 .map(|s| s.contains(substring))
46 .unwrap_or(false),
47 QueryFilter::And(filters) => filters.iter().all(|f| f.matches(metadata)),
48 QueryFilter::Or(filters) => filters.iter().any(|f| f.matches(metadata)),
49 }
50 }
51}
52
53fn parse_query(pattern: &str) -> Result<QueryFilter> {
60 let pattern = pattern.trim();
61
62 if let Some(colon_pos) = pattern.find(':') {
64 let key = pattern[..colon_pos].trim();
65 let rest = pattern[colon_pos + 1..].trim();
66
67 if rest.starts_with('"') && rest.ends_with('"') {
69 let value = rest[1..rest.len() - 1].to_string();
70 return Ok(QueryFilter::Equals(key.to_string(), Value::String(value)));
71 }
72
73 if rest.starts_with("contains(") && rest.ends_with(")") {
75 let inner = &rest[9..rest.len() - 1];
76 if inner.starts_with('"') && inner.ends_with('"') {
77 let substring = inner[1..inner.len() - 1].to_string();
78 return Ok(QueryFilter::Contains(key.to_string(), substring));
79 }
80 }
81 }
82
83 if let Some(gt_pos) = pattern.find(" > ") {
85 let key = pattern[..gt_pos].trim();
86 let rest = pattern[gt_pos + 3..].trim();
87 if let Ok(num) = rest.parse::<f64>() {
88 return Ok(QueryFilter::GreaterThan(key.to_string(), num));
89 }
90 }
91
92 if let Some(lt_pos) = pattern.find(" < ") {
94 let key = pattern[..lt_pos].trim();
95 let rest = pattern[lt_pos + 3..].trim();
96 if let Ok(num) = rest.parse::<f64>() {
97 return Ok(QueryFilter::LessThan(key.to_string(), num));
98 }
99 }
100
101 Err(Error::config_error(format!(
102 "Unable to parse query pattern: {}",
103 pattern
104 )))
105}
106
107pub struct MetadataTools {
109 pub manager: Arc<VaultManager>,
110}
111
112impl MetadataTools {
113 pub fn new(manager: Arc<VaultManager>) -> Self {
115 Self { manager }
116 }
117
118 pub async fn query_metadata(&self, pattern: &str) -> Result<Value> {
120 let filter = parse_query(pattern)?;
121
122 let files = self.manager.scan_vault().await?;
124 let mut matches = Vec::new();
125
126 for file_path in files {
127 if !file_path.ends_with(".md") {
128 continue;
129 }
130
131 match self.manager.parse_file(&file_path).await {
133 Ok(vault_file) => {
134 if let Some(frontmatter) = vault_file.frontmatter
135 && filter.matches(&frontmatter.data)
136 {
137 let display_path = file_path
138 .strip_prefix(self.manager.vault_path())
139 .map(|p| p.to_string_lossy().to_string())
140 .unwrap_or_else(|_| file_path.to_string_lossy().to_string());
141
142 matches.push(json!({
143 "path": display_path,
144 "metadata": frontmatter.data
145 }));
146 }
147 }
148 Err(_) => {
149 continue;
151 }
152 }
153 }
154
155 Ok(json!({
156 "query": pattern,
157 "matched": matches.len(),
158 "files": matches
159 }))
160 }
161
162 pub async fn get_metadata_value(&self, file: &str, key: &str) -> Result<Value> {
164 let file_path = PathBuf::from(file);
166
167 let vault_file = self.manager.parse_file(&file_path).await?;
169
170 let frontmatter = vault_file
172 .frontmatter
173 .ok_or_else(|| Error::not_found("No frontmatter in file".to_string()))?;
174
175 let mut current: &Value = &Value::Object(serde_json::Map::from_iter(
177 frontmatter.data.iter().map(|(k, v)| (k.clone(), v.clone())),
178 ));
179
180 for part in key.split('.') {
181 current = current
182 .get(part)
183 .ok_or_else(|| Error::not_found(format!("Key not found: {}", key)))?;
184 }
185
186 Ok(json!({
187 "file": file,
188 "key": key,
189 "value": current
190 }))
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_parse_query_equals_string() {
200 let filter = parse_query(r#"status: "draft""#).unwrap();
201 matches!(filter, QueryFilter::Equals(_, _));
202 }
203
204 #[test]
205 fn test_parse_query_greater_than() {
206 let filter = parse_query("priority > 3").unwrap();
207 matches!(filter, QueryFilter::GreaterThan(_, _));
208 }
209
210 #[test]
211 fn test_parse_query_less_than() {
212 let filter = parse_query("priority < 5").unwrap();
213 matches!(filter, QueryFilter::LessThan(_, _));
214 }
215
216 #[test]
217 fn test_parse_query_contains() {
218 let filter = parse_query(r#"tags: contains("important")"#).unwrap();
219 matches!(filter, QueryFilter::Contains(_, _));
220 }
221
222 #[test]
223 fn test_filter_matches_equals() {
224 let mut metadata = HashMap::new();
225 metadata.insert("status".to_string(), Value::String("draft".to_string()));
226
227 let filter = QueryFilter::Equals("status".to_string(), Value::String("draft".to_string()));
228 assert!(filter.matches(&metadata));
229
230 let filter_no_match =
231 QueryFilter::Equals("status".to_string(), Value::String("active".to_string()));
232 assert!(!filter_no_match.matches(&metadata));
233 }
234
235 #[test]
236 fn test_filter_matches_greater_than() {
237 let mut metadata = HashMap::new();
238 metadata.insert(
239 "priority".to_string(),
240 Value::Number(serde_json::Number::from(5)),
241 );
242
243 let filter = QueryFilter::GreaterThan("priority".to_string(), 3.0);
244 assert!(filter.matches(&metadata));
245
246 let filter_no_match = QueryFilter::GreaterThan("priority".to_string(), 5.0);
247 assert!(!filter_no_match.matches(&metadata));
248 }
249
250 #[test]
251 fn test_filter_matches_contains() {
252 let mut metadata = HashMap::new();
253 metadata.insert(
254 "tags".to_string(),
255 Value::String("important task".to_string()),
256 );
257
258 let filter = QueryFilter::Contains("tags".to_string(), "important".to_string());
259 assert!(filter.matches(&metadata));
260
261 let filter_no_match = QueryFilter::Contains("tags".to_string(), "urgent".to_string());
262 assert!(!filter_no_match.matches(&metadata));
263 }
264
265 #[test]
266 fn test_filter_matches_and() {
267 let mut metadata = HashMap::new();
268 metadata.insert("status".to_string(), Value::String("draft".to_string()));
269 metadata.insert(
270 "priority".to_string(),
271 Value::Number(serde_json::Number::from(5)),
272 );
273
274 let filter = QueryFilter::And(vec![
275 QueryFilter::Equals("status".to_string(), Value::String("draft".to_string())),
276 QueryFilter::GreaterThan("priority".to_string(), 3.0),
277 ]);
278 assert!(filter.matches(&metadata));
279
280 let filter_no_match = QueryFilter::And(vec![
281 QueryFilter::Equals("status".to_string(), Value::String("draft".to_string())),
282 QueryFilter::GreaterThan("priority".to_string(), 5.0),
283 ]);
284 assert!(!filter_no_match.matches(&metadata));
285 }
286
287 #[test]
288 fn test_filter_matches_or() {
289 let mut metadata = HashMap::new();
290 metadata.insert("status".to_string(), Value::String("draft".to_string()));
291
292 let filter = QueryFilter::Or(vec![
293 QueryFilter::Equals("status".to_string(), Value::String("active".to_string())),
294 QueryFilter::Equals("status".to_string(), Value::String("draft".to_string())),
295 ]);
296 assert!(filter.matches(&metadata));
297
298 let filter_no_match = QueryFilter::Or(vec![
299 QueryFilter::Equals("status".to_string(), Value::String("archived".to_string())),
300 QueryFilter::Equals("status".to_string(), Value::String("active".to_string())),
301 ]);
302 assert!(!filter_no_match.matches(&metadata));
303 }
304}