Skip to main content

cratestack_axum/
query.rs

1//! Query-string parsing for axum-bound handlers: percent-decoded pair
2//! extraction and the structured filter expression grammar
3//! (`?where=...`) used by macro-generated `list` endpoints.
4
5mod 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}