1use std::sync::Mutex;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use vissue_core::catalog::{CatalogService, load_recs};
7use vissue_core::config::Layout;
8use vissue_core::error::Error;
9use vissue_core::events;
10use vissue_core::ops::{self, UpdateOutcome};
11use vissue_core::store;
12use vissue_core::views::{
13 AgendaRow, ClaimRow, Excerpt, IssueDetail, IssueRec, ListQuery, RelatedHit, SearchHit, TreeNode,
14};
15
16use crate::backend::{BackendKind, BoardBackend, ListPage, MutResult, UpdateReq};
17
18#[derive(Debug)]
20pub struct CoreBackend {
21 layout: Layout,
22 identity: String,
23 recs: Mutex<Vec<IssueRec>>,
24 generation: AtomicU64,
25}
26
27impl CoreBackend {
28 pub fn open(layout: Layout, identity: impl Into<String>) -> Result<Self, Error> {
34 let recs = load_recs(&layout)?;
35 let generation = events::generation(&layout);
36 Ok(Self {
37 layout,
38 identity: identity.into(),
39 recs: Mutex::new(recs),
40 generation: AtomicU64::new(generation),
41 })
42 }
43
44 fn reload(&self) -> Result<(), Error> {
45 let recs = load_recs(&self.layout)?;
46 *self.recs.lock().expect("core recs") = recs;
47 self.generation
48 .store(events::generation(&self.layout), Ordering::SeqCst);
49 Ok(())
50 }
51
52 fn with_service<T>(
53 &self,
54 f: impl FnOnce(&CatalogService<'_>) -> Result<T, Error>,
55 ) -> Result<T, Error> {
56 let recs = self.recs.lock().expect("core recs");
57 f(&CatalogService::from_recs(&recs))
58 }
59
60 fn mut_result(&self, report: String, id: &str) -> Result<MutResult, Error> {
61 self.reload()?;
62 let issue = self.with_service(|svc| svc.detail(id)).ok();
63 Ok(MutResult {
64 ok: true,
65 report,
66 issue,
67 revision: 0,
68 generation: self.generation.load(Ordering::SeqCst),
69 })
70 }
71}
72
73impl BoardBackend for CoreBackend {
74 fn layout(&self) -> &Layout {
75 &self.layout
76 }
77
78 fn generation(&self) -> u64 {
79 self.generation.load(Ordering::SeqCst)
80 }
81
82 fn revision(&self) -> u64 {
83 0
84 }
85
86 fn live(&self) -> BackendKind {
87 BackendKind::Core
88 }
89
90 fn identity(&self) -> &str {
91 &self.identity
92 }
93
94 fn list(&self, q: ListQuery) -> Result<ListPage, Error> {
95 self.with_service(|svc| {
96 let issues = svc.issues_rows(q)?;
97 let matched = issues.len() as u64;
98 Ok(ListPage {
99 issues,
100 total: matched,
101 matched,
102 revision: 0,
103 generation: self.generation.load(Ordering::SeqCst),
104 unchanged: false,
105 })
106 })
107 }
108
109 fn ready(&self, project: Option<&str>) -> Result<ListPage, Error> {
110 self.with_service(|svc| {
111 let issues = svc.ready(project)?;
112 let matched = issues.len() as u64;
113 Ok(ListPage {
114 issues,
115 total: matched,
116 matched,
117 revision: 0,
118 generation: self.generation.load(Ordering::SeqCst),
119 unchanged: false,
120 })
121 })
122 }
123
124 fn get(&self, id: &str) -> Result<IssueDetail, Error> {
125 self.with_service(|svc| svc.detail(id))
126 }
127
128 fn excerpt(&self, id: &str) -> Result<Excerpt, Error> {
129 self.with_service(|svc| svc.excerpt(id))
130 }
131
132 fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchHit>, Error> {
133 self.with_service(|svc| svc.search(query, limit))
134 }
135
136 fn claims(&self, holder: Option<&str>, project: Option<&str>) -> Result<Vec<ClaimRow>, Error> {
137 self.with_service(|svc| svc.claims(holder, project))
138 }
139
140 fn agenda(&self, days: i64, project: Option<&str>) -> Result<Vec<AgendaRow>, Error> {
141 self.with_service(|svc| svc.agenda(days, project))
142 }
143
144 fn tree(&self, id: &str) -> Result<TreeNode, Error> {
145 self.with_service(|svc| svc.tree(id))
146 }
147
148 fn related(&self, id: &str, depth: usize, limit: usize) -> Result<Vec<RelatedHit>, Error> {
149 self.with_service(|svc| svc.related(id, depth, limit))
150 }
151
152 fn projects(&self) -> Result<Vec<String>, Error> {
153 store::list_projects(&self.layout)
154 }
155
156 fn claim(&self, id: &str, force: bool) -> Result<MutResult, Error> {
157 let report = ops::claim_as(&self.layout, id, force, &self.identity)?;
158 self.mut_result(report, id)
159 }
160
161 fn note(&self, id: &str, text: &str) -> Result<MutResult, Error> {
162 let report = ops::note(&self.layout, id, text)?;
163 self.mut_result(report, id)
164 }
165
166 fn update(&self, req: UpdateReq) -> Result<MutResult, Error> {
167 let UpdateOutcome { report, .. } = ops::update_as_pred(
168 &self.layout,
169 &req.id,
170 req.state.as_deref(),
171 req.priority,
172 req.block.as_deref(),
173 req.unblock.as_deref(),
174 &self.identity,
175 vissue_core::UpdatePred {
176 if_state: req.if_state.as_deref(),
177 if_gen: req.if_gen,
178 },
179 )?;
180 self.mut_result(report, &req.id)
181 }
182
183 fn create(&self, project: &str, title: &str) -> Result<MutResult, Error> {
184 let report = ops::create(
185 &self.layout,
186 project,
187 title,
188 ops::CreateOpts {
189 quiet: true,
190 ..ops::CreateOpts::default()
191 },
192 )?;
193 let id = report.split_whitespace().next().unwrap_or("").to_string();
194 self.mut_result(report, &id)
195 }
196
197 fn open(&self, id: &str) -> Result<IssueDetail, Error> {
198 self.get(id)
199 }
200
201 fn wait(&self, last: u64, timeout_ms: u64) -> Result<u64, Error> {
202 let generation = events::wait_generation(&self.layout, last, 200, timeout_ms)?;
203 if generation > last {
204 self.reload()?;
205 }
206 Ok(generation)
207 }
208
209 fn refresh(&self) -> Result<(), Error> {
210 self.reload()
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::backend::{BackendKind, BoardBackend, UpdateReq};
218 use std::path::Path;
219 use vissue_core::config::DEFAULT_PREFIX;
220 use vissue_core::views::ListQuery;
221
222 fn fixture() -> Layout {
223 Layout::new(
224 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixture_vault"),
225 DEFAULT_PREFIX,
226 )
227 }
228
229 #[test]
230 fn core_revision_is_zero_and_ready_matches_fixture() {
231 let backend = CoreBackend::open(fixture(), "snap").unwrap();
232 assert_eq!(backend.revision(), 0);
233 assert_eq!(backend.live(), BackendKind::Core);
234 assert_eq!(backend.identity(), "snap");
235 let page = backend.ready(None).unwrap();
236 let ids: Vec<_> = page.issues.iter().map(|r| r.id.as_str()).collect();
237 assert_eq!(ids, ["atlas-1a2b", "atlas-2c3d", "beacon-5j6k"]);
238 assert_eq!(backend.wait(backend.generation(), 1).unwrap(), 0);
239 assert!(!backend.projects().unwrap().is_empty());
240 assert_eq!(
241 backend
242 .list(ListQuery {
243 project: Some("beacon".into()),
244 ..ListQuery::default()
245 })
246 .unwrap()
247 .issues
248 .len(),
249 2
250 );
251 assert_eq!(backend.search("retry", 5).unwrap().len(), 1);
252 assert_eq!(backend.claims(None, None).unwrap().len(), 1);
253 assert!(!backend.agenda(400, None).unwrap().is_empty());
254 let tree = backend.tree("atlas-1a2b").unwrap();
255 assert_eq!(tree.id, "atlas-1a2b");
256 assert!(!backend.related("atlas-1a2b", 2, 5).unwrap().is_empty());
257 assert!(
258 backend
259 .excerpt("atlas-2c3d")
260 .unwrap()
261 .text
262 .contains("summary")
263 );
264 assert_eq!(backend.open("atlas-2c3d").unwrap().id, "atlas-2c3d");
265 assert!(backend.get("missing").is_err());
266 }
267
268 #[test]
269 fn core_mutations_reload_the_catalog() {
270 let dir = tempfile::tempdir().unwrap();
271 let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixture_vault");
272 copy_tree(&src.join(DEFAULT_PREFIX), &dir.path().join(DEFAULT_PREFIX));
273 let layout = Layout::new(dir.path(), DEFAULT_PREFIX);
274 let backend = CoreBackend::open(layout, "tui-test").unwrap();
275 assert!(backend.claim("atlas-2c3d", false).unwrap().ok);
276 assert_eq!(
277 backend.get("atlas-2c3d").unwrap().claimed_by.as_deref(),
278 Some("tui-test")
279 );
280 assert!(backend.note("atlas-2c3d", "hi").unwrap().ok);
281 assert!(
282 backend
283 .update(UpdateReq {
284 id: "atlas-2c3d".into(),
285 state: Some("BLOCKED".into()),
286 ..UpdateReq::default()
287 })
288 .unwrap()
289 .ok
290 );
291 assert_eq!(backend.get("atlas-2c3d").unwrap().state, "BLOCKED");
292 }
293
294 fn copy_tree(src: &Path, dest: &Path) {
295 std::fs::create_dir_all(dest).unwrap();
296 for entry in std::fs::read_dir(src).unwrap() {
297 let entry = entry.unwrap();
298 let target = dest.join(entry.file_name());
299 if entry.file_type().unwrap().is_dir() {
300 copy_tree(&entry.path(), &target);
301 } else {
302 std::fs::copy(entry.path(), &target).unwrap();
303 }
304 }
305 }
306}