1use std::collections::BTreeMap;
8use std::sync::mpsc::Receiver;
9
10use serde_json::Value;
11
12use crate::error::{Error, Result};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum SourceIdentity {
17 Inferred,
19 Borne,
21}
22
23impl SourceIdentity {
24 #[must_use]
25 pub const fn as_str(&self) -> &'static str {
26 match self {
27 SourceIdentity::Inferred => "inferred",
28 SourceIdentity::Borne => "borne",
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct SourceCapabilities {
36 pub identity: SourceIdentity,
37 pub write_through: bool,
38 pub watch: bool,
39}
40
41impl Default for SourceCapabilities {
42 fn default() -> Self {
43 Self {
44 identity: SourceIdentity::Inferred,
45 write_through: false,
46 watch: false,
47 }
48 }
49}
50
51fn js_truthy(v: Option<&Value>) -> bool {
53 match v {
54 None | Some(Value::Null) => false,
55 Some(Value::Bool(b)) => *b,
56 Some(Value::Number(n)) => n.as_f64().is_some_and(|f| f != 0.0 && !f.is_nan()),
57 Some(Value::String(s)) => !s.is_empty(),
58 Some(Value::Array(_) | Value::Object(_)) => true,
59 }
60}
61
62impl SourceCapabilities {
63 #[must_use]
66 pub fn from_json(v: Option<&Value>) -> Self {
67 let obj = v.and_then(Value::as_object);
68 Self {
69 identity: match obj.and_then(|o| o.get("identity")).and_then(Value::as_str) {
70 Some("borne") => SourceIdentity::Borne,
71 _ => SourceIdentity::Inferred,
72 },
73 write_through: js_truthy(obj.and_then(|o| o.get("writeThrough"))),
74 watch: js_truthy(obj.and_then(|o| o.get("watch"))),
75 }
76 }
77
78 #[must_use]
79 pub fn to_json(&self) -> Value {
80 serde_json::json!({
81 "identity": self.identity.as_str(),
82 "writeThrough": self.write_through,
83 "watch": self.watch,
84 })
85 }
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct SourceEntry {
91 pub path: String,
93 pub revision: String,
95 pub source_id: Option<String>,
97}
98
99impl SourceEntry {
100 #[must_use]
102 pub fn from_json(v: &Value) -> Option<Self> {
103 Some(Self {
104 path: v.get("path")?.as_str()?.to_owned(),
105 revision: match v.get("revision") {
106 Some(Value::String(s)) => s.clone(),
107 Some(other) if !other.is_null() => other.to_string(),
108 _ => String::new(),
109 },
110 source_id: v.get("sourceId").and_then(Value::as_str).map(str::to_owned),
111 })
112 }
113}
114
115impl SourceEntry {
116 #[must_use]
118 pub fn to_json(&self) -> Value {
119 let mut v = serde_json::json!({ "path": self.path, "revision": self.revision });
120 if let Some(id) = &self.source_id {
121 v["sourceId"] = Value::String(id.clone());
122 }
123 v
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
129pub struct SourceItem {
130 pub entry: SourceEntry,
131 pub content: String,
133}
134
135impl SourceItem {
136 #[must_use]
138 pub fn from_json(v: Option<&Value>) -> Option<Self> {
139 let v = v?;
140 if v.is_null() {
141 return None;
142 }
143 let entry = SourceEntry::from_json(v)?;
144 let content = match v.get("content") {
145 Some(Value::String(s)) => s.clone(),
146 _ => String::new(),
147 };
148 Some(Self { entry, content })
149 }
150
151 #[must_use]
153 pub fn to_json(&self) -> Value {
154 let mut v = self.entry.to_json();
155 v["content"] = Value::String(self.content.clone());
156 v
157 }
158}
159
160pub trait SyncSource {
162 fn capabilities(&self) -> SourceCapabilities;
163
164 fn enumerate(&mut self) -> Result<Vec<SourceEntry>>;
166
167 fn fetch(&mut self, path: &str) -> Result<Option<SourceItem>>;
169
170 fn write(&mut self, _path: &str, _content: &str) -> Result<()> {
172 Err(Error::Unsupported("write".to_owned()))
173 }
174
175 fn remove(&mut self, _path: &str) -> Result<()> {
177 Err(Error::Unsupported("remove".to_owned()))
178 }
179
180 fn watch(&mut self) -> Result<Receiver<Vec<String>>> {
183 Err(Error::Unsupported("watch".to_owned()))
184 }
185
186 fn unwatch(&mut self) -> Result<()> {
188 Ok(())
189 }
190
191 fn close(&mut self) -> Result<()> {
193 Ok(())
194 }
195}
196
197#[derive(Debug, Default)]
200pub struct MemSource {
201 pub caps: SourceCapabilities,
202 pub files: BTreeMap<String, String>,
203 pub log: Vec<(&'static str, String, String)>,
205 revisions: BTreeMap<String, u64>,
207 batches: Option<std::sync::mpsc::Sender<Vec<String>>>,
208}
209
210impl MemSource {
211 #[must_use]
212 pub fn new(caps: SourceCapabilities) -> Self {
213 Self {
214 caps,
215 ..Self::default()
216 }
217 }
218
219 #[must_use]
221 pub fn with_files(files: &[(&str, &str)]) -> Self {
222 let mut s = Self::new(SourceCapabilities {
223 identity: SourceIdentity::Inferred,
224 write_through: true,
225 watch: true,
226 });
227 for (p, c) in files {
228 s.files.insert((*p).to_owned(), (*c).to_owned());
229 }
230 s
231 }
232
233 pub fn set(&mut self, path: &str, content: &str) {
235 self.files.insert(path.to_owned(), content.to_owned());
236 *self.revisions.entry(path.to_owned()).or_insert(0) += 1;
237 }
238
239 pub fn emit(&self, paths: &[&str]) {
241 if let Some(tx) = &self.batches {
242 let _ = tx.send(paths.iter().map(|p| (*p).to_owned()).collect());
243 }
244 }
245
246 fn entry(&self, path: &str) -> SourceEntry {
247 SourceEntry {
248 path: path.to_owned(),
249 revision: self.revisions.get(path).copied().unwrap_or(0).to_string(),
250 source_id: None,
251 }
252 }
253}
254
255impl SyncSource for MemSource {
256 fn capabilities(&self) -> SourceCapabilities {
257 self.caps
258 }
259
260 fn enumerate(&mut self) -> Result<Vec<SourceEntry>> {
261 Ok(self.files.keys().map(|p| self.entry(p)).collect())
262 }
263
264 fn fetch(&mut self, path: &str) -> Result<Option<SourceItem>> {
265 Ok(self.files.get(path).map(|content| SourceItem {
266 entry: self.entry(path),
267 content: content.clone(),
268 }))
269 }
270
271 fn write(&mut self, path: &str, content: &str) -> Result<()> {
272 if !self.caps.write_through {
273 return Err(Error::Unsupported("write".to_owned()));
274 }
275 self.set(path, content);
276 self.log
277 .push(("write", path.to_owned(), content.to_owned()));
278 Ok(())
279 }
280
281 fn remove(&mut self, path: &str) -> Result<()> {
282 if !self.caps.write_through {
283 return Err(Error::Unsupported("remove".to_owned()));
284 }
285 self.files.remove(path);
286 self.log.push(("remove", path.to_owned(), String::new()));
287 Ok(())
288 }
289
290 fn watch(&mut self) -> Result<Receiver<Vec<String>>> {
291 if !self.caps.watch {
292 return Err(Error::Unsupported("watch".to_owned()));
293 }
294 let (tx, rx) = std::sync::mpsc::channel();
295 self.batches = Some(tx);
296 Ok(rx)
297 }
298
299 fn unwatch(&mut self) -> Result<()> {
300 self.batches = None;
301 Ok(())
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use serde_json::json;
309
310 #[test]
311 fn capabilities_parse_with_js_truthiness() {
312 let c = SourceCapabilities::from_json(Some(
313 &json!({"identity": "borne", "writeThrough": "yes", "watch": 0}),
314 ));
315 assert_eq!(c.identity, SourceIdentity::Borne);
316 assert!(c.write_through);
317 assert!(!c.watch);
318 let c = SourceCapabilities::from_json(Some(&json!({"identity": "weird", "watch": true})));
319 assert_eq!(c.identity, SourceIdentity::Inferred);
320 assert!(c.watch && !c.write_through);
321 assert_eq!(
322 SourceCapabilities::from_json(None),
323 SourceCapabilities::default()
324 );
325 assert_eq!(
326 SourceCapabilities::from_json(Some(&json!(null))),
327 SourceCapabilities::default()
328 );
329 assert_eq!(
330 c.to_json(),
331 json!({"identity": "inferred", "writeThrough": false, "watch": true})
332 );
333 }
334
335 #[test]
336 fn entries_and_items_parse() {
337 let e =
338 SourceEntry::from_json(&json!({"path": "a.md", "revision": "1:2", "sourceId": "x"}))
339 .unwrap();
340 assert_eq!(
341 (e.path.as_str(), e.revision.as_str(), e.source_id.as_deref()),
342 ("a.md", "1:2", Some("x"))
343 );
344 assert_eq!(
345 SourceEntry::from_json(&json!({"path": "a.md", "revision": 7}))
346 .unwrap()
347 .revision,
348 "7"
349 );
350 assert_eq!(SourceEntry::from_json(&json!({"revision": "1"})), None);
351 let it = SourceItem::from_json(Some(
352 &json!({"path": "a.md", "revision": "r", "content": "# A\n"}),
353 ))
354 .unwrap();
355 assert_eq!(it.content, "# A\n");
356 assert_eq!(
357 it.to_json(),
358 json!({"path": "a.md", "revision": "r", "content": "# A\n"})
359 );
360 assert_eq!(
361 e.to_json(),
362 json!({"path": "a.md", "revision": "1:2", "sourceId": "x"})
363 );
364 assert_eq!(SourceItem::from_json(Some(&json!(null))), None);
365 assert_eq!(SourceItem::from_json(None), None);
366 }
367
368 #[test]
369 fn mem_source_behaves() {
370 let mut s = MemSource::with_files(&[("a.md", "A")]);
371 assert_eq!(s.enumerate().unwrap()[0].path, "a.md");
372 assert_eq!(s.fetch("a.md").unwrap().unwrap().content, "A");
373 assert!(s.fetch("b.md").unwrap().is_none());
374 s.write("b.md", "B").unwrap();
375 s.remove("a.md").unwrap();
376 assert_eq!(s.log.len(), 2);
377 let rx = s.watch().unwrap();
378 s.emit(&["b.md"]);
379 assert_eq!(rx.recv().unwrap(), ["b.md"]);
380 s.unwatch().unwrap();
381 s.close().unwrap();
382 let mut ro = MemSource::new(SourceCapabilities::default());
383 assert!(matches!(ro.write("x", "y"), Err(Error::Unsupported(_))));
384 assert!(matches!(ro.remove("x"), Err(Error::Unsupported(_))));
385 assert!(matches!(ro.watch(), Err(Error::Unsupported(_))));
386 }
387}