tellaro-query-language 3.0.1

A flexible, human-friendly query language for searching and filtering structured data
Documentation
//! Abstract Syntax Tree (AST) structures for TQL.
//!
//! These structures represent the parsed query tree and match the Python implementation's AST.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Top-level AST node types
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AstNode {
    /// Match all records (empty query)
    MatchAll,

    /// Comparison operation (field op value)
    Comparison(ComparisonNode),

    /// Logical operation (AND/OR)
    LogicalOp(LogicalOpNode),

    /// Unary operation (NOT)
    UnaryOp(UnaryOpNode),

    /// Collection operation (ANY/ALL/NONE)
    CollectionOp(CollectionOpNode),

    /// GeoIP expression
    GeoExpr(GeoExprNode),

    /// DNS lookup expression
    NslookupExpr(NslookupExprNode),

    /// Stats expression only
    StatsExpr(StatsNode),

    /// Query with stats
    QueryWithStats(QueryWithStatsNode),
}

/// Comparison node: field operator value
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ComparisonNode {
    /// Field name (may include dots for nested access)
    pub field: String,

    /// Comparison operator (eq, ne, gt, contains, etc.)
    pub operator: String,

    /// Expected value (None for exists/not_exists operators)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub value: Option<Value>,

    /// Field mutators (transformations applied to field)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field_mutators: Option<Vec<Mutator>>,

    /// Value mutators (transformations applied to value)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub value_mutators: Option<Vec<Mutator>>,

    /// Type hint for the field
    #[serde(skip_serializing_if = "Option::is_none")]
    pub type_hint: Option<String>,
}

/// Logical operation node: left AND/OR right
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LogicalOpNode {
    /// Logical operator (and, or)
    pub operator: String,

    /// Left operand
    pub left: Box<AstNode>,

    /// Right operand
    pub right: Box<AstNode>,
}

/// Unary operation node: NOT operand
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnaryOpNode {
    /// Unary operator (not)
    pub operator: String,

    /// Operand to negate
    pub operand: Box<AstNode>,
}

/// Collection operation node: ANY/ALL/NONE field op value
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CollectionOpNode {
    /// Collection operator (any, all, none, not_any, not_all, not_none)
    pub operator: String,

    /// Field name (should be an array/list field)
    pub field: String,

    /// Comparison operator to apply to elements
    pub comparison_operator: String,

    /// Value to compare against
    pub value: Value,

    /// Field mutators
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field_mutators: Option<Vec<Mutator>>,

    /// Type hint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub type_hint: Option<String>,
}

/// GeoIP expression node
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeoExprNode {
    /// Field containing IP address
    pub field: String,

    /// Field mutators
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field_mutators: Option<Vec<Mutator>>,

    /// Type hint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub type_hint: Option<String>,

    /// Conditions to apply to GeoIP results
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conditions: Option<Box<AstNode>>,

    /// GeoIP parameters
    #[serde(skip_serializing_if = "Option::is_none")]
    pub geo_params: Option<HashMap<String, Value>>,
}

/// DNS lookup expression node
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NslookupExprNode {
    /// Field containing hostname
    pub field: String,

    /// Field mutators
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field_mutators: Option<Vec<Mutator>>,

    /// Type hint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub type_hint: Option<String>,

    /// Conditions to apply to nslookup results
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conditions: Option<Box<AstNode>>,

    /// Nslookup parameters
    #[serde(skip_serializing_if = "Option::is_none")]
    pub nslookup_params: Option<HashMap<String, Value>>,
}

/// Visualization parameter value
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VizParamValue {
    /// String value
    String(String),
    /// Numeric integer value
    Integer(i64),
    /// Numeric float value
    Float(f64),
    /// Boolean value
    Boolean(bool),
}

/// Stats expression node
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StatsNode {
    /// Aggregation functions to apply
    pub aggregations: Vec<Aggregation>,

    /// Fields to group by
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub group_by: Vec<GroupBy>,

    /// Visualization hint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub viz_hint: Option<String>,

    /// Visualization parameters (e.g., title="Events", stacked=true)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub viz_params: Option<HashMap<String, VizParamValue>>,
}

/// Query with stats node: filter | stats
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QueryWithStatsNode {
    /// Filter expression
    pub filter: Box<AstNode>,

    /// Stats expression
    pub stats: StatsNode,
}

/// Aggregation function specification
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Aggregation {
    /// Aggregation function name (count, sum, avg, etc.)
    pub function: String,

    /// Field to aggregate (None for count(*))
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field: Option<String>,

    /// Alias for the result
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alias: Option<String>,

    /// Modifier (top, bottom)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub modifier: Option<String>,

    /// Limit for modifier
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,

    /// Percentile values (for percentile functions)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub percentile_values: Option<Vec<f64>>,

    /// Rank values (for percentile_rank functions)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rank_values: Option<Vec<f64>>,

    /// Field mutators for the aggregation field
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field_mutators: Option<Vec<Mutator>>,
}

/// Group by specification
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GroupBy {
    /// Field to group by
    pub field: String,

    /// Bucket size (for top N grouping)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bucket_size: Option<usize>,
}

/// Field/value mutator (transformation function)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Mutator {
    /// Mutator function name (lowercase, base64_encode, etc.)
    pub name: String,

    /// Positional arguments to the mutator
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub args: Vec<Value>,

    /// Named arguments to the mutator (e.g., find='world', delimiter=',')
    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty", default)]
    pub named_args: std::collections::HashMap<String, Value>,
}

/// Value types
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
    /// String value
    String(String),

    /// Integer value
    Integer(i64),

    /// Float value
    Float(f64),

    /// Boolean value
    Boolean(bool),

    /// List/array value
    List(Vec<Value>),

    /// Null value
    Null,
}

impl Value {
    /// Check if value is null
    pub fn is_null(&self) -> bool {
        matches!(self, Value::Null)
    }

    /// Convert to string if possible
    pub fn as_string(&self) -> Option<&str> {
        match self {
            Value::String(s) => Some(s),
            _ => None,
        }
    }

    /// Convert to integer if possible
    pub fn as_integer(&self) -> Option<i64> {
        match self {
            Value::Integer(i) => Some(*i),
            Value::Float(f) => Some(*f as i64),
            _ => None,
        }
    }

    /// Convert to float if possible
    pub fn as_float(&self) -> Option<f64> {
        match self {
            Value::Float(f) => Some(*f),
            Value::Integer(i) => Some(*i as f64),
            _ => None,
        }
    }

    /// Convert to boolean if possible
    pub fn as_bool(&self) -> Option<bool> {
        match self {
            Value::Boolean(b) => Some(*b),
            _ => None,
        }
    }

    /// Convert to list if possible
    pub fn as_list(&self) -> Option<&Vec<Value>> {
        match self {
            Value::List(l) => Some(l),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_value_conversions() {
        let str_val = Value::String("test".to_string());
        assert_eq!(str_val.as_string(), Some("test"));
        assert!(str_val.as_integer().is_none());

        let int_val = Value::Integer(42);
        assert_eq!(int_val.as_integer(), Some(42));
        assert_eq!(int_val.as_float(), Some(42.0));

        let null_val = Value::Null;
        assert!(null_val.is_null());
    }

    #[test]
    fn test_serialization() {
        let node = ComparisonNode {
            field: "test".to_string(),
            operator: "eq".to_string(),
            value: Some(Value::String("value".to_string())),
            field_mutators: None,
            value_mutators: None,
            type_hint: None,
        };

        let json = serde_json::to_string(&node).unwrap();
        assert!(json.contains("\"field\":\"test\""));
    }
}