1use std::collections::BTreeMap;
9use std::sync::mpsc::{Receiver, RecvTimeoutError};
10use std::time::{Duration, Instant};
11
12use serde_json::Value;
13
14use crate::error::{Error, Result};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum SourceIdentity {
19 Inferred,
21 Borne,
23}
24
25impl SourceIdentity {
26 #[must_use]
27 pub const fn as_str(&self) -> &'static str {
28 match self {
29 SourceIdentity::Inferred => "inferred",
30 SourceIdentity::Borne => "borne",
31 }
32 }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct SourceCapabilities {
38 pub identity: SourceIdentity,
39 pub write_through: bool,
40 pub watch: bool,
41}
42
43impl Default for SourceCapabilities {
44 fn default() -> Self {
45 Self {
46 identity: SourceIdentity::Inferred,
47 write_through: false,
48 watch: false,
49 }
50 }
51}
52
53fn js_truthy(v: Option<&Value>) -> bool {
55 match v {
56 None | Some(Value::Null) => false,
57 Some(Value::Bool(b)) => *b,
58 Some(Value::Number(n)) => n.as_f64().is_some_and(|f| f != 0.0 && !f.is_nan()),
59 Some(Value::String(s)) => !s.is_empty(),
60 Some(Value::Array(_) | Value::Object(_)) => true,
61 }
62}
63
64impl SourceCapabilities {
65 #[must_use]
68 pub fn from_json(v: Option<&Value>) -> Self {
69 let obj = v.and_then(Value::as_object);
70 Self {
71 identity: match obj.and_then(|o| o.get("identity")).and_then(Value::as_str) {
72 Some("borne") => SourceIdentity::Borne,
73 _ => SourceIdentity::Inferred,
74 },
75 write_through: js_truthy(obj.and_then(|o| o.get("writeThrough"))),
76 watch: js_truthy(obj.and_then(|o| o.get("watch"))),
77 }
78 }
79
80 #[must_use]
81 pub fn to_json(&self) -> Value {
82 serde_json::json!({
83 "identity": self.identity.as_str(),
84 "writeThrough": self.write_through,
85 "watch": self.watch,
86 })
87 }
88}
89
90#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct SourceEntry {
93 pub path: String,
95 pub revision: String,
97 pub source_id: Option<String>,
99}
100
101impl SourceEntry {
102 #[must_use]
104 pub fn from_json(v: &Value) -> Option<Self> {
105 Some(Self {
106 path: v.get("path")?.as_str()?.to_owned(),
107 revision: match v.get("revision") {
108 Some(Value::String(s)) => s.clone(),
109 Some(other) if !other.is_null() => other.to_string(),
110 _ => String::new(),
111 },
112 source_id: v.get("sourceId").and_then(Value::as_str).map(str::to_owned),
113 })
114 }
115}
116
117impl SourceEntry {
118 #[must_use]
120 pub fn to_json(&self) -> Value {
121 let mut v = serde_json::json!({ "path": self.path, "revision": self.revision });
122 if let Some(id) = &self.source_id {
123 v["sourceId"] = Value::String(id.clone());
124 }
125 v
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct SourceItem {
132 pub entry: SourceEntry,
133 pub content: String,
135}
136
137impl SourceItem {
138 #[must_use]
140 pub fn from_json(v: Option<&Value>) -> Option<Self> {
141 let v = v?;
142 if v.is_null() {
143 return None;
144 }
145 let entry = SourceEntry::from_json(v)?;
146 let content = match v.get("content") {
147 Some(Value::String(s)) => s.clone(),
148 _ => String::new(),
149 };
150 Some(Self { entry, content })
151 }
152
153 #[must_use]
155 pub fn to_json(&self) -> Value {
156 let mut v = self.entry.to_json();
157 v["content"] = Value::String(self.content.clone());
158 v
159 }
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
166pub enum WatchEvent {
167 Ready,
170 Batch(Vec<String>),
172}
173
174impl WatchEvent {
175 #[must_use]
177 pub fn to_json(&self) -> Value {
178 match self {
179 WatchEvent::Ready => serde_json::json!({ "event": "ready" }),
180 WatchEvent::Batch(paths) => {
181 serde_json::json!({ "event": "batch", "paths": paths })
182 }
183 }
184 }
185}
186
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189pub enum Readiness {
190 Ready,
192 TimedOut,
195 Ended,
197}
198
199#[must_use]
203pub fn wait_ready(rx: &Receiver<WatchEvent>, patience: Duration) -> (Readiness, Vec<Vec<String>>) {
204 let deadline = Instant::now() + patience;
205 let mut early = Vec::new();
206 loop {
207 let now = Instant::now();
208 if now >= deadline {
209 return (Readiness::TimedOut, early);
210 }
211 match rx.recv_timeout(deadline - now) {
212 Ok(WatchEvent::Ready) => return (Readiness::Ready, early),
213 Ok(WatchEvent::Batch(paths)) => early.push(paths),
214 Err(RecvTimeoutError::Timeout) => return (Readiness::TimedOut, early),
215 Err(RecvTimeoutError::Disconnected) => return (Readiness::Ended, early),
216 }
217 }
218}
219
220pub trait SyncSource {
222 fn capabilities(&self) -> SourceCapabilities;
223
224 fn enumerate(&mut self) -> Result<Vec<SourceEntry>>;
226
227 fn fetch(&mut self, path: &str) -> Result<Option<SourceItem>>;
229
230 fn write(&mut self, _path: &str, _content: &str) -> Result<()> {
232 Err(Error::Unsupported("write".to_owned()))
233 }
234
235 fn remove(&mut self, _path: &str) -> Result<()> {
237 Err(Error::Unsupported("remove".to_owned()))
238 }
239
240 fn watch(&mut self) -> Result<Receiver<WatchEvent>> {
244 Err(Error::Unsupported("watch".to_owned()))
245 }
246
247 fn unwatch(&mut self) -> Result<()> {
249 Ok(())
250 }
251
252 fn close(&mut self) -> Result<()> {
254 Ok(())
255 }
256}
257
258#[derive(Debug, Default)]
261pub struct MemSource {
262 pub caps: SourceCapabilities,
263 pub files: BTreeMap<String, String>,
264 pub log: Vec<(&'static str, String, String)>,
266 revisions: BTreeMap<String, u64>,
268 events: Option<std::sync::mpsc::Sender<WatchEvent>>,
269}
270
271impl MemSource {
272 #[must_use]
273 pub fn new(caps: SourceCapabilities) -> Self {
274 Self {
275 caps,
276 ..Self::default()
277 }
278 }
279
280 #[must_use]
282 pub fn with_files(files: &[(&str, &str)]) -> Self {
283 let mut s = Self::new(SourceCapabilities {
284 identity: SourceIdentity::Inferred,
285 write_through: true,
286 watch: true,
287 });
288 for (p, c) in files {
289 s.files.insert((*p).to_owned(), (*c).to_owned());
290 }
291 s
292 }
293
294 pub fn set(&mut self, path: &str, content: &str) {
296 self.files.insert(path.to_owned(), content.to_owned());
297 *self.revisions.entry(path.to_owned()).or_insert(0) += 1;
298 }
299
300 pub fn emit(&self, paths: &[&str]) {
302 if let Some(tx) = &self.events {
303 let _ = tx.send(WatchEvent::Batch(
304 paths.iter().map(|p| (*p).to_owned()).collect(),
305 ));
306 }
307 }
308
309 fn entry(&self, path: &str) -> SourceEntry {
310 SourceEntry {
311 path: path.to_owned(),
312 revision: self.revisions.get(path).copied().unwrap_or(0).to_string(),
313 source_id: None,
314 }
315 }
316}
317
318impl SyncSource for MemSource {
319 fn capabilities(&self) -> SourceCapabilities {
320 self.caps
321 }
322
323 fn enumerate(&mut self) -> Result<Vec<SourceEntry>> {
324 Ok(self.files.keys().map(|p| self.entry(p)).collect())
325 }
326
327 fn fetch(&mut self, path: &str) -> Result<Option<SourceItem>> {
328 Ok(self.files.get(path).map(|content| SourceItem {
329 entry: self.entry(path),
330 content: content.clone(),
331 }))
332 }
333
334 fn write(&mut self, path: &str, content: &str) -> Result<()> {
335 if !self.caps.write_through {
336 return Err(Error::Unsupported("write".to_owned()));
337 }
338 self.set(path, content);
339 self.log
340 .push(("write", path.to_owned(), content.to_owned()));
341 Ok(())
342 }
343
344 fn remove(&mut self, path: &str) -> Result<()> {
345 if !self.caps.write_through {
346 return Err(Error::Unsupported("remove".to_owned()));
347 }
348 self.files.remove(path);
349 self.log.push(("remove", path.to_owned(), String::new()));
350 Ok(())
351 }
352
353 fn watch(&mut self) -> Result<Receiver<WatchEvent>> {
356 if !self.caps.watch {
357 return Err(Error::Unsupported("watch".to_owned()));
358 }
359 let (tx, rx) = std::sync::mpsc::channel();
360 let _ = tx.send(WatchEvent::Ready);
361 self.events = Some(tx);
362 Ok(rx)
363 }
364
365 fn unwatch(&mut self) -> Result<()> {
366 self.events = None;
367 Ok(())
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use serde_json::json;
375
376 #[test]
377 fn capabilities_parse_with_js_truthiness() {
378 let c = SourceCapabilities::from_json(Some(
379 &json!({"identity": "borne", "writeThrough": "yes", "watch": 0}),
380 ));
381 assert_eq!(c.identity, SourceIdentity::Borne);
382 assert!(c.write_through);
383 assert!(!c.watch);
384 let c = SourceCapabilities::from_json(Some(&json!({"identity": "weird", "watch": true})));
385 assert_eq!(c.identity, SourceIdentity::Inferred);
386 assert!(c.watch && !c.write_through);
387 assert_eq!(
388 SourceCapabilities::from_json(None),
389 SourceCapabilities::default()
390 );
391 assert_eq!(
392 SourceCapabilities::from_json(Some(&json!(null))),
393 SourceCapabilities::default()
394 );
395 assert_eq!(
396 c.to_json(),
397 json!({"identity": "inferred", "writeThrough": false, "watch": true})
398 );
399 }
400
401 #[test]
402 fn entries_and_items_parse() {
403 let e =
404 SourceEntry::from_json(&json!({"path": "a.md", "revision": "1:2", "sourceId": "x"}))
405 .unwrap();
406 assert_eq!(
407 (e.path.as_str(), e.revision.as_str(), e.source_id.as_deref()),
408 ("a.md", "1:2", Some("x"))
409 );
410 assert_eq!(
411 SourceEntry::from_json(&json!({"path": "a.md", "revision": 7}))
412 .unwrap()
413 .revision,
414 "7"
415 );
416 assert_eq!(SourceEntry::from_json(&json!({"revision": "1"})), None);
417 let it = SourceItem::from_json(Some(
418 &json!({"path": "a.md", "revision": "r", "content": "# A\n"}),
419 ))
420 .unwrap();
421 assert_eq!(it.content, "# A\n");
422 assert_eq!(
423 it.to_json(),
424 json!({"path": "a.md", "revision": "r", "content": "# A\n"})
425 );
426 assert_eq!(
427 e.to_json(),
428 json!({"path": "a.md", "revision": "1:2", "sourceId": "x"})
429 );
430 assert_eq!(SourceItem::from_json(Some(&json!(null))), None);
431 assert_eq!(SourceItem::from_json(None), None);
432 }
433
434 #[test]
435 fn mem_source_behaves() {
436 let mut s = MemSource::with_files(&[("a.md", "A")]);
437 assert_eq!(s.enumerate().unwrap()[0].path, "a.md");
438 assert_eq!(s.fetch("a.md").unwrap().unwrap().content, "A");
439 assert!(s.fetch("b.md").unwrap().is_none());
440 s.write("b.md", "B").unwrap();
441 s.remove("a.md").unwrap();
442 assert_eq!(s.log.len(), 2);
443 let rx = s.watch().unwrap();
444 s.emit(&["b.md"]);
445 assert_eq!(rx.recv().unwrap(), WatchEvent::Ready, "ready at once");
446 assert_eq!(
447 rx.recv().unwrap(),
448 WatchEvent::Batch(vec!["b.md".to_owned()])
449 );
450 s.unwatch().unwrap();
451 assert!(rx.recv().is_err(), "unwatch closes the stream");
452 s.close().unwrap();
453 let mut ro = MemSource::new(SourceCapabilities::default());
454 assert!(matches!(ro.write("x", "y"), Err(Error::Unsupported(_))));
455 assert!(matches!(ro.remove("x"), Err(Error::Unsupported(_))));
456 assert!(matches!(ro.watch(), Err(Error::Unsupported(_))));
457 }
458
459 #[test]
460 fn watch_events_spell_the_wire() {
461 assert_eq!(WatchEvent::Ready.to_json(), json!({"event": "ready"}));
462 assert_eq!(
463 WatchEvent::Batch(vec!["a.md".into(), "sub/b.md".into()]).to_json(),
464 json!({"event": "batch", "paths": ["a.md", "sub/b.md"]})
465 );
466 }
467
468 #[test]
469 fn wait_ready_keeps_early_batches_and_bounds_the_wait() {
470 let patience = Duration::from_millis(200);
471 let (tx, rx) = std::sync::mpsc::channel();
473 tx.send(WatchEvent::Batch(vec!["early.md".into()])).unwrap();
474 tx.send(WatchEvent::Ready).unwrap();
475 tx.send(WatchEvent::Batch(vec!["later.md".into()])).unwrap();
476 assert_eq!(
477 wait_ready(&rx, patience),
478 (Readiness::Ready, vec![vec!["early.md".to_owned()]])
479 );
480 assert_eq!(
481 rx.try_recv().unwrap(),
482 WatchEvent::Batch(vec!["later.md".into()]),
483 "what follows ready stays in the stream"
484 );
485 let (tx, rx) = std::sync::mpsc::channel::<WatchEvent>();
487 tx.send(WatchEvent::Batch(vec!["a.md".into()])).unwrap();
488 let started = Instant::now();
489 assert_eq!(
490 wait_ready(&rx, patience),
491 (Readiness::TimedOut, vec![vec!["a.md".to_owned()]])
492 );
493 assert!(started.elapsed() >= patience);
494 let (tx, rx) = std::sync::mpsc::channel::<WatchEvent>();
496 drop(tx);
497 assert_eq!(wait_ready(&rx, patience), (Readiness::Ended, vec![]));
498 let mut s = MemSource::with_files(&[]);
500 let rx = s.watch().unwrap();
501 assert_eq!(wait_ready(&rx, patience), (Readiness::Ready, vec![]));
502 }
503}