presentar_yaml/
expression.rs

1//! Expression language for data binding.
2//!
3//! Syntax: `{{ source | transform | transform }}`
4
5use serde::{Deserialize, Serialize};
6
7/// Parsed expression.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Expression {
10    /// Source identifier (e.g., "data.transactions")
11    pub source: String,
12    /// Chain of transforms
13    pub transforms: Vec<Transform>,
14}
15
16/// A transform operation in the expression pipeline.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum Transform {
19    /// Filter rows: `filter(field=value)`
20    Filter {
21        /// Field to filter on
22        field: String,
23        /// Value to match
24        value: String,
25    },
26    /// Select columns: `select(field1, field2)`
27    Select {
28        /// Fields to select
29        fields: Vec<String>,
30    },
31    /// Sort rows: `sort(field, desc=true)`
32    Sort {
33        /// Field to sort by
34        field: String,
35        /// Sort descending
36        desc: bool,
37    },
38    /// Limit rows: `limit(n)`
39    Limit {
40        /// Maximum number of rows
41        n: usize,
42    },
43    /// Count rows
44    Count,
45    /// Sum a field: `sum(field)`
46    Sum {
47        /// Field to sum
48        field: String,
49    },
50    /// Average a field: `mean(field)`
51    Mean {
52        /// Field to average
53        field: String,
54    },
55    /// Rate over window: `rate(window)`
56    Rate {
57        /// Time window (e.g., "1m", "5m")
58        window: String,
59    },
60    /// Convert to percentage
61    Percentage,
62    /// Join with another dataset: `join(other, on=field)`
63    Join {
64        /// Other dataset
65        other: String,
66        /// Join field
67        on: String,
68    },
69    /// Sample rows: `sample(n)`
70    Sample {
71        /// Number of rows to sample
72        n: usize,
73    },
74    /// Group by field: `group_by(field)`
75    GroupBy {
76        /// Field to group by
77        field: String,
78    },
79    /// Get distinct values: `distinct(field)`
80    Distinct {
81        /// Field to get distinct values from (optional)
82        field: Option<String>,
83    },
84    /// Advanced filter with operators: `where(field, op, value)`
85    Where {
86        /// Field to filter on
87        field: String,
88        /// Comparison operator (eq, ne, gt, lt, gte, lte, contains)
89        op: String,
90        /// Value to compare
91        value: String,
92    },
93    /// Offset/skip rows: `offset(n)`
94    Offset {
95        /// Number of rows to skip
96        n: usize,
97    },
98    /// Minimum value: `min(field)`
99    Min {
100        /// Field to find minimum
101        field: String,
102    },
103    /// Maximum value: `max(field)`
104    Max {
105        /// Field to find maximum
106        field: String,
107    },
108    /// First n rows: `first(n)`
109    First {
110        /// Number of rows
111        n: usize,
112    },
113    /// Last n rows: `last(n)`
114    Last {
115        /// Number of rows
116        n: usize,
117    },
118    /// Flatten nested arrays: `flatten`
119    Flatten,
120    /// Reverse order: `reverse`
121    Reverse,
122    /// Map/transform each element: `map(expr)`
123    Map {
124        /// Expression to apply to each element
125        expr: String,
126    },
127    /// Reduce/fold elements: `reduce(initial, accumulator_expr)`
128    Reduce {
129        /// Initial value
130        initial: String,
131        /// Accumulator expression (uses `acc` and `item` variables)
132        expr: String,
133    },
134    /// Aggregate after group_by: `agg(field, op)`
135    Aggregate {
136        /// Field to aggregate
137        field: String,
138        /// Aggregation operation (sum, count, mean, min, max)
139        op: AggregateOp,
140    },
141    /// Pivot table: `pivot(row_field, col_field, value_field)`
142    Pivot {
143        /// Field for row labels
144        row_field: String,
145        /// Field for column headers
146        col_field: String,
147        /// Field for values
148        value_field: String,
149    },
150    /// Running total: `cumsum(field)`
151    CumulativeSum {
152        /// Field to sum
153        field: String,
154    },
155    /// Rank within group: `rank(field, method)`
156    Rank {
157        /// Field to rank by
158        field: String,
159        /// Ranking method (dense, ordinal, average)
160        method: RankMethod,
161    },
162    /// Moving average: `moving_avg(field, window)`
163    MovingAverage {
164        /// Field to average
165        field: String,
166        /// Window size
167        window: usize,
168    },
169    /// Percent change: `pct_change(field)`
170    PercentChange {
171        /// Field to calculate percent change
172        field: String,
173    },
174    /// Model suggestion: `suggest(prefix, count)` - for N-gram/autocomplete models
175    Suggest {
176        /// Input prefix to get suggestions for
177        prefix: String,
178        /// Maximum number of suggestions to return
179        count: usize,
180    },
181}
182
183/// Aggregation operations for group_by.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185pub enum AggregateOp {
186    /// Sum values
187    Sum,
188    /// Count values
189    Count,
190    /// Mean/average
191    Mean,
192    /// Minimum
193    Min,
194    /// Maximum
195    Max,
196    /// First value
197    First,
198    /// Last value
199    Last,
200}
201
202impl AggregateOp {
203    /// Parse from string.
204    pub fn from_str(s: &str) -> Option<Self> {
205        match s.to_lowercase().as_str() {
206            "sum" => Some(Self::Sum),
207            "count" => Some(Self::Count),
208            "mean" | "avg" | "average" => Some(Self::Mean),
209            "min" => Some(Self::Min),
210            "max" => Some(Self::Max),
211            "first" => Some(Self::First),
212            "last" => Some(Self::Last),
213            _ => None,
214        }
215    }
216}
217
218/// Ranking methods.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
220pub enum RankMethod {
221    /// Dense ranking (1, 2, 2, 3)
222    #[default]
223    Dense,
224    /// Ordinal ranking (1, 2, 3, 4)
225    Ordinal,
226    /// Average ranking (1, 2.5, 2.5, 4)
227    Average,
228}
229
230impl RankMethod {
231    /// Parse from string.
232    pub fn from_str(s: &str) -> Option<Self> {
233        match s.to_lowercase().as_str() {
234            "dense" => Some(Self::Dense),
235            "ordinal" => Some(Self::Ordinal),
236            "average" | "avg" => Some(Self::Average),
237            _ => None,
238        }
239    }
240}
241
242/// Expression parser.
243#[derive(Debug, Default)]
244pub struct ExpressionParser;
245
246impl ExpressionParser {
247    /// Create a new parser.
248    #[must_use]
249    pub const fn new() -> Self {
250        Self
251    }
252
253    /// Parse an expression string.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if the expression is invalid.
258    pub fn parse(&self, input: &str) -> Result<Expression, ExpressionError> {
259        let input = input.trim();
260
261        // Check for {{ }} wrapper
262        let inner = if input.starts_with("{{") && input.ends_with("}}") {
263            input[2..input.len() - 2].trim()
264        } else {
265            input
266        };
267
268        // Split by pipe
269        let parts: Vec<&str> = inner.split('|').map(str::trim).collect();
270
271        if parts.is_empty() {
272            return Err(ExpressionError::EmptyExpression);
273        }
274
275        let source = parts[0].to_string();
276        let mut transforms = Vec::new();
277
278        for part in &parts[1..] {
279            let transform = self.parse_transform(part)?;
280            transforms.push(transform);
281        }
282
283        Ok(Expression { source, transforms })
284    }
285
286    #[allow(clippy::too_many_lines)]
287    fn parse_transform(&self, input: &str) -> Result<Transform, ExpressionError> {
288        let input = input.trim();
289
290        // Check for function call: name(args)
291        if let Some(paren_pos) = input.find('(') {
292            let name = &input[..paren_pos];
293            let args_str = input[paren_pos + 1..].trim_end_matches(')').trim();
294
295            match name {
296                "filter" => {
297                    let (field, value) = self.parse_key_value(args_str)?;
298                    Ok(Transform::Filter { field, value })
299                }
300                "select" => {
301                    let fields: Vec<String> = args_str
302                        .split(',')
303                        .map(|s| s.trim().to_string())
304                        .filter(|s| !s.is_empty())
305                        .collect();
306                    Ok(Transform::Select { fields })
307                }
308                "sort" => {
309                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
310                    let field = (*parts.first().unwrap_or(&"")).to_string();
311                    let desc = parts.get(1).is_some_and(|s| s.contains("desc=true"));
312                    Ok(Transform::Sort { field, desc })
313                }
314                "limit" => {
315                    let n = args_str
316                        .parse()
317                        .map_err(|_| ExpressionError::InvalidArgument("limit".to_string()))?;
318                    Ok(Transform::Limit { n })
319                }
320                "sum" => Ok(Transform::Sum {
321                    field: args_str.to_string(),
322                }),
323                "mean" => Ok(Transform::Mean {
324                    field: args_str.to_string(),
325                }),
326                "rate" => Ok(Transform::Rate {
327                    window: args_str.to_string(),
328                }),
329                "join" => {
330                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
331                    let other = (*parts.first().unwrap_or(&"")).to_string();
332                    let on = parts
333                        .get(1)
334                        .and_then(|s| s.strip_prefix("on="))
335                        .unwrap_or("")
336                        .to_string();
337                    Ok(Transform::Join { other, on })
338                }
339                "sample" => {
340                    let n = args_str
341                        .parse()
342                        .map_err(|_| ExpressionError::InvalidArgument("sample".to_string()))?;
343                    Ok(Transform::Sample { n })
344                }
345                "group_by" => Ok(Transform::GroupBy {
346                    field: args_str.to_string(),
347                }),
348                "distinct" => {
349                    let field = if args_str.is_empty() {
350                        None
351                    } else {
352                        Some(args_str.to_string())
353                    };
354                    Ok(Transform::Distinct { field })
355                }
356                "where" => {
357                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
358                    if parts.len() < 3 {
359                        return Err(ExpressionError::InvalidArgument(
360                            "where requires field, op, value".to_string(),
361                        ));
362                    }
363                    Ok(Transform::Where {
364                        field: parts[0].to_string(),
365                        op: parts[1].to_string(),
366                        value: parts[2].to_string(),
367                    })
368                }
369                "offset" => {
370                    let n = args_str
371                        .parse()
372                        .map_err(|_| ExpressionError::InvalidArgument("offset".to_string()))?;
373                    Ok(Transform::Offset { n })
374                }
375                "min" => Ok(Transform::Min {
376                    field: args_str.to_string(),
377                }),
378                "max" => Ok(Transform::Max {
379                    field: args_str.to_string(),
380                }),
381                "first" => {
382                    let n = args_str
383                        .parse()
384                        .map_err(|_| ExpressionError::InvalidArgument("first".to_string()))?;
385                    Ok(Transform::First { n })
386                }
387                "last" => {
388                    let n = args_str
389                        .parse()
390                        .map_err(|_| ExpressionError::InvalidArgument("last".to_string()))?;
391                    Ok(Transform::Last { n })
392                }
393                "map" => Ok(Transform::Map {
394                    expr: args_str.to_string(),
395                }),
396                "reduce" => {
397                    let parts: Vec<&str> = args_str.splitn(2, ',').map(str::trim).collect();
398                    if parts.len() < 2 {
399                        return Err(ExpressionError::InvalidArgument(
400                            "reduce requires initial, expr".to_string(),
401                        ));
402                    }
403                    Ok(Transform::Reduce {
404                        initial: parts[0].to_string(),
405                        expr: parts[1].to_string(),
406                    })
407                }
408                "agg" | "aggregate" => {
409                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
410                    if parts.len() < 2 {
411                        return Err(ExpressionError::InvalidArgument(
412                            "agg requires field, op".to_string(),
413                        ));
414                    }
415                    let op = AggregateOp::from_str(parts[1]).ok_or_else(|| {
416                        ExpressionError::InvalidArgument(format!(
417                            "unknown aggregate op: {}",
418                            parts[1]
419                        ))
420                    })?;
421                    Ok(Transform::Aggregate {
422                        field: parts[0].to_string(),
423                        op,
424                    })
425                }
426                "pivot" => {
427                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
428                    if parts.len() < 3 {
429                        return Err(ExpressionError::InvalidArgument(
430                            "pivot requires row_field, col_field, value_field".to_string(),
431                        ));
432                    }
433                    Ok(Transform::Pivot {
434                        row_field: parts[0].to_string(),
435                        col_field: parts[1].to_string(),
436                        value_field: parts[2].to_string(),
437                    })
438                }
439                "cumsum" => Ok(Transform::CumulativeSum {
440                    field: args_str.to_string(),
441                }),
442                "rank" => {
443                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
444                    let field = (*parts.first().unwrap_or(&"")).to_string();
445                    let method = parts
446                        .get(1)
447                        .and_then(|s| RankMethod::from_str(s))
448                        .unwrap_or_default();
449                    Ok(Transform::Rank { field, method })
450                }
451                "moving_avg" | "ma" => {
452                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
453                    if parts.len() < 2 {
454                        return Err(ExpressionError::InvalidArgument(
455                            "moving_avg requires field, window".to_string(),
456                        ));
457                    }
458                    let window = parts[1].parse().map_err(|_| {
459                        ExpressionError::InvalidArgument("moving_avg window".to_string())
460                    })?;
461                    Ok(Transform::MovingAverage {
462                        field: parts[0].to_string(),
463                        window,
464                    })
465                }
466                "pct_change" => Ok(Transform::PercentChange {
467                    field: args_str.to_string(),
468                }),
469                "suggest" => {
470                    let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
471                    if parts.len() < 2 {
472                        return Err(ExpressionError::InvalidArgument(
473                            "suggest requires prefix, count".to_string(),
474                        ));
475                    }
476                    let count = parts[1].parse().map_err(|_| {
477                        ExpressionError::InvalidArgument("suggest count".to_string())
478                    })?;
479                    Ok(Transform::Suggest {
480                        prefix: parts[0].to_string(),
481                        count,
482                    })
483                }
484                _ => Err(ExpressionError::UnknownTransform(name.to_string())),
485            }
486        } else {
487            // Simple transform without args
488            match input {
489                "count" => Ok(Transform::Count),
490                "percentage" => Ok(Transform::Percentage),
491                "flatten" => Ok(Transform::Flatten),
492                "reverse" => Ok(Transform::Reverse),
493                "distinct" => Ok(Transform::Distinct { field: None }),
494                _ => Err(ExpressionError::UnknownTransform(input.to_string())),
495            }
496        }
497    }
498
499    fn parse_key_value(&self, input: &str) -> Result<(String, String), ExpressionError> {
500        let parts: Vec<&str> = input.splitn(2, '=').collect();
501        if parts.len() != 2 {
502            return Err(ExpressionError::InvalidArgument(input.to_string()));
503        }
504        Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
505    }
506}
507
508/// Expression parsing error.
509#[derive(Debug, Clone, PartialEq, Eq)]
510pub enum ExpressionError {
511    /// Empty expression
512    EmptyExpression,
513    /// Unknown transform function
514    UnknownTransform(String),
515    /// Invalid argument
516    InvalidArgument(String),
517}
518
519impl std::fmt::Display for ExpressionError {
520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521        match self {
522            Self::EmptyExpression => write!(f, "empty expression"),
523            Self::UnknownTransform(name) => write!(f, "unknown transform: {name}"),
524            Self::InvalidArgument(arg) => write!(f, "invalid argument: {arg}"),
525        }
526    }
527}
528
529impl std::error::Error for ExpressionError {}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[test]
536    fn test_parse_simple_source() {
537        let parser = ExpressionParser::new();
538        let expr = parser.parse("data.transactions").unwrap();
539        assert_eq!(expr.source, "data.transactions");
540        assert!(expr.transforms.is_empty());
541    }
542
543    #[test]
544    fn test_parse_with_braces() {
545        let parser = ExpressionParser::new();
546        let expr = parser.parse("{{ data.transactions }}").unwrap();
547        assert_eq!(expr.source, "data.transactions");
548    }
549
550    #[test]
551    fn test_parse_count() {
552        let parser = ExpressionParser::new();
553        let expr = parser.parse("{{ data.items | count }}").unwrap();
554        assert_eq!(expr.transforms, vec![Transform::Count]);
555    }
556
557    #[test]
558    fn test_parse_filter() {
559        let parser = ExpressionParser::new();
560        let expr = parser.parse("{{ data | filter(status=active) }}").unwrap();
561        assert_eq!(
562            expr.transforms,
563            vec![Transform::Filter {
564                field: "status".to_string(),
565                value: "active".to_string(),
566            }]
567        );
568    }
569
570    #[test]
571    fn test_parse_select() {
572        let parser = ExpressionParser::new();
573        let expr = parser
574            .parse("{{ data | select(id, name, email) }}")
575            .unwrap();
576        assert_eq!(
577            expr.transforms,
578            vec![Transform::Select {
579                fields: vec!["id".to_string(), "name".to_string(), "email".to_string()],
580            }]
581        );
582    }
583
584    #[test]
585    fn test_parse_sort() {
586        let parser = ExpressionParser::new();
587        let expr = parser
588            .parse("{{ data | sort(created_at, desc=true) }}")
589            .unwrap();
590        assert_eq!(
591            expr.transforms,
592            vec![Transform::Sort {
593                field: "created_at".to_string(),
594                desc: true,
595            }]
596        );
597    }
598
599    #[test]
600    fn test_parse_limit() {
601        let parser = ExpressionParser::new();
602        let expr = parser.parse("{{ data | limit(10) }}").unwrap();
603        assert_eq!(expr.transforms, vec![Transform::Limit { n: 10 }]);
604    }
605
606    #[test]
607    fn test_parse_chain() {
608        let parser = ExpressionParser::new();
609        let expr = parser
610            .parse("{{ data.transactions | filter(status=completed) | count }}")
611            .unwrap();
612
613        assert_eq!(expr.source, "data.transactions");
614        assert_eq!(expr.transforms.len(), 2);
615        assert_eq!(
616            expr.transforms[0],
617            Transform::Filter {
618                field: "status".to_string(),
619                value: "completed".to_string(),
620            }
621        );
622        assert_eq!(expr.transforms[1], Transform::Count);
623    }
624
625    #[test]
626    fn test_parse_join() {
627        let parser = ExpressionParser::new();
628        let expr = parser
629            .parse("{{ data.orders | join(data.customers, on=customer_id) }}")
630            .unwrap();
631
632        assert_eq!(
633            expr.transforms,
634            vec![Transform::Join {
635                other: "data.customers".to_string(),
636                on: "customer_id".to_string(),
637            }]
638        );
639    }
640
641    #[test]
642    fn test_parse_sample() {
643        let parser = ExpressionParser::new();
644        let expr = parser.parse("{{ data | sample(100) }}").unwrap();
645        assert_eq!(expr.transforms, vec![Transform::Sample { n: 100 }]);
646    }
647
648    #[test]
649    fn test_parse_sum() {
650        let parser = ExpressionParser::new();
651        let expr = parser.parse("{{ data | sum(amount) }}").unwrap();
652        assert_eq!(
653            expr.transforms,
654            vec![Transform::Sum {
655                field: "amount".to_string(),
656            }]
657        );
658    }
659
660    #[test]
661    fn test_parse_error_unknown_transform() {
662        let parser = ExpressionParser::new();
663        let result = parser.parse("{{ data | unknown() }}");
664        assert!(matches!(result, Err(ExpressionError::UnknownTransform(_))));
665    }
666
667    #[test]
668    fn test_expression_error_display() {
669        assert_eq!(
670            ExpressionError::EmptyExpression.to_string(),
671            "empty expression"
672        );
673        assert_eq!(
674            ExpressionError::UnknownTransform("foo".to_string()).to_string(),
675            "unknown transform: foo"
676        );
677    }
678
679    // =========================================================================
680    // Map/Reduce/GroupBy and Advanced Transform Tests
681    // =========================================================================
682
683    #[test]
684    fn test_parse_map() {
685        let parser = ExpressionParser::new();
686        let expr = parser.parse("{{ data | map(item.value * 2) }}").unwrap();
687        assert_eq!(
688            expr.transforms,
689            vec![Transform::Map {
690                expr: "item.value * 2".to_string(),
691            }]
692        );
693    }
694
695    #[test]
696    fn test_parse_reduce() {
697        let parser = ExpressionParser::new();
698        let expr = parser
699            .parse("{{ data | reduce(0, acc + item.value) }}")
700            .unwrap();
701        assert_eq!(
702            expr.transforms,
703            vec![Transform::Reduce {
704                initial: "0".to_string(),
705                expr: "acc + item.value".to_string(),
706            }]
707        );
708    }
709
710    #[test]
711    fn test_parse_reduce_missing_args() {
712        let parser = ExpressionParser::new();
713        let result = parser.parse("{{ data | reduce(0) }}");
714        assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
715    }
716
717    #[test]
718    fn test_parse_aggregate_sum() {
719        let parser = ExpressionParser::new();
720        let expr = parser
721            .parse("{{ data | group_by(category) | agg(amount, sum) }}")
722            .unwrap();
723        assert_eq!(
724            expr.transforms,
725            vec![
726                Transform::GroupBy {
727                    field: "category".to_string()
728                },
729                Transform::Aggregate {
730                    field: "amount".to_string(),
731                    op: AggregateOp::Sum,
732                },
733            ]
734        );
735    }
736
737    #[test]
738    fn test_parse_aggregate_mean() {
739        let parser = ExpressionParser::new();
740        let expr = parser.parse("{{ data | agg(price, mean) }}").unwrap();
741        assert_eq!(
742            expr.transforms,
743            vec![Transform::Aggregate {
744                field: "price".to_string(),
745                op: AggregateOp::Mean,
746            }]
747        );
748    }
749
750    #[test]
751    fn test_parse_aggregate_count() {
752        let parser = ExpressionParser::new();
753        let expr = parser.parse("{{ data | agg(id, count) }}").unwrap();
754        assert_eq!(
755            expr.transforms,
756            vec![Transform::Aggregate {
757                field: "id".to_string(),
758                op: AggregateOp::Count,
759            }]
760        );
761    }
762
763    #[test]
764    fn test_parse_aggregate_alias() {
765        let parser = ExpressionParser::new();
766        let expr = parser.parse("{{ data | aggregate(value, max) }}").unwrap();
767        assert_eq!(
768            expr.transforms,
769            vec![Transform::Aggregate {
770                field: "value".to_string(),
771                op: AggregateOp::Max,
772            }]
773        );
774    }
775
776    #[test]
777    fn test_parse_aggregate_invalid_op() {
778        let parser = ExpressionParser::new();
779        let result = parser.parse("{{ data | agg(field, unknown_op) }}");
780        assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
781    }
782
783    #[test]
784    fn test_parse_pivot() {
785        let parser = ExpressionParser::new();
786        let expr = parser
787            .parse("{{ data | pivot(date, product, sales) }}")
788            .unwrap();
789        assert_eq!(
790            expr.transforms,
791            vec![Transform::Pivot {
792                row_field: "date".to_string(),
793                col_field: "product".to_string(),
794                value_field: "sales".to_string(),
795            }]
796        );
797    }
798
799    #[test]
800    fn test_parse_pivot_missing_args() {
801        let parser = ExpressionParser::new();
802        let result = parser.parse("{{ data | pivot(date, product) }}");
803        assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
804    }
805
806    #[test]
807    fn test_parse_cumsum() {
808        let parser = ExpressionParser::new();
809        let expr = parser.parse("{{ data | cumsum(balance) }}").unwrap();
810        assert_eq!(
811            expr.transforms,
812            vec![Transform::CumulativeSum {
813                field: "balance".to_string(),
814            }]
815        );
816    }
817
818    #[test]
819    fn test_parse_rank_default() {
820        let parser = ExpressionParser::new();
821        let expr = parser.parse("{{ data | rank(score) }}").unwrap();
822        assert_eq!(
823            expr.transforms,
824            vec![Transform::Rank {
825                field: "score".to_string(),
826                method: RankMethod::Dense,
827            }]
828        );
829    }
830
831    #[test]
832    fn test_parse_rank_ordinal() {
833        let parser = ExpressionParser::new();
834        let expr = parser.parse("{{ data | rank(score, ordinal) }}").unwrap();
835        assert_eq!(
836            expr.transforms,
837            vec![Transform::Rank {
838                field: "score".to_string(),
839                method: RankMethod::Ordinal,
840            }]
841        );
842    }
843
844    #[test]
845    fn test_parse_rank_average() {
846        let parser = ExpressionParser::new();
847        let expr = parser.parse("{{ data | rank(score, average) }}").unwrap();
848        assert_eq!(
849            expr.transforms,
850            vec![Transform::Rank {
851                field: "score".to_string(),
852                method: RankMethod::Average,
853            }]
854        );
855    }
856
857    #[test]
858    fn test_parse_moving_average() {
859        let parser = ExpressionParser::new();
860        let expr = parser.parse("{{ data | moving_avg(price, 5) }}").unwrap();
861        assert_eq!(
862            expr.transforms,
863            vec![Transform::MovingAverage {
864                field: "price".to_string(),
865                window: 5,
866            }]
867        );
868    }
869
870    #[test]
871    fn test_parse_moving_average_alias() {
872        let parser = ExpressionParser::new();
873        let expr = parser.parse("{{ data | ma(price, 10) }}").unwrap();
874        assert_eq!(
875            expr.transforms,
876            vec![Transform::MovingAverage {
877                field: "price".to_string(),
878                window: 10,
879            }]
880        );
881    }
882
883    #[test]
884    fn test_parse_moving_average_missing_window() {
885        let parser = ExpressionParser::new();
886        let result = parser.parse("{{ data | moving_avg(price) }}");
887        assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
888    }
889
890    #[test]
891    fn test_parse_pct_change() {
892        let parser = ExpressionParser::new();
893        let expr = parser.parse("{{ data | pct_change(value) }}").unwrap();
894        assert_eq!(
895            expr.transforms,
896            vec![Transform::PercentChange {
897                field: "value".to_string(),
898            }]
899        );
900    }
901
902    #[test]
903    fn test_parse_complex_pipeline() {
904        let parser = ExpressionParser::new();
905        let expr = parser
906            .parse("{{ data | filter(status=active) | group_by(category) | agg(amount, sum) | sort(amount, desc=true) | limit(10) }}")
907            .unwrap();
908
909        assert_eq!(expr.transforms.len(), 5);
910        assert!(matches!(expr.transforms[0], Transform::Filter { .. }));
911        assert!(matches!(expr.transforms[1], Transform::GroupBy { .. }));
912        assert!(matches!(expr.transforms[2], Transform::Aggregate { .. }));
913        assert!(matches!(
914            expr.transforms[3],
915            Transform::Sort { desc: true, .. }
916        ));
917        assert!(matches!(expr.transforms[4], Transform::Limit { n: 10 }));
918    }
919
920    #[test]
921    fn test_parse_map_reduce_pipeline() {
922        let parser = ExpressionParser::new();
923        let expr = parser
924            .parse("{{ data | map(item.value * 2) | reduce(0, acc + item) }}")
925            .unwrap();
926
927        assert_eq!(expr.transforms.len(), 2);
928        assert!(matches!(expr.transforms[0], Transform::Map { .. }));
929        assert!(matches!(expr.transforms[1], Transform::Reduce { .. }));
930    }
931
932    // =========================================================================
933    // AggregateOp Tests
934    // =========================================================================
935
936    #[test]
937    fn test_aggregate_op_from_str() {
938        assert_eq!(AggregateOp::from_str("sum"), Some(AggregateOp::Sum));
939        assert_eq!(AggregateOp::from_str("count"), Some(AggregateOp::Count));
940        assert_eq!(AggregateOp::from_str("mean"), Some(AggregateOp::Mean));
941        assert_eq!(AggregateOp::from_str("avg"), Some(AggregateOp::Mean));
942        assert_eq!(AggregateOp::from_str("average"), Some(AggregateOp::Mean));
943        assert_eq!(AggregateOp::from_str("min"), Some(AggregateOp::Min));
944        assert_eq!(AggregateOp::from_str("max"), Some(AggregateOp::Max));
945        assert_eq!(AggregateOp::from_str("first"), Some(AggregateOp::First));
946        assert_eq!(AggregateOp::from_str("last"), Some(AggregateOp::Last));
947        assert_eq!(AggregateOp::from_str("unknown"), None);
948    }
949
950    #[test]
951    fn test_aggregate_op_case_insensitive() {
952        assert_eq!(AggregateOp::from_str("SUM"), Some(AggregateOp::Sum));
953        assert_eq!(AggregateOp::from_str("Sum"), Some(AggregateOp::Sum));
954        assert_eq!(AggregateOp::from_str("MEAN"), Some(AggregateOp::Mean));
955    }
956
957    // =========================================================================
958    // RankMethod Tests
959    // =========================================================================
960
961    #[test]
962    fn test_rank_method_from_str() {
963        assert_eq!(RankMethod::from_str("dense"), Some(RankMethod::Dense));
964        assert_eq!(RankMethod::from_str("ordinal"), Some(RankMethod::Ordinal));
965        assert_eq!(RankMethod::from_str("average"), Some(RankMethod::Average));
966        assert_eq!(RankMethod::from_str("avg"), Some(RankMethod::Average));
967        assert_eq!(RankMethod::from_str("unknown"), None);
968    }
969
970    #[test]
971    fn test_rank_method_default() {
972        assert_eq!(RankMethod::default(), RankMethod::Dense);
973    }
974}