Skip to main content

ralph/queue/search/
mod.rs

1//! Task queue search and filtering.
2//!
3//! Responsibilities:
4//! - Filtering tasks by status/tag/scope
5//! - Searching across task text fields (substring, regex, or fuzzy matching)
6//!
7//! Not handled here:
8//! - Queue persistence, repair, or validation (see `crate::queue`)
9//! - Task mutation or state changes
10//! - Search result ordering beyond basic filtering
11//!
12//! Invariants/assumptions:
13//! - Search patterns are normalized (lowercase, trimmed) before comparison
14//! - Regex compilation failures are propagated to callers
15//! - Empty filter sets match all tasks (no filtering applied)
16//!
17//! It is split out from `queue.rs` to keep that parent module focused on
18//! persistence/repair/validation while keeping a stable public API via
19//! re-exports from `crate::queue`.
20
21mod fields;
22mod filter;
23mod fuzzy;
24mod normalize;
25mod options;
26mod substring;
27
28#[cfg(test)]
29mod test_support;
30
31pub use filter::filter_tasks;
32pub use fuzzy::fuzzy_search_tasks;
33pub use options::SearchOptions;
34pub use substring::search_tasks;
35
36use crate::contracts::Task;
37use anyhow::Result;
38
39/// Unified search entry point that handles all search modes.
40///
41/// Delegates to fuzzy matching, regex, or substring search based on
42/// the options provided. Fuzzy and regex modes are mutually exclusive;
43/// fuzzy takes precedence if both are enabled.
44pub fn search_tasks_with_options<'a>(
45    tasks: impl IntoIterator<Item = &'a Task>,
46    query: &str,
47    options: &SearchOptions,
48) -> Result<Vec<&'a Task>> {
49    if options.use_fuzzy {
50        fuzzy_search_tasks(tasks, query, options.case_sensitive)
51            .map(|results| results.into_iter().map(|(_, task)| task).collect())
52    } else {
53        search_tasks(tasks, query, options.use_regex, options.case_sensitive)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use test_support::task;
61
62    #[test]
63    fn search_tasks_with_options_fuzzy_mode() -> Result<()> {
64        let mut t1 = task("RQ-0001");
65        t1.title = "Fix authentication".to_string();
66
67        let mut t2 = task("RQ-0002");
68        t2.title = "Update docs".to_string();
69
70        let tasks: Vec<&Task> = vec![&t1, &t2];
71        let options = SearchOptions {
72            use_regex: false,
73            case_sensitive: false,
74            use_fuzzy: true,
75            scopes: vec![],
76        };
77
78        let results = search_tasks_with_options(tasks.iter().copied(), "auth", &options)?;
79        assert_eq!(results.len(), 1);
80        assert_eq!(results[0].id, "RQ-0001");
81        Ok(())
82    }
83
84    #[test]
85    fn search_tasks_with_options_regex_mode() -> Result<()> {
86        let mut t1 = task("RQ-0001");
87        t1.title = "Fix RQ-1234 bug".to_string();
88
89        let mut t2 = task("RQ-0002");
90        t2.title = "Update docs".to_string();
91
92        let tasks: Vec<&Task> = vec![&t1, &t2];
93        let options = SearchOptions {
94            use_regex: true,
95            case_sensitive: false,
96            use_fuzzy: false,
97            scopes: vec![],
98        };
99
100        let results = search_tasks_with_options(tasks.iter().copied(), r"RQ-\d{4}", &options)?;
101        assert_eq!(results.len(), 1);
102        assert_eq!(results[0].id, "RQ-0001");
103        Ok(())
104    }
105
106    #[test]
107    fn search_tasks_with_options_substring_mode() -> Result<()> {
108        let mut t1 = task("RQ-0001");
109        t1.title = "Fix authentication".to_string();
110
111        let mut t2 = task("RQ-0002");
112        t2.title = "Update docs".to_string();
113
114        let tasks: Vec<&Task> = vec![&t1, &t2];
115        let options = SearchOptions {
116            use_regex: false,
117            case_sensitive: false,
118            use_fuzzy: false,
119            scopes: vec![],
120        };
121
122        let results = search_tasks_with_options(tasks.iter().copied(), "auth", &options)?;
123        assert_eq!(results.len(), 1);
124        assert_eq!(results[0].id, "RQ-0001");
125        Ok(())
126    }
127}