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}