1use std::collections::BTreeMap;
2use std::sync::{Arc, RwLock};
3
4use async_trait::async_trait;
5use serde_json::Value;
6
7use super::{SearchEngine, document_with_id, validate_index, validate_query};
8use crate::Error;
9
10type Documents = BTreeMap<String, BTreeMap<i32, Value>>;
11
12#[derive(Clone, Default)]
14pub struct MockSearchEngine {
15 documents: Arc<RwLock<Documents>>,
16}
17
18impl MockSearchEngine {
19 pub fn new() -> Self {
21 Self::default()
22 }
23
24 fn read_documents(&self) -> Result<std::sync::RwLockReadGuard<'_, Documents>, Error> {
25 self.documents
26 .read()
27 .map_err(|_| Error::Internal("Scout mock read lock is poisoned".to_string()))
28 }
29
30 fn write_documents(&self) -> Result<std::sync::RwLockWriteGuard<'_, Documents>, Error> {
31 self.documents
32 .write()
33 .map_err(|_| Error::Internal("Scout mock write lock is poisoned".to_string()))
34 }
35}
36
37#[async_trait]
38impl SearchEngine for MockSearchEngine {
39 async fn update(&self, table: &str, id: i32, payload: Value) -> Result<(), Error> {
40 validate_index(table)?;
41 let payload = document_with_id(id, payload)?;
42 self.write_documents()?
43 .entry(table.to_string())
44 .or_default()
45 .insert(id, payload);
46 Ok(())
47 }
48
49 async fn delete(&self, table: &str, id: i32) -> Result<(), Error> {
50 validate_index(table)?;
51 if id <= 0 {
52 return Err(Error::Validation(
53 "Scout document id must be positive".to_string(),
54 ));
55 }
56 if let Some(index) = self.write_documents()?.get_mut(table) {
57 index.remove(&id);
58 }
59 Ok(())
60 }
61
62 async fn search(&self, table: &str, query: &str) -> Result<Vec<i32>, Error> {
63 validate_index(table)?;
64 validate_query(query)?;
65 let normalized = query.to_lowercase();
66 let documents = self.read_documents()?;
67 let Some(index) = documents.get(table) else {
68 return Ok(Vec::new());
69 };
70 let mut ids = Vec::new();
71 for (id, payload) in index {
72 if normalized.is_empty() || payload.to_string().to_lowercase().contains(&normalized) {
73 ids.push(*id);
74 }
75 }
76 Ok(ids)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[tokio::test]
85 async fn mock_search_is_deterministic_and_bounded() {
86 let engine = MockSearchEngine::new();
87 engine
88 .update("articles", 2, serde_json::json!({"title": "Rust"}))
89 .await
90 .expect("insert second document");
91 engine
92 .update("articles", 1, serde_json::json!({"title": "Rullst"}))
93 .await
94 .expect("insert first document");
95 assert_eq!(
96 engine.search("articles", "rust").await.expect("search"),
97 vec![2]
98 );
99 assert_eq!(
100 engine.search("articles", "").await.expect("list all"),
101 vec![1, 2]
102 );
103 engine.delete("articles", 1).await.expect("delete document");
104 assert_eq!(
105 engine.search("articles", "").await.expect("list remaining"),
106 vec![2]
107 );
108 assert!(
109 engine
110 .update("Bad Index", 3, serde_json::json!({}))
111 .await
112 .is_err()
113 );
114 assert!(
115 engine
116 .update("articles", 3, serde_json::json!({"id": 4}))
117 .await
118 .is_err()
119 );
120 assert!(engine.search("articles", "\n").await.is_err());
121 }
122}