pacsea/logic/gating.rs
1//! Access control for package detail loading to optimize performance.
2
3use std::collections::HashSet;
4use std::sync::{OnceLock, RwLock};
5
6use crate::state::AppState;
7
8/// What: Lazily construct and return the global set of package names permitted for detail fetching.
9///
10/// Inputs:
11/// - (none): Initializes an `RwLock<HashSet<String>>` on first access.
12///
13/// Output:
14/// - Returns a reference to the lock guarding the allowed-name set.
15///
16/// Details:
17/// - Uses `OnceLock` to avoid race conditions during initialization while keeping lookups fast.
18fn allowed_set() -> &'static RwLock<HashSet<String>> {
19 static ALLOWED: OnceLock<RwLock<HashSet<String>>> = OnceLock::new();
20 ALLOWED.get_or_init(|| RwLock::new(HashSet::new()))
21}
22
23/// What: Check whether details loading is currently allowed for a package name.
24///
25/// Inputs:
26/// - `name`: Package name to test
27///
28/// Output:
29/// - `true` when the name is currently allowed; otherwise `false` (or `true` if the lock fails).
30///
31/// Details:
32/// - Fails open when the read lock cannot be acquired to avoid blocking UI interactions.
33#[must_use]
34pub fn is_allowed(name: &str) -> bool {
35 allowed_set().read().ok().is_none_or(|s| s.contains(name))
36}
37
38/// What: Restrict details loading to only the currently selected package.
39///
40/// Inputs:
41/// - `app`: Application state to read the current selection from
42///
43/// Output:
44/// - Updates the internal allowed set to contain only the selected package; no-op if none.
45///
46/// Details:
47/// - Clears any previously allowed names to prioritise responsiveness during rapid navigation.
48pub fn set_allowed_only_selected(app: &AppState) {
49 if let Some(sel) = app.results.get(app.selected)
50 && let Ok(mut w) = allowed_set().write()
51 {
52 w.clear();
53 w.insert(sel.name.clone());
54 }
55}
56
57/// What: Allow details loading for a "ring" around the current selection.
58///
59/// Inputs:
60/// - `app`: Application state to read the current selection and results from
61/// - `radius`: Number of neighbors above and below to include
62///
63/// Output:
64/// - Updates the internal allowed set to the ring of names around the selection.
65///
66/// Details:
67/// - Includes the selected package itself and symmetrically expands within bounds while respecting the radius.
68pub fn set_allowed_ring(app: &AppState, radius: usize) {
69 let mut ring: HashSet<String> = HashSet::new();
70 if let Some(sel) = app.results.get(app.selected) {
71 ring.insert(sel.name.clone());
72 }
73 let len = app.results.len();
74 let mut step = 1usize;
75 while step <= radius {
76 if let Some(i) = app.selected.checked_sub(step)
77 && let Some(it) = app.results.get(i)
78 {
79 ring.insert(it.name.clone());
80 }
81 let below = app.selected + step;
82 if below < len
83 && let Some(it) = app.results.get(below)
84 {
85 ring.insert(it.name.clone());
86 }
87 step += 1;
88 }
89 if let Ok(mut w) = allowed_set().write() {
90 *w = ring;
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn item_official(name: &str, repo: &str) -> crate::state::PackageItem {
99 crate::state::PackageItem {
100 name: name.to_string(),
101 version: "1.0".to_string(),
102 description: format!("{name} desc"),
103 source: crate::state::Source::Official {
104 repo: repo.to_string(),
105 arch: "x86_64".to_string(),
106 },
107 popularity: None,
108 out_of_date: None,
109 orphaned: false,
110 }
111 }
112
113 #[test]
114 /// What: Check allowed-set helpers toggle between single selection and ring modes.
115 ///
116 /// Inputs:
117 /// - Results array with four packages and selected index set to one.
118 ///
119 /// Output:
120 /// - Only the selected package allowed initially; after calling `set_allowed_ring`, adjacent packages become allowed.
121 ///
122 /// Details:
123 /// - Validates transition between restrictive and radius-based gating policies.
124 fn allowed_only_selected_and_ring() {
125 let app = crate::state::AppState {
126 results: vec![
127 item_official("a", "core"),
128 item_official("b", "extra"),
129 item_official("c", "extra"),
130 item_official("d", "other"),
131 ],
132 selected: 1,
133 ..Default::default()
134 };
135 set_allowed_only_selected(&app);
136 assert!(is_allowed("b"));
137 assert!(!is_allowed("a") || !is_allowed("c") || !is_allowed("d"));
138
139 set_allowed_ring(&app, 1);
140 assert!(is_allowed("a") || is_allowed("c"));
141 }
142}