synaptic_deep/backend/
store.rs1use async_trait::async_trait;
2use regex::Regex;
3use std::collections::HashMap;
4use std::sync::Arc;
5use synaptic_core::{Store, SynapticError};
6
7use super::{Backend, DirEntry, GrepMatch, GrepOutputMode};
8
9pub struct StoreBackend {
14 store: Arc<dyn Store>,
15 namespace: Vec<String>,
16}
17
18impl StoreBackend {
19 pub fn new(store: Arc<dyn Store>, namespace: Vec<String>) -> Self {
20 Self { store, namespace }
21 }
22
23 fn ns_refs(&self) -> Vec<&str> {
24 self.namespace.iter().map(|s| s.as_str()).collect()
25 }
26}
27
28fn normalize_path(path: &str) -> String {
29 let trimmed = path.trim_matches('/');
30 if trimmed == "." {
31 String::new()
32 } else {
33 trimmed.to_string()
34 }
35}
36
37fn glob_to_regex(pattern: &str) -> String {
38 let mut regex = String::from("^");
39 let mut chars = pattern.chars().peekable();
40 while let Some(c) = chars.next() {
41 match c {
42 '*' => {
43 if chars.peek() == Some(&'*') {
44 chars.next();
45 if chars.peek() == Some(&'/') {
46 chars.next();
47 regex.push_str("(.*/)?");
48 } else {
49 regex.push_str(".*");
50 }
51 } else {
52 regex.push_str("[^/]*");
53 }
54 }
55 '?' => regex.push_str("[^/]"),
56 '.' => regex.push_str("\\."),
57 '{' => regex.push('('),
58 '}' => regex.push(')'),
59 ',' => regex.push('|'),
60 c => regex.push(c),
61 }
62 }
63 regex.push('$');
64 regex
65}
66
67#[async_trait]
68impl Backend for StoreBackend {
69 async fn ls(&self, path: &str) -> Result<Vec<DirEntry>, SynapticError> {
70 let ns = self.ns_refs();
71 let items = self.store.search(&ns, None, 10000).await?;
72 let prefix = normalize_path(path);
73 let prefix_with_slash = if prefix.is_empty() {
74 String::new()
75 } else {
76 format!("{}/", prefix)
77 };
78
79 let mut entries: HashMap<String, bool> = HashMap::new();
80 for item in items {
81 let rel = if prefix_with_slash.is_empty() {
82 item.key.clone()
83 } else if let Some(rel) = item.key.strip_prefix(&prefix_with_slash) {
84 rel.to_string()
85 } else {
86 continue;
87 };
88
89 if let Some(slash_pos) = rel.find('/') {
90 entries.insert(rel[..slash_pos].to_string(), true);
91 } else if !rel.is_empty() {
92 entries.entry(rel).or_insert(false);
93 }
94 }
95
96 let mut result: Vec<DirEntry> = entries
97 .into_iter()
98 .map(|(name, is_dir)| DirEntry {
99 name,
100 is_dir,
101 size: None,
102 })
103 .collect();
104 result.sort_by(|a, b| a.name.cmp(&b.name));
105 Ok(result)
106 }
107
108 async fn read_file(
109 &self,
110 path: &str,
111 offset: usize,
112 limit: usize,
113 ) -> Result<String, SynapticError> {
114 let ns = self.ns_refs();
115 let normalized = normalize_path(path);
116 let item = self
117 .store
118 .get(&ns, &normalized)
119 .await?
120 .ok_or_else(|| SynapticError::Tool(format!("file not found: {}", path)))?;
121
122 let content = item
123 .value
124 .get("content")
125 .and_then(|v| v.as_str())
126 .unwrap_or("");
127
128 let lines: Vec<&str> = content.lines().collect();
129 let total = lines.len();
130 if offset >= total {
131 return Ok(String::new());
132 }
133 let end = (offset + limit).min(total);
134 Ok(lines[offset..end].join("\n"))
135 }
136
137 async fn write_file(&self, path: &str, content: &str) -> Result<(), SynapticError> {
138 let ns = self.ns_refs();
139 let normalized = normalize_path(path);
140 self.store
141 .put(&ns, &normalized, serde_json::json!({ "content": content }))
142 .await
143 }
144
145 async fn edit_file(
146 &self,
147 path: &str,
148 old_text: &str,
149 new_text: &str,
150 replace_all: bool,
151 ) -> Result<(), SynapticError> {
152 let ns = self.ns_refs();
153 let normalized = normalize_path(path);
154 let item = self
155 .store
156 .get(&ns, &normalized)
157 .await?
158 .ok_or_else(|| SynapticError::Tool(format!("file not found: {}", path)))?;
159
160 let content = item
161 .value
162 .get("content")
163 .and_then(|v| v.as_str())
164 .unwrap_or("");
165
166 if !content.contains(old_text) {
167 return Err(SynapticError::Tool(format!(
168 "old_string not found in {}",
169 path
170 )));
171 }
172
173 let new_content = if replace_all {
174 content.replace(old_text, new_text)
175 } else {
176 content.replacen(old_text, new_text, 1)
177 };
178
179 self.store
180 .put(
181 &ns,
182 &normalized,
183 serde_json::json!({ "content": new_content }),
184 )
185 .await
186 }
187
188 async fn glob(&self, pattern: &str, base: &str) -> Result<Vec<String>, SynapticError> {
189 let ns = self.ns_refs();
190 let items = self.store.search(&ns, None, 10000).await?;
191 let base_normalized = normalize_path(base);
192
193 let regex_str = glob_to_regex(pattern);
194 let re = Regex::new(®ex_str)
195 .map_err(|e| SynapticError::Tool(format!("invalid glob pattern: {}", e)))?;
196
197 let mut matches = Vec::new();
198 for item in items {
199 let rel = if base_normalized.is_empty() {
200 item.key.clone()
201 } else if let Some(rel) = item.key.strip_prefix(&format!("{}/", base_normalized)) {
202 rel.to_string()
203 } else {
204 continue;
205 };
206 if re.is_match(&rel) {
207 matches.push(item.key);
208 }
209 }
210 matches.sort();
211 Ok(matches)
212 }
213
214 async fn grep(
215 &self,
216 pattern: &str,
217 path: Option<&str>,
218 file_glob: Option<&str>,
219 output_mode: GrepOutputMode,
220 ) -> Result<String, SynapticError> {
221 let ns = self.ns_refs();
222 let items = self.store.search(&ns, None, 10000).await?;
223 let re = Regex::new(pattern)
224 .map_err(|e| SynapticError::Tool(format!("invalid regex: {}", e)))?;
225 let glob_re = file_glob.and_then(|g| Regex::new(&glob_to_regex(g)).ok());
226 let base = path.map(normalize_path).unwrap_or_default();
227
228 let mut file_matches: Vec<GrepMatch> = Vec::new();
229 let mut match_files: Vec<String> = Vec::new();
230 let mut match_counts: HashMap<String, usize> = HashMap::new();
231
232 for item in items {
233 if !base.is_empty() && !item.key.starts_with(&base) {
234 continue;
235 }
236 if let Some(ref gre) = glob_re {
237 let rel = if base.is_empty() {
238 item.key.clone()
239 } else {
240 item.key
241 .strip_prefix(&format!("{}/", base))
242 .unwrap_or(&item.key)
243 .to_string()
244 };
245 if !gre.is_match(&rel) {
246 continue;
247 }
248 }
249
250 let content = item
251 .value
252 .get("content")
253 .and_then(|v| v.as_str())
254 .unwrap_or("");
255 let mut found = false;
256 for (line_num, line) in content.lines().enumerate() {
257 if re.is_match(line) {
258 found = true;
259 file_matches.push(GrepMatch {
260 file: item.key.clone(),
261 line_number: line_num + 1,
262 line: line.to_string(),
263 });
264 *match_counts.entry(item.key.clone()).or_insert(0) += 1;
265 }
266 }
267 if found {
268 match_files.push(item.key.clone());
269 }
270 }
271
272 match output_mode {
273 GrepOutputMode::FilesWithMatches => {
274 match_files.sort();
275 Ok(match_files.join("\n"))
276 }
277 GrepOutputMode::Content => {
278 file_matches
279 .sort_by(|a, b| a.file.cmp(&b.file).then(a.line_number.cmp(&b.line_number)));
280 Ok(file_matches
281 .iter()
282 .map(|m| format!("{}:{}:{}", m.file, m.line_number, m.line))
283 .collect::<Vec<_>>()
284 .join("\n"))
285 }
286 GrepOutputMode::Count => {
287 let mut counts: Vec<_> = match_counts.into_iter().collect();
288 counts.sort_by(|a, b| a.0.cmp(&b.0));
289 Ok(counts
290 .iter()
291 .map(|(f, c)| format!("{}:{}", f, c))
292 .collect::<Vec<_>>()
293 .join("\n"))
294 }
295 }
296 }
297}