package filter import ( "time" "github.com/pkg/errors" exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) // parseContext carries the schema plus the frozen evaluation time used to fold // the `now` variable into a constant. Freezing once per compile guarantees a // single filter observes a single instant. type parseContext struct { schema Schema now time.Time } func buildCondition(expr *exprv1.Expr, pc parseContext) (Condition, error) { switch v := expr.ExprKind.(type) { case *exprv1.Expr_CallExpr: return buildCallCondition(v.CallExpr, pc) case *exprv1.Expr_ConstExpr: val, err := getConstValue(expr) if err != nil { return nil, err } if v, ok := val.(bool); ok { return &ConstantCondition{Value: v}, nil } return nil, errors.New("filter must evaluate to a boolean value") case *exprv1.Expr_IdentExpr: name := v.IdentExpr.GetName() field, ok := pc.schema.Field(name) if !ok { return nil, errors.Errorf("unknown identifier %q", name) } if field.Type != FieldTypeBool { return nil, errors.Errorf("identifier %q is not boolean", name) } return &FieldPredicateCondition{Field: name}, nil case *exprv1.Expr_ComprehensionExpr: return buildComprehensionCondition(v.ComprehensionExpr, pc.schema) default: return nil, errors.New("unsupported top-level expression") } } func buildCallCondition(call *exprv1.Expr_Call, pc parseContext) (Condition, error) { switch call.Function { case "_&&_": if len(call.Args) != 2 { return nil, errors.New("logical AND expects two arguments") } left, err := buildCondition(call.Args[0], pc) if err != nil { return nil, err } right, err := buildCondition(call.Args[1], pc) if err != nil { return nil, err } return &LogicalCondition{ Operator: LogicalAnd, Left: left, Right: right, }, nil case "_||_": if len(call.Args) != 2 { return nil, errors.New("logical OR expects two arguments") } left, err := buildCondition(call.Args[0], pc) if err != nil { return nil, err } right, err := buildCondition(call.Args[1], pc) if err != nil { return nil, err } return &LogicalCondition{ Operator: LogicalOr, Left: left, Right: right, }, nil case "!_": if len(call.Args) != 1 { return nil, errors.New("logical NOT expects one argument") } child, err := buildCondition(call.Args[0], pc) if err != nil { return nil, err } return &NotCondition{Expr: child}, nil case "_==_", "_!=_", "_<_", "_>_", "_<=_", "_>=_": return buildComparisonCondition(call, pc) case "@in": return buildInCondition(call, pc) case "contains": return buildTextMatchCondition(call, pc.schema, TextMatchContains) case "startsWith": return buildTextMatchCondition(call, pc.schema, TextMatchPrefix) case "endsWith": return buildTextMatchCondition(call, pc.schema, TextMatchSuffix) case "matches": return buildMatchesCondition(call, pc.schema) case "sets.contains", "sets.intersects", "sets.equivalent": return buildSetCondition(call, pc) default: val, ok, err := evaluateBool(call) if err != nil { return nil, err } if ok { return &ConstantCondition{Value: val}, nil } return nil, errors.Errorf("unsupported call expression %q", call.Function) } } func buildComparisonCondition(call *exprv1.Expr_Call, pc parseContext) (Condition, error) { if len(call.Args) != 2 { return nil, errors.New("comparison expects two arguments") } op, err := toComparisonOperator(call.Function) if err != nil { return nil, err } left, err := buildValueExpr(call.Args[0], pc) if err != nil { return nil, err } right, err := buildValueExpr(call.Args[1], pc) if err != nil { return nil, err } // The renderer expects a field/function/accessor on the left. A folded // literal on the left (e.g. now.getMonth() == created_ts.getMonth()) swaps // operands; two literals fold to a constant outcome. if leftLit, ok := left.(*LiteralValue); ok { if rightLit, ok := right.(*LiteralValue); ok { outcome, err := compareLiterals(leftLit.Value, op, rightLit.Value) if err != nil { return nil, err } return &ConstantCondition{Value: outcome}, nil } left, right = right, left op = mirrorComparisonOperator(op) } // If the left side is a field, validate allowed operators. if field, ok := left.(*FieldRef); ok { def, exists := pc.schema.Field(field.Name) if !exists { return nil, errors.Errorf("unknown identifier %q", field.Name) } if def.Kind == FieldKindVirtualAlias { def, exists = pc.schema.ResolveAlias(field.Name) if !exists { return nil, errors.Errorf("invalid alias %q", field.Name) } } if def.AllowedComparisonOps != nil { if _, allowed := def.AllowedComparisonOps[op]; !allowed { return nil, errors.Errorf("operator %s not allowed for field %q", op, field.Name) } } } return &ComparisonCondition{ Left: left, Operator: op, Right: right, }, nil } // mirrorComparisonOperator flips an operator so swapped operands keep the // original meaning (a < b ⇔ b > a). func mirrorComparisonOperator(op ComparisonOperator) ComparisonOperator { switch op { case CompareLt: return CompareGt case CompareLte: return CompareGte case CompareGt: return CompareLt case CompareGte: return CompareLte default: return op } } // compareLiterals evaluates a comparison whose operands both folded to // constants (e.g. now.getFullYear() >= 2026) into a boolean outcome. func compareLiterals(left any, op ComparisonOperator, right any) (bool, error) { if l, r, ok := asFloats(left, right); ok { switch op { case CompareEq: return l == r, nil case CompareNeq: return l != r, nil case CompareLt: return l < r, nil case CompareLte: return l <= r, nil case CompareGt: return l > r, nil case CompareGte: return l >= r, nil } } if l, ok := left.(string); ok { if r, ok := right.(string); ok { switch op { case CompareEq: return l == r, nil case CompareNeq: return l != r, nil case CompareLt: return l < r, nil case CompareLte: return l <= r, nil case CompareGt: return l > r, nil case CompareGte: return l >= r, nil } } } if l, ok := left.(bool); ok { if r, ok := right.(bool); ok { switch op { case CompareEq: return l == r, nil case CompareNeq: return l != r, nil } } } return false, errors.Errorf("unsupported constant comparison %T %s %T", left, op, right) } // asFloats widens both operands to float64 when each is numeric. func asFloats(left, right any) (float64, float64, bool) { l, lok := toFloat(left) r, rok := toFloat(right) return l, r, lok && rok } func toFloat(v any) (float64, bool) { switch x := v.(type) { case int64: return float64(x), true case float64: return x, true } return 0, false } func buildInCondition(call *exprv1.Expr_Call, pc parseContext) (Condition, error) { if len(call.Args) != 2 { return nil, errors.New("in operator expects two arguments") } // Handle identifier in list syntax. if identName, err := getIdentName(call.Args[0]); err == nil { if field, ok := pc.schema.Field(identName); ok && field.Kind == FieldKindVirtualAlias { if _, aliasOk := pc.schema.ResolveAlias(identName); !aliasOk { return nil, errors.Errorf("invalid alias %q", identName) } } else if !ok { return nil, errors.Errorf("unknown identifier %q", identName) } if listExpr := call.Args[1].GetListExpr(); listExpr != nil { values := make([]ValueExpr, 0, len(listExpr.Elements)) for _, element := range listExpr.Elements { value, err := buildValueExpr(element, pc) if err != nil { return nil, err } values = append(values, value) } return &InCondition{ Left: &FieldRef{Name: identName}, Values: values, }, nil } } // Handle "value in identifier" syntax. if identName, err := getIdentName(call.Args[1]); err == nil { if _, ok := pc.schema.Field(identName); !ok { return nil, errors.Errorf("unknown identifier %q", identName) } element, err := buildValueExpr(call.Args[0], pc) if err != nil { return nil, err } return &ElementInCondition{ Element: element, Field: identName, }, nil } return nil, errors.New("invalid use of in operator") } func buildTextMatchCondition(call *exprv1.Expr_Call, schema Schema, mode TextMatchMode) (Condition, error) { if call.Target == nil { return nil, errors.New("text match requires a target") } targetName, err := getIdentName(call.Target) if err != nil { return nil, err } field, ok := schema.Field(targetName) if !ok { return nil, errors.Errorf("unknown identifier %q", targetName) } if !field.SupportsContains { return nil, errors.Errorf("identifier %q does not support text matching", targetName) } if len(call.Args) != 1 { return nil, errors.New("text match expects exactly one argument") } value, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "text match only supports literal arguments") } str, ok := value.(string) if !ok { return nil, errors.New("text match argument must be a string") } return &TextMatchCondition{ Field: targetName, Mode: mode, Value: str, }, nil } func buildMatchesCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) { if call.Target == nil { return nil, errors.New("matches requires a target") } targetName, err := getIdentName(call.Target) if err != nil { return nil, err } field, ok := schema.Field(targetName) if !ok { return nil, errors.Errorf("unknown identifier %q", targetName) } if !field.SupportsContains { return nil, errors.Errorf("identifier %q does not support matches()", targetName) } if len(call.Args) != 1 { return nil, errors.New("matches expects exactly one argument") } value, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "matches only supports literal arguments") } pattern, ok := value.(string) if !ok { return nil, errors.New("matches argument must be a string") } return &RegexCondition{ Field: targetName, Pattern: pattern, }, nil } func buildValueExpr(expr *exprv1.Expr, pc parseContext) (ValueExpr, error) { if identName, err := getIdentName(expr); err == nil { // `now` is not a schema field; it folds to the frozen evaluation time. if identName == "now" { return &LiteralValue{Value: pc.now.Unix()}, nil } if _, ok := pc.schema.Field(identName); !ok { return nil, errors.Errorf("unknown identifier %q", identName) } return &FieldRef{Name: identName}, nil } if literal, err := getConstValue(expr); err == nil { return &LiteralValue{Value: literal}, nil } if value, ok, err := evaluateNumeric(expr, pc.now); err != nil { return nil, err } else if ok { return &LiteralValue{Value: value}, nil } if boolVal, ok, err := evaluateBoolExpr(expr); err != nil { return nil, err } else if ok { return &LiteralValue{Value: boolVal}, nil } if call := expr.GetCallExpr(); call != nil { if call.Target != nil && isTimestampAccessor(call.Function) { return buildTimestampAccessor(call, pc) } switch call.Function { case "size": if len(call.Args) != 1 { return nil, errors.New("size() expects one argument") } arg, err := buildValueExpr(call.Args[0], pc) if err != nil { return nil, err } return &FunctionValue{ Name: "size", Args: []ValueExpr{arg}, }, nil case "_+_", "_-_", "_*_": value, ok, err := evaluateNumeric(expr, pc.now) if err != nil { return nil, err } if ok { return &LiteralValue{Value: value}, nil } default: // Fall through to error return below } } return nil, errors.New("unsupported value expression") } func toComparisonOperator(fn string) (ComparisonOperator, error) { switch fn { case "_==_": return CompareEq, nil case "_!=_": return CompareNeq, nil case "_<_": return CompareLt, nil case "_>_": return CompareGt, nil case "_<=_": return CompareLte, nil case "_>=_": return CompareGte, nil default: return "", errors.Errorf("unsupported comparison operator %q", fn) } } func getIdentName(expr *exprv1.Expr) (string, error) { if ident := expr.GetIdentExpr(); ident != nil { return ident.GetName(), nil } return "", errors.New("expression is not an identifier") } func getConstValue(expr *exprv1.Expr) (interface{}, error) { v, ok := expr.ExprKind.(*exprv1.Expr_ConstExpr) if !ok { return nil, errors.New("expression is not a literal") } switch x := v.ConstExpr.ConstantKind.(type) { case *exprv1.Constant_StringValue: return v.ConstExpr.GetStringValue(), nil case *exprv1.Constant_Int64Value: return v.ConstExpr.GetInt64Value(), nil case *exprv1.Constant_Uint64Value: return int64(v.ConstExpr.GetUint64Value()), nil case *exprv1.Constant_DoubleValue: return v.ConstExpr.GetDoubleValue(), nil case *exprv1.Constant_BoolValue: return v.ConstExpr.GetBoolValue(), nil case *exprv1.Constant_NullValue: return nil, nil default: return nil, errors.Errorf("unsupported constant %T", x) } } func evaluateBool(call *exprv1.Expr_Call) (bool, bool, error) { val, ok, err := evaluateBoolExpr(&exprv1.Expr{ExprKind: &exprv1.Expr_CallExpr{CallExpr: call}}) return val, ok, err } func evaluateBoolExpr(expr *exprv1.Expr) (bool, bool, error) { if literal, err := getConstValue(expr); err == nil { if b, ok := literal.(bool); ok { return b, true, nil } return false, false, nil } if call := expr.GetCallExpr(); call != nil && call.Function == "!_" { if len(call.Args) != 1 { return false, false, errors.New("NOT expects exactly one argument") } val, ok, err := evaluateBoolExpr(call.Args[0]) if err != nil || !ok { return false, false, err } return !val, true, nil } return false, false, nil } // evaluateNumeric constant-folds an expression to an int64 measured in seconds: // timestamps and `now` fold to Unix epoch seconds, durations fold to a number of // seconds, and the two combine through standard arithmetic. CEL has already // type-checked the operand combinations, so the folded int math is well-formed. func evaluateNumeric(expr *exprv1.Expr, now time.Time) (int64, bool, error) { if literal, err := getConstValue(expr); err == nil { switch v := literal.(type) { case int64: return v, true, nil case float64: return int64(v), true, nil } return 0, false, nil } // The `now` variable folds to the frozen evaluation time. if ident := expr.GetIdentExpr(); ident != nil { if ident.GetName() == "now" { return now.Unix(), true, nil } return 0, false, nil } call := expr.GetCallExpr() if call == nil { return 0, false, nil } switch call.Function { case "timestamp": return evaluateTimestamp(call) case "duration": return evaluateDuration(call) case "_+_", "_-_", "_*_", "_/_", "_%_": if len(call.Args) != 2 { return 0, false, errors.New("arithmetic requires two arguments") } left, ok, err := evaluateNumeric(call.Args[0], now) if err != nil { return 0, false, err } if !ok { return 0, false, nil } right, ok, err := evaluateNumeric(call.Args[1], now) if err != nil { return 0, false, err } if !ok { return 0, false, nil } switch call.Function { case "_+_": return left + right, true, nil case "_-_": return left - right, true, nil case "_*_": return left * right, true, nil case "_/_": if right == 0 { return 0, false, errors.New("division by zero") } return left / right, true, nil case "_%_": if right == 0 { return 0, false, errors.New("modulo by zero") } return left % right, true, nil default: return 0, false, errors.Errorf("unsupported arithmetic operator %q", call.Function) } default: return 0, false, nil } } // evaluateTimestamp folds timestamp("RFC3339") and timestamp() into // Unix epoch seconds. func evaluateTimestamp(call *exprv1.Expr_Call) (int64, bool, error) { if len(call.Args) != 1 { return 0, false, errors.New("timestamp() expects one argument") } value, err := getConstValue(call.Args[0]) if err != nil { return 0, false, errors.Wrap(err, "timestamp() only supports literal arguments") } switch v := value.(type) { case string: ts, err := time.Parse(time.RFC3339, v) if err != nil { return 0, false, errors.Wrap(err, "invalid timestamp literal") } return ts.Unix(), true, nil case int64: return v, true, nil default: return 0, false, errors.New("timestamp() argument must be an RFC3339 string or epoch int") } } // evaluateDuration folds duration("") into a number of seconds. func evaluateDuration(call *exprv1.Expr_Call) (int64, bool, error) { if len(call.Args) != 1 { return 0, false, errors.New("duration() expects one argument") } value, err := getConstValue(call.Args[0]) if err != nil { return 0, false, errors.Wrap(err, "duration() only supports literal arguments") } str, ok := value.(string) if !ok { return 0, false, errors.New("duration() argument must be a string") } d, err := time.ParseDuration(str) if err != nil { return 0, false, errors.Wrap(err, "invalid duration literal") } return int64(d.Seconds()), true, nil } // timestampAccessors is the set of supported CEL timestamp accessor methods. var timestampAccessors = map[string]bool{ "getFullYear": true, "getMonth": true, "getDate": true, "getDayOfMonth": true, "getDayOfWeek": true, "getDayOfYear": true, "getHours": true, "getMinutes": true, "getSeconds": true, } func isTimestampAccessor(name string) bool { return timestampAccessors[name] } // buildTimestampAccessor converts created_ts.getMonth() into a FieldAccessorValue. // A `now` target folds to a literal date part of the frozen evaluation time, so // expressions like created_ts.getMonth() == now.getMonth() stay dynamic per compile. // Timezone arguments are rejected; extraction is UTC (see renderer). func buildTimestampAccessor(call *exprv1.Expr_Call, pc parseContext) (ValueExpr, error) { targetName, err := getIdentName(call.Target) if err != nil { return nil, errors.Wrap(err, "timestamp accessor requires a field target") } if len(call.Args) != 0 { return nil, errors.Errorf("%s() with a timezone argument is not supported", call.Function) } if targetName == "now" { return &LiteralValue{Value: foldNowAccessor(call.Function, pc.now)}, nil } field, ok := pc.schema.Field(targetName) if !ok { return nil, errors.Errorf("unknown identifier %q", targetName) } if field.Type != FieldTypeTimestamp { return nil, errors.Errorf("%s() is only valid on timestamp fields, got %q", call.Function, targetName) } return &FieldAccessorValue{Field: targetName, Accessor: call.Function}, nil } // foldNowAccessor evaluates a timestamp accessor against the frozen evaluation // time in UTC, matching CEL result bases (0-based month, day-of-month, day-of-week // with 0 = Sunday, and day-of-year; 1-based getDate). func foldNowAccessor(accessor string, now time.Time) int64 { t := now.UTC() switch accessor { case "getFullYear": return int64(t.Year()) case "getMonth": return int64(t.Month()) - 1 case "getDate": return int64(t.Day()) case "getDayOfMonth": return int64(t.Day()) - 1 case "getDayOfWeek": return int64(t.Weekday()) case "getDayOfYear": return int64(t.YearDay()) - 1 case "getHours": return int64(t.Hour()) case "getMinutes": return int64(t.Minute()) case "getSeconds": return int64(t.Second()) } return 0 } // buildSetCondition desugars ext.Sets() operations over a JSON list field into // existing IR: membership reduces to ElementInCondition, and equivalence adds a // length check. This relies on the list field being a set (no duplicates), which // holds for memo tags. func buildSetCondition(call *exprv1.Expr_Call, pc parseContext) (Condition, error) { if len(call.Args) != 2 { return nil, errors.Errorf("%s expects two arguments", call.Function) } fieldName, err := getIdentName(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "set operations require a list field as the first argument") } field, ok := pc.schema.Field(fieldName) if !ok { return nil, errors.Errorf("unknown identifier %q", fieldName) } if field.Kind != FieldKindJSONList { return nil, errors.Errorf("set operations require a list field, got %q", fieldName) } listExpr := call.Args[1].GetListExpr() if listExpr == nil { return nil, errors.New("set operations require a list literal as the second argument") } values := make([]string, 0, len(listExpr.Elements)) for _, el := range listExpr.Elements { v, err := getConstValue(el) if err != nil { return nil, errors.Wrap(err, "set operations only support literal string elements") } s, ok := v.(string) if !ok { return nil, errors.New("set operations require string elements") } values = append(values, s) } membership := func(s string) Condition { return &ElementInCondition{Element: &LiteralValue{Value: s}, Field: fieldName} } sizeEquals := func(n int) Condition { return &ComparisonCondition{ Left: &FunctionValue{Name: "size", Args: []ValueExpr{&FieldRef{Name: fieldName}}}, Operator: CompareEq, Right: &LiteralValue{Value: int64(n)}, } } switch call.Function { case "sets.contains": if len(values) == 0 { return &ConstantCondition{Value: true}, nil } return combineConditions(LogicalAnd, mapConditions(values, membership)), nil case "sets.intersects": if len(values) == 0 { return &ConstantCondition{Value: false}, nil } return combineConditions(LogicalOr, mapConditions(values, membership)), nil case "sets.equivalent": distinct := distinctStrings(values) if len(distinct) == 0 { return sizeEquals(0), nil } contains := combineConditions(LogicalAnd, mapConditions(distinct, membership)) return &LogicalCondition{Operator: LogicalAnd, Left: contains, Right: sizeEquals(len(distinct))}, nil default: return nil, errors.Errorf("unsupported set operation %q", call.Function) } } func mapConditions(values []string, f func(string) Condition) []Condition { conds := make([]Condition, 0, len(values)) for _, v := range values { conds = append(conds, f(v)) } return conds } func combineConditions(op LogicalOperator, conds []Condition) Condition { result := conds[0] for _, c := range conds[1:] { result = &LogicalCondition{Operator: op, Left: result, Right: c} } return result } func distinctStrings(values []string) []string { seen := make(map[string]bool, len(values)) out := make([]string, 0, len(values)) for _, v := range values { if !seen[v] { seen[v] = true out = append(out, v) } } return out } // buildComprehensionCondition handles CEL comprehension expressions (exists, all, etc.). func buildComprehensionCondition(comp *exprv1.Expr_Comprehension, schema Schema) (Condition, error) { // Determine the comprehension kind by examining the loop initialization and step kind, err := detectComprehensionKind(comp) if err != nil { return nil, err } // Get the field being iterated over iterRangeIdent := comp.IterRange.GetIdentExpr() if iterRangeIdent == nil { return nil, errors.New("comprehension range must be a field identifier") } fieldName := iterRangeIdent.GetName() // Validate the field field, ok := schema.Field(fieldName) if !ok { return nil, errors.Errorf("unknown field %q in comprehension", fieldName) } if field.Kind != FieldKindJSONList { return nil, errors.Errorf("field %q does not support comprehension (must be a list)", fieldName) } // Extract the predicate from the loop step predicate, err := extractPredicate(comp, schema) if err != nil { return nil, err } return &ListComprehensionCondition{ Kind: kind, Field: fieldName, IterVar: comp.IterVar, Predicate: predicate, }, nil } // detectComprehensionKind determines if this is an exists() macro. // Only exists() is currently supported. func detectComprehensionKind(comp *exprv1.Expr_Comprehension) (ComprehensionKind, error) { // Check the accumulator initialization accuInit := comp.AccuInit.GetConstExpr() if accuInit == nil { return "", errors.New("comprehension accumulator must be initialized with a constant") } // exists() starts with false and uses OR (||) in loop step if !accuInit.GetBoolValue() { if step := comp.LoopStep.GetCallExpr(); step != nil && step.Function == "_||_" { return ComprehensionExists, nil } } // all() starts with true and uses AND (&&) in the loop step. if accuInit.GetBoolValue() { if step := comp.LoopStep.GetCallExpr(); step != nil && step.Function == "_&&_" { return ComprehensionAll, nil } } // exists_one() starts at int(0) and increments via a conditional (predicate ? // accu + 1 : accu) in the loop step. if _, isInt := accuInit.GetConstantKind().(*exprv1.Constant_Int64Value); isInt { if step := comp.LoopStep.GetCallExpr(); step != nil && step.Function == "_?_:_" { return ComprehensionExistsOne, nil } } return "", errors.New("unsupported comprehension type (supported: exists, all, exists_one)") } // extractPredicate extracts the predicate expression from the comprehension loop step. func extractPredicate(comp *exprv1.Expr_Comprehension, _ Schema) (PredicateExpr, error) { // The loop step is: @result || predicate(t) for exists // or: @result && predicate(t) for all step := comp.LoopStep.GetCallExpr() if step == nil { return nil, errors.New("comprehension loop step must be a call expression") } // exists/all: accu || predicate / accu && predicate -> predicate is arg[1]. // exists_one: predicate ? accu + 1 : accu -> predicate is arg[0]. var predicateExpr *exprv1.Expr if step.Function == "_?_:_" { if len(step.Args) != 3 { return nil, errors.New("exists_one loop step must have three arguments") } predicateExpr = step.Args[0] } else { if len(step.Args) != 2 { return nil, errors.New("comprehension loop step must have two arguments") } predicateExpr = step.Args[1] } predicateCall := predicateExpr.GetCallExpr() if predicateCall == nil { return nil, errors.New("comprehension predicate must be a function call") } // Handle different predicate functions switch predicateCall.Function { case "_==_": return buildEqualsPredicate(predicateCall, comp.IterVar) case "startsWith": return buildStartsWithPredicate(predicateCall, comp.IterVar) case "endsWith": return buildEndsWithPredicate(predicateCall, comp.IterVar) case "contains": return buildContainsPredicate(predicateCall, comp.IterVar) default: return nil, errors.Errorf(`unsupported predicate function %q in comprehension (supported: ==, startsWith, endsWith, contains)`, predicateCall.Function) } } // buildEqualsPredicate extracts the value from t == "value". func buildEqualsPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) { if len(call.Args) != 2 { return nil, errors.New("equality predicate expects exactly two arguments") } var constExpr *exprv1.Expr switch { case isIterVarExpr(call.Args[0], iterVar): constExpr = call.Args[1] case isIterVarExpr(call.Args[1], iterVar): constExpr = call.Args[0] default: return nil, errors.Errorf("equality predicate must compare against the iteration variable %q", iterVar) } value, err := getConstValue(constExpr) if err != nil { return nil, errors.Wrap(err, "equality argument must be a constant string") } valueStr, ok := value.(string) if !ok { return nil, errors.New("equality argument must be a string") } return &EqualsPredicate{Value: valueStr}, nil } func isIterVarExpr(expr *exprv1.Expr, iterVar string) bool { target := expr.GetIdentExpr() return target != nil && target.GetName() == iterVar } // buildStartsWithPredicate extracts the pattern from t.startsWith("prefix"). func buildStartsWithPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) { // Verify the target is the iteration variable if target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar { return nil, errors.Errorf("startsWith target must be the iteration variable %q", iterVar) } if len(call.Args) != 1 { return nil, errors.New("startsWith expects exactly one argument") } prefix, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "startsWith argument must be a constant string") } prefixStr, ok := prefix.(string) if !ok { return nil, errors.New("startsWith argument must be a string") } return &StartsWithPredicate{Prefix: prefixStr}, nil } // buildEndsWithPredicate extracts the pattern from t.endsWith("suffix"). func buildEndsWithPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) { if target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar { return nil, errors.Errorf("endsWith target must be the iteration variable %q", iterVar) } if len(call.Args) != 1 { return nil, errors.New("endsWith expects exactly one argument") } suffix, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "endsWith argument must be a constant string") } suffixStr, ok := suffix.(string) if !ok { return nil, errors.New("endsWith argument must be a string") } return &EndsWithPredicate{Suffix: suffixStr}, nil } // buildContainsPredicate extracts the pattern from t.contains("substring"). func buildContainsPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) { if target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar { return nil, errors.Errorf("contains target must be the iteration variable %q", iterVar) } if len(call.Args) != 1 { return nil, errors.New("contains expects exactly one argument") } substring, err := getConstValue(call.Args[0]) if err != nil { return nil, errors.Wrap(err, "contains argument must be a constant string") } substringStr, ok := substring.(string) if !ok { return nil, errors.New("contains argument must be a string") } return &ContainsPredicate{Substring: substringStr}, nil }