tellaro_query_language/parser/
ast.rs

1//! Abstract Syntax Tree (AST) structures for TQL.
2//!
3//! These structures represent the parsed query tree and match the Python implementation's AST.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Top-level AST node types
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum AstNode {
12    /// Match all records (empty query)
13    MatchAll,
14
15    /// Comparison operation (field op value)
16    Comparison(ComparisonNode),
17
18    /// Logical operation (AND/OR)
19    LogicalOp(LogicalOpNode),
20
21    /// Unary operation (NOT)
22    UnaryOp(UnaryOpNode),
23
24    /// Collection operation (ANY/ALL/NONE)
25    CollectionOp(CollectionOpNode),
26
27    /// GeoIP expression
28    GeoExpr(GeoExprNode),
29
30    /// DNS lookup expression
31    NslookupExpr(NslookupExprNode),
32
33    /// Stats expression only
34    StatsExpr(StatsNode),
35
36    /// Query with stats
37    QueryWithStats(QueryWithStatsNode),
38}
39
40/// Comparison node: field operator value
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct ComparisonNode {
43    /// Field name (may include dots for nested access)
44    pub field: String,
45
46    /// Comparison operator (eq, ne, gt, contains, etc.)
47    pub operator: String,
48
49    /// Expected value (None for exists/not_exists operators)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub value: Option<Value>,
52
53    /// Field mutators (transformations applied to field)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub field_mutators: Option<Vec<Mutator>>,
56
57    /// Value mutators (transformations applied to value)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub value_mutators: Option<Vec<Mutator>>,
60
61    /// Type hint for the field
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub type_hint: Option<String>,
64}
65
66/// Logical operation node: left AND/OR right
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct LogicalOpNode {
69    /// Logical operator (and, or)
70    pub operator: String,
71
72    /// Left operand
73    pub left: Box<AstNode>,
74
75    /// Right operand
76    pub right: Box<AstNode>,
77}
78
79/// Unary operation node: NOT operand
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct UnaryOpNode {
82    /// Unary operator (not)
83    pub operator: String,
84
85    /// Operand to negate
86    pub operand: Box<AstNode>,
87}
88
89/// Collection operation node: ANY/ALL/NONE field op value
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct CollectionOpNode {
92    /// Collection operator (any, all, none, not_any, not_all, not_none)
93    pub operator: String,
94
95    /// Field name (should be an array/list field)
96    pub field: String,
97
98    /// Comparison operator to apply to elements
99    pub comparison_operator: String,
100
101    /// Value to compare against
102    pub value: Value,
103
104    /// Field mutators
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub field_mutators: Option<Vec<Mutator>>,
107
108    /// Type hint
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub type_hint: Option<String>,
111}
112
113/// GeoIP expression node
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct GeoExprNode {
116    /// Field containing IP address
117    pub field: String,
118
119    /// Field mutators
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub field_mutators: Option<Vec<Mutator>>,
122
123    /// Type hint
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub type_hint: Option<String>,
126
127    /// Conditions to apply to GeoIP results
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub conditions: Option<Box<AstNode>>,
130
131    /// GeoIP parameters
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub geo_params: Option<HashMap<String, Value>>,
134}
135
136/// DNS lookup expression node
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct NslookupExprNode {
139    /// Field containing hostname
140    pub field: String,
141
142    /// Field mutators
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub field_mutators: Option<Vec<Mutator>>,
145
146    /// Type hint
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub type_hint: Option<String>,
149
150    /// Conditions to apply to nslookup results
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub conditions: Option<Box<AstNode>>,
153
154    /// Nslookup parameters
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub nslookup_params: Option<HashMap<String, Value>>,
157}
158
159/// Stats expression node
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161pub struct StatsNode {
162    /// Aggregation functions to apply
163    pub aggregations: Vec<Aggregation>,
164
165    /// Fields to group by
166    #[serde(skip_serializing_if = "Vec::is_empty", default)]
167    pub group_by: Vec<GroupBy>,
168
169    /// Visualization hint
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub viz_hint: Option<String>,
172}
173
174/// Query with stats node: filter | stats
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct QueryWithStatsNode {
177    /// Filter expression
178    pub filter: Box<AstNode>,
179
180    /// Stats expression
181    pub stats: StatsNode,
182}
183
184/// Aggregation function specification
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186pub struct Aggregation {
187    /// Aggregation function name (count, sum, avg, etc.)
188    pub function: String,
189
190    /// Field to aggregate (None for count(*))
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub field: Option<String>,
193
194    /// Alias for the result
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub alias: Option<String>,
197
198    /// Modifier (top, bottom)
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub modifier: Option<String>,
201
202    /// Limit for modifier
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub limit: Option<usize>,
205
206    /// Percentile values (for percentile functions)
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub percentile_values: Option<Vec<f64>>,
209
210    /// Rank values (for percentile_rank functions)
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub rank_values: Option<Vec<f64>>,
213
214    /// Field mutators for the aggregation field
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub field_mutators: Option<Vec<Mutator>>,
217}
218
219/// Group by specification
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221pub struct GroupBy {
222    /// Field to group by
223    pub field: String,
224
225    /// Bucket size (for top N grouping)
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub bucket_size: Option<usize>,
228}
229
230/// Field/value mutator (transformation function)
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub struct Mutator {
233    /// Mutator function name (lowercase, base64_encode, etc.)
234    pub name: String,
235
236    /// Arguments to the mutator
237    #[serde(skip_serializing_if = "Vec::is_empty", default)]
238    pub args: Vec<Value>,
239}
240
241/// Value types
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243#[serde(untagged)]
244pub enum Value {
245    /// String value
246    String(String),
247
248    /// Integer value
249    Integer(i64),
250
251    /// Float value
252    Float(f64),
253
254    /// Boolean value
255    Boolean(bool),
256
257    /// List/array value
258    List(Vec<Value>),
259
260    /// Null value
261    Null,
262}
263
264impl Value {
265    /// Check if value is null
266    pub fn is_null(&self) -> bool {
267        matches!(self, Value::Null)
268    }
269
270    /// Convert to string if possible
271    pub fn as_string(&self) -> Option<&str> {
272        match self {
273            Value::String(s) => Some(s),
274            _ => None,
275        }
276    }
277
278    /// Convert to integer if possible
279    pub fn as_integer(&self) -> Option<i64> {
280        match self {
281            Value::Integer(i) => Some(*i),
282            Value::Float(f) => Some(*f as i64),
283            _ => None,
284        }
285    }
286
287    /// Convert to float if possible
288    pub fn as_float(&self) -> Option<f64> {
289        match self {
290            Value::Float(f) => Some(*f),
291            Value::Integer(i) => Some(*i as f64),
292            _ => None,
293        }
294    }
295
296    /// Convert to boolean if possible
297    pub fn as_bool(&self) -> Option<bool> {
298        match self {
299            Value::Boolean(b) => Some(*b),
300            _ => None,
301        }
302    }
303
304    /// Convert to list if possible
305    pub fn as_list(&self) -> Option<&Vec<Value>> {
306        match self {
307            Value::List(l) => Some(l),
308            _ => None,
309        }
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_value_conversions() {
319        let str_val = Value::String("test".to_string());
320        assert_eq!(str_val.as_string(), Some("test"));
321        assert!(str_val.as_integer().is_none());
322
323        let int_val = Value::Integer(42);
324        assert_eq!(int_val.as_integer(), Some(42));
325        assert_eq!(int_val.as_float(), Some(42.0));
326
327        let null_val = Value::Null;
328        assert!(null_val.is_null());
329    }
330
331    #[test]
332    fn test_serialization() {
333        let node = ComparisonNode {
334            field: "test".to_string(),
335            operator: "eq".to_string(),
336            value: Some(Value::String("value".to_string())),
337            field_mutators: None,
338            value_mutators: None,
339            type_hint: None,
340        };
341
342        let json = serde_json::to_string(&node).unwrap();
343        assert!(json.contains("\"field\":\"test\""));
344    }
345}