ragfs_query/
parser.rs

1//! Query DSL parser.
2
3use ragfs_core::SearchFilter;
4
5/// Parsed query with text and filters.
6#[derive(Debug, Clone)]
7pub struct ParsedQuery {
8    /// Main query text
9    pub text: String,
10    /// Extracted filters
11    pub filters: Vec<SearchFilter>,
12    /// Result limit
13    pub limit: usize,
14}
15
16/// Query parser for the DSL.
17pub struct QueryParser {
18    /// Default result limit
19    default_limit: usize,
20}
21
22impl QueryParser {
23    /// Create a new query parser.
24    #[must_use]
25    pub fn new(default_limit: usize) -> Self {
26        Self { default_limit }
27    }
28
29    /// Parse a query string.
30    ///
31    /// Supports filters like:
32    /// - `lang:rust` or `language:python`
33    /// - `path:src/**`
34    /// - `type:code` or `type:text`
35    /// - `limit:10`
36    #[must_use]
37    pub fn parse(&self, query: &str) -> ParsedQuery {
38        let mut text_parts = Vec::new();
39        let mut filters = Vec::new();
40        let mut limit = self.default_limit;
41
42        for part in query.split_whitespace() {
43            if let Some((key, value)) = part.split_once(':') {
44                match key.to_lowercase().as_str() {
45                    "lang" | "language" => {
46                        filters.push(SearchFilter::Language(value.to_string()));
47                    }
48                    "path" => {
49                        if value.contains('*') {
50                            filters.push(SearchFilter::PathGlob(value.to_string()));
51                        } else {
52                            filters.push(SearchFilter::PathPrefix(value.to_string()));
53                        }
54                    }
55                    "type" | "mime" => {
56                        filters.push(SearchFilter::MimeType(value.to_string()));
57                    }
58                    "limit" => {
59                        if let Ok(n) = value.parse() {
60                            limit = n;
61                        }
62                    }
63                    "depth" => {
64                        if let Ok(n) = value.parse() {
65                            filters.push(SearchFilter::MaxDepth(n));
66                        }
67                    }
68                    _ => {
69                        // Unknown filter, treat as text
70                        text_parts.push(part);
71                    }
72                }
73            } else {
74                text_parts.push(part);
75            }
76        }
77
78        ParsedQuery {
79            text: text_parts.join(" "),
80            filters,
81            limit,
82        }
83    }
84}
85
86impl Default for QueryParser {
87    fn default() -> Self {
88        Self::new(10)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_parse_simple() {
98        let parser = QueryParser::default();
99        let result = parser.parse("how to implement auth");
100
101        assert_eq!(result.text, "how to implement auth");
102        assert!(result.filters.is_empty());
103        assert_eq!(result.limit, 10);
104    }
105
106    #[test]
107    fn test_parse_with_filters() {
108        let parser = QueryParser::default();
109        let result = parser.parse("authentication lang:rust path:src/** limit:5");
110
111        assert_eq!(result.text, "authentication");
112        assert_eq!(result.filters.len(), 2);
113        assert_eq!(result.limit, 5);
114    }
115
116    #[test]
117    fn test_parse_empty_query() {
118        let parser = QueryParser::default();
119        let result = parser.parse("");
120
121        assert_eq!(result.text, "");
122        assert!(result.filters.is_empty());
123        assert_eq!(result.limit, 10);
124    }
125
126    #[test]
127    fn test_parse_language_filter() {
128        let parser = QueryParser::default();
129        let result = parser.parse("lang:rust");
130
131        assert_eq!(result.text, "");
132        assert_eq!(result.filters.len(), 1);
133        assert!(matches!(&result.filters[0], SearchFilter::Language(l) if l == "rust"));
134    }
135
136    #[test]
137    fn test_parse_path_prefix() {
138        let parser = QueryParser::default();
139        let result = parser.parse("path:src/lib");
140
141        assert_eq!(result.filters.len(), 1);
142        assert!(matches!(&result.filters[0], SearchFilter::PathPrefix(p) if p == "src/lib"));
143    }
144
145    #[test]
146    fn test_parse_depth_filter() {
147        let parser = QueryParser::default();
148        let result = parser.parse("depth:2 search term");
149
150        assert_eq!(result.text, "search term");
151        assert_eq!(result.filters.len(), 1);
152        assert!(matches!(&result.filters[0], SearchFilter::MaxDepth(2)));
153    }
154
155    #[test]
156    fn test_parse_invalid_limit() {
157        let parser = QueryParser::default();
158        let result = parser.parse("limit:abc search");
159
160        assert_eq!(result.text, "search");
161        assert_eq!(result.limit, 10); // Default preserved when invalid
162    }
163
164    #[test]
165    fn test_parse_unknown_filter_as_text() {
166        let parser = QueryParser::default();
167        let result = parser.parse("unknown:value search");
168
169        assert_eq!(result.text, "unknown:value search");
170        assert!(result.filters.is_empty());
171    }
172}