1use cratestack_core::CoolError;
6use url::form_urlencoded;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum QueryExpr {
10 Predicate { key: String, value: String },
11 All(Vec<QueryExpr>),
12 Any(Vec<QueryExpr>),
13 Not(Box<QueryExpr>),
14}
15
16pub fn parse_query_pairs(raw_query: Option<&str>) -> Result<Vec<(String, String)>, CoolError> {
17 let Some(raw_query) = raw_query else {
18 return Ok(Vec::new());
19 };
20
21 let mut pairs = Vec::new();
22 for (key, value) in form_urlencoded::parse(raw_query.as_bytes()) {
23 pairs.push((key.into_owned(), value.into_owned()));
24 }
25 Ok(pairs)
26}
27
28pub fn parse_filter_expression(input: &str) -> Result<QueryExpr, CoolError> {
29 let mut parser = FilterExpressionParser::new(input);
30 let expr = parser.parse_expr()?;
31 parser.skip_whitespace();
32 if !parser.is_eof() {
33 return Err(CoolError::BadRequest(format!(
34 "unexpected trailing filter expression content near '{}'",
35 parser.remaining(),
36 )));
37 }
38 Ok(expr)
39}
40
41pub(crate) struct FilterExpressionParser<'a> {
42 input: &'a str,
43 cursor: usize,
44}
45
46impl<'a> FilterExpressionParser<'a> {
47 fn new(input: &'a str) -> Self {
48 Self { input, cursor: 0 }
49 }
50
51 fn parse_expr(&mut self) -> Result<QueryExpr, CoolError> {
52 self.parse_or()
53 }
54
55 fn parse_or(&mut self) -> Result<QueryExpr, CoolError> {
56 let mut nodes = vec![self.parse_and()?];
57 loop {
58 self.skip_whitespace();
59 if !self.consume('|') {
60 break;
61 }
62 nodes.push(self.parse_and()?);
63 }
64 Ok(if nodes.len() == 1 {
65 nodes.pop().expect("single node should exist")
66 } else {
67 QueryExpr::Any(nodes)
68 })
69 }
70
71 fn parse_and(&mut self) -> Result<QueryExpr, CoolError> {
72 let mut nodes = vec![self.parse_factor()?];
73 loop {
74 self.skip_whitespace();
75 if !self.consume(',') {
76 break;
77 }
78 nodes.push(self.parse_factor()?);
79 }
80 Ok(if nodes.len() == 1 {
81 nodes.pop().expect("single node should exist")
82 } else {
83 QueryExpr::All(nodes)
84 })
85 }
86
87 fn parse_factor(&mut self) -> Result<QueryExpr, CoolError> {
88 self.skip_whitespace();
89 if self.consume_keyword("not") {
90 self.skip_whitespace();
91 if !self.consume('(') {
92 return Err(CoolError::BadRequest(
93 "negated filter expression must use not(...)".to_owned(),
94 ));
95 }
96 let expr = self.parse_expr()?;
97 self.skip_whitespace();
98 if !self.consume(')') {
99 return Err(CoolError::BadRequest(
100 "unterminated negated filter expression".to_owned(),
101 ));
102 }
103 return Ok(QueryExpr::Not(Box::new(expr)));
104 }
105 if self.consume('(') {
106 let expr = self.parse_expr()?;
107 self.skip_whitespace();
108 if !self.consume(')') {
109 return Err(CoolError::BadRequest(
110 "unterminated grouped filter expression".to_owned(),
111 ));
112 }
113 return Ok(expr);
114 }
115
116 self.parse_predicate()
117 }
118
119 fn parse_predicate(&mut self) -> Result<QueryExpr, CoolError> {
120 let start = self.cursor;
121 while let Some(ch) = self.peek() {
122 if matches!(ch, ',' | '|' | ')') {
123 break;
124 }
125 self.cursor += ch.len_utf8();
126 }
127 let raw = self.input[start..self.cursor].trim();
128 let (key, value) = raw.split_once('=').ok_or_else(|| {
129 CoolError::BadRequest(format!(
130 "invalid grouped filter '{}': expected key=value",
131 raw,
132 ))
133 })?;
134 if key.trim().is_empty() || value.trim().is_empty() {
135 return Err(CoolError::BadRequest(format!(
136 "invalid grouped filter '{}': expected non-empty key and value",
137 raw,
138 )));
139 }
140 Ok(QueryExpr::Predicate {
141 key: key.trim().to_owned(),
142 value: value.trim().to_owned(),
143 })
144 }
145
146 fn consume(&mut self, expected: char) -> bool {
147 match self.peek() {
148 Some(ch) if ch == expected => {
149 self.cursor += ch.len_utf8();
150 true
151 }
152 _ => false,
153 }
154 }
155
156 fn consume_keyword(&mut self, expected: &str) -> bool {
157 let remaining = &self.input[self.cursor..];
158 if !remaining.starts_with(expected) {
159 return false;
160 }
161 let boundary = remaining[expected.len()..].chars().next();
162 if boundary.is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
163 return false;
164 }
165 self.cursor += expected.len();
166 true
167 }
168
169 fn peek(&self) -> Option<char> {
170 self.input[self.cursor..].chars().next()
171 }
172
173 fn skip_whitespace(&mut self) {
174 while let Some(ch) = self.peek() {
175 if !ch.is_whitespace() {
176 break;
177 }
178 self.cursor += ch.len_utf8();
179 }
180 }
181
182 fn remaining(&self) -> &str {
183 &self.input[self.cursor..]
184 }
185
186 fn is_eof(&self) -> bool {
187 self.cursor >= self.input.len()
188 }
189}