1use ragfs_core::SearchFilter;
4
5#[derive(Debug, Clone)]
7pub struct ParsedQuery {
8 pub text: String,
10 pub filters: Vec<SearchFilter>,
12 pub limit: usize,
14 pub scope_prefix: Option<String>,
17}
18
19pub struct QueryParser {
21 default_limit: usize,
23}
24
25impl QueryParser {
26 #[must_use]
28 pub fn new(default_limit: usize) -> Self {
29 Self { default_limit }
30 }
31
32 #[must_use]
40 pub fn parse(&self, query: &str) -> ParsedQuery {
41 let mut text_parts = Vec::new();
42 let mut filters = Vec::new();
43 let mut limit = self.default_limit;
44 let mut scope_prefix = None;
45
46 for part in query.split_whitespace() {
47 if let Some((key, value)) = part.split_once(':') {
48 match key.to_lowercase().as_str() {
49 "lang" | "language" => {
50 filters.push(SearchFilter::Language(value.to_string()));
51 }
52 "path" => {
53 if value.contains('*') {
54 filters.push(SearchFilter::PathGlob(value.to_string()));
55 } else {
56 filters.push(SearchFilter::PathPrefix(value.to_string()));
57 }
58 }
59 "type" | "mime" => {
60 filters.push(SearchFilter::MimeType(value.to_string()));
61 }
62 "limit" => {
63 if let Ok(n) = value.parse() {
64 limit = n;
65 }
66 }
67 "depth" => {
68 if let Ok(n) = value.parse() {
69 filters.push(SearchFilter::MaxDepth(n));
70 }
71 }
72 "scope" => {
73 scope_prefix = Some(value.to_string());
74 }
75 _ => {
76 text_parts.push(part);
78 }
79 }
80 } else {
81 text_parts.push(part);
82 }
83 }
84
85 ParsedQuery {
86 text: text_parts.join(" "),
87 filters,
88 limit,
89 scope_prefix,
90 }
91 }
92}
93
94impl Default for QueryParser {
95 fn default() -> Self {
96 Self::new(10)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_parse_simple() {
106 let parser = QueryParser::default();
107 let result = parser.parse("how to implement auth");
108
109 assert_eq!(result.text, "how to implement auth");
110 assert!(result.filters.is_empty());
111 assert_eq!(result.limit, 10);
112 }
113
114 #[test]
115 fn test_parse_with_filters() {
116 let parser = QueryParser::default();
117 let result = parser.parse("authentication lang:rust path:src/** limit:5");
118
119 assert_eq!(result.text, "authentication");
120 assert_eq!(result.filters.len(), 2);
121 assert_eq!(result.limit, 5);
122 }
123
124 #[test]
125 fn test_parse_empty_query() {
126 let parser = QueryParser::default();
127 let result = parser.parse("");
128
129 assert_eq!(result.text, "");
130 assert!(result.filters.is_empty());
131 assert_eq!(result.limit, 10);
132 }
133
134 #[test]
135 fn test_parse_language_filter() {
136 let parser = QueryParser::default();
137 let result = parser.parse("lang:rust");
138
139 assert_eq!(result.text, "");
140 assert_eq!(result.filters.len(), 1);
141 assert!(matches!(&result.filters[0], SearchFilter::Language(l) if l == "rust"));
142 }
143
144 #[test]
145 fn test_parse_path_prefix() {
146 let parser = QueryParser::default();
147 let result = parser.parse("path:src/lib");
148
149 assert_eq!(result.filters.len(), 1);
150 assert!(matches!(&result.filters[0], SearchFilter::PathPrefix(p) if p == "src/lib"));
151 }
152
153 #[test]
154 fn test_parse_depth_filter() {
155 let parser = QueryParser::default();
156 let result = parser.parse("depth:2 search term");
157
158 assert_eq!(result.text, "search term");
159 assert_eq!(result.filters.len(), 1);
160 assert!(matches!(&result.filters[0], SearchFilter::MaxDepth(2)));
161 }
162
163 #[test]
164 fn test_parse_invalid_limit() {
165 let parser = QueryParser::default();
166 let result = parser.parse("limit:abc search");
167
168 assert_eq!(result.text, "search");
169 assert_eq!(result.limit, 10); }
171
172 #[test]
173 fn test_parse_unknown_filter_as_text() {
174 let parser = QueryParser::default();
175 let result = parser.parse("unknown:value search");
176
177 assert_eq!(result.text, "unknown:value search");
178 assert!(result.filters.is_empty());
179 }
180
181 #[test]
182 fn test_parse_scope_prefix() {
183 let parser = QueryParser::default();
184 let result = parser.parse("authentication scope:src/auth");
185
186 assert_eq!(result.text, "authentication");
187 assert_eq!(result.scope_prefix.as_deref(), Some("src/auth"));
188 assert!(result.filters.is_empty());
189 }
190}