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