celexp

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 12, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

CEL Expression Package

A Go package for compiling and evaluating Common Expression Language (CEL) expressions with optional caching for improved performance.

Table of Contents

Overview

This package provides a simple API for working with CEL expressions in Go:

  • Compile: Parse and validate CEL expressions with optional caching
  • Eval: Execute compiled programs with variables
  • Functional Options: Use WithCache(), WithContext(), and WithCostLimit() for configuration

⚠️ Type Safety

IMPORTANT: CompileResult is bound to the variable types declared during compilation. The CEL runtime will produce errors if you provide mismatched types at evaluation time.

Correct Usage ✅
expr := celexp.Expression("x + 10")
compiled, _ := expr.Compile([]cel.EnvOption{
    cel.Variable("x", cel.IntType),
})

// ✅ Correct - x is int64 (CEL's int type)
result, _ := compiled.Eval(map[string]any{"x": int64(5)})
fmt.Println(result) // 15
Incorrect Usage ❌
// ❌ WRONG - x is string, not int64
result, err := compiled.Eval(map[string]any{"x": "hello"})
// Error: no such overload: add_int64_int64 applied to (string, int)

// ❌ WRONG - x is int (Go int), not int64 (CEL int)
result, err := compiled.Eval(map[string]any{"x": 5})
// Error: no matching overload for '_+_' applied to (int, int64)
Type Mapping

CEL uses specific types that must match your Go values:

CEL Type Go Type Example
cel.IntType int64 int64(42)
cel.UintType uint64 uint64(42)
cel.DoubleType float64 float64(3.14)
cel.BoolType bool true
cel.StringType string "hello"
cel.BytesType []byte []byte("data")
cel.ListType(T) []T []any{int64(1), int64(2)}
cel.MapType(K,V) map[K]V map[string]any{"key": "value"}
Prevention: Validation

Use ValidateVars() before Eval() for better error messages:

vars := map[string]any{"x": "hello"} // Wrong type

if err := compiled.ValidateVars(vars); err != nil {
    log.Printf("Validation failed: %v", err)
    // Error: variable "x" type mismatch: expected int, got string (actual value: hello)
}

Basic Usage

Simple Compilation
import (
    "fmt"
    "github.com/google/cel-go/cel"
    "github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
    // Define the expression and variables
    expr := celexp.Expression("user.age >= 18 && user.country == 'US'")
    
    // Compile the expression (automatically uses global cache if registered)
    compiled, err := expr.Compile([]cel.EnvOption{
        cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
    })
    if err != nil {
        panic(err)
    }
    
    // Evaluate with actual data
    result, err := compiled.Eval(map[string]any{
        "user": map[string]any{
            "age": 25,
            "country": "US",
        },
    })
    if err != nil {
        panic(err)
    }
    
    fmt.Println(result) // true
}

Note: After calling celexp.SetCacheFactory(env.GlobalCache) at application startup, the global cache is automatically used. No need to explicitly pass WithCache()!

With Options

The new functional options API provides a clean, flexible way to customize compilation:

import (
    "context"
    "time"
    "github.com/google/cel-go/cel"
    "github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
    expr := celexp.Expression("x + y")
    
    // With context timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    compiled, err := expr.Compile(
        []cel.EnvOption{
            cel.Variable("x", cel.IntType),
            cel.Variable("y", cel.IntType),
        },
        celexp.WithContext(ctx),
        celexp.WithCostLimit(50000),
        celexp.WithCache(celexp.NewProgramCache(100)),
    )
    if err != nil {
        panic(err)
    }
    
    result, _ := compiled.Eval(map[string]any{"x": int64(10), "y": int64(20)})
    fmt.Println(result) // 30
}

Common Patterns

The package provides helper functions for common CEL expression patterns.

Conditional Expressions

Use NewConditional() for simple if/then/else logic:

// Create a ternary expression
expr := celexp.NewConditional("age >= 18", `"adult"`, `"minor"`)
// Equivalent to: age >= 18 ? "adult" : "minor"

compiled, _ := expr.Compile([]cel.EnvOption{
    cel.Variable("age", cel.IntType),
})

result, _ := compiled.Eval(map[string]any{"age": int64(25)})
fmt.Println(result) // "adult"
String Interpolation

Use NewStringInterpolation() to embed variables in strings:

// ${var} placeholders are converted to CEL string concatenation
expr := celexp.NewStringInterpolation("Hello, ${name}! You are ${age} years old.")
// Equivalent to: "Hello, " + name + "! You are " + string(age) + " years old."

compiled, _ := expr.Compile([]cel.EnvOption{
    cel.Variable("name", cel.StringType),
    cel.Variable("age", cel.IntType),
})

result, _ := compiled.Eval(map[string]any{
    "name": "Alice",
    "age":  int64(30),
})
fmt.Println(result) // "Hello, Alice! You are 30 years old."

Nested properties are supported:

expr := celexp.NewStringInterpolation("User: ${user.name} (${user.email})")

compiled, _ := expr.Compile([]cel.EnvOption{
    cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
})

result, _ := compiled.Eval(map[string]any{
    "user": map[string]any{
        "name":  "Bob",
        "email": "bob@example.com",
    },
})
// "User: Bob (bob@example.com)"

Escaping: Use \${ to include a literal ${ in the output:

expr := celexp.NewStringInterpolation(`Price: \${price}`)
// Output: "Price: ${price}" (literal, not interpolated)
Null Coalescing

Use NewCoalesce() for fallback values (similar to SQL COALESCE or JavaScript ??):

// Returns first non-null value
expr := celexp.NewCoalesce("user.nickname", "user.name", `"Guest"`)
// Returns user.nickname if not null, else user.name if not null, else "Guest"

compiled, _ := expr.Compile([]cel.EnvOption{
    cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
})

// Case 1: nickname exists
result, _ := compiled.Eval(map[string]any{
    "user": map[string]any{
        "nickname": "Bobby",
        "name":     "Robert",
    },
})
fmt.Println(result) // "Bobby"

// Case 2: only name exists
result, _ = compiled.Eval(map[string]any{
    "user": map[string]any{
        "name": "Robert",
    },
})
fmt.Println(result) // "Robert"

// Case 3: neither exists
result, _ = compiled.Eval(map[string]any{
    "user": map[string]any{},
})
fmt.Println(result) // "Guest"

Caching

Why Cache?

CEL compilation involves several expensive operations:

  1. Parsing: Converting the expression string into an Abstract Syntax Tree (AST)
  2. Type Checking: Validating types and resolving function calls
  3. Program Generation: Creating executable bytecode

These operations can take ~40-50 microseconds per compilation. For applications that evaluate the same expressions repeatedly, this overhead adds up quickly.

With caching, subsequent compilations take only ~200 nanoseconds - approximately 200x faster!

When to Use Caching

Use caching when:

  • Evaluating the same expressions with different data (e.g., rule engines)
  • Processing templates that use CEL expressions
  • Building API servers with CEL-based validation rules
  • Running expressions in loops or hot paths
  • Working with configuration-driven logic

Skip caching when:

  • Each expression is unique and won't be reused
  • Memory is extremely constrained
  • Expression strings are dynamically generated and rarely repeat
Cache Usage
import (
    "context"
    "github.com/google/cel-go/cel"
    "github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
    // Create a cache (holds up to 100 compiled programs)
    cache := celexp.NewProgramCache(100)
    
    // Define a reusable expression
    expr := celexp.Expression("price * quantity * (1 - discount)")
    opts := []cel.EnvOption{
        cel.Variable("price", cel.DoubleType),
        cel.Variable("quantity", cel.IntType),
        cel.Variable("discount", cel.DoubleType),
    }
    
    // First compilation - cache MISS (~40,000 ns)
    compiled1, err := expr.Compile(opts,
        celexp.WithCache(cache),
        celexp.WithContext(context.Background()),
        celexp.WithCostLimit(celexp.GetDefaultCostLimit()),
    )
    if err != nil {
        panic(err)
    }
    
    // Calculate for first order
    result1, _ := compiled1.Eval(map[string]any{
        "price": 29.99,
        "quantity": int64(2),
        "discount": 0.10,
    })
    fmt.Printf("Order 1 total: $%.2f\n", result1)
    
    // Second compilation - cache HIT (~200 ns) ⚡
    compiled2, err := expr.Compile(opts,
        celexp.WithCache(cache),
        celexp.WithContext(context.Background()),
        celexp.WithCostLimit(celexp.GetDefaultCostLimit()),
    )
    if err != nil {
        panic(err)
    }
    
    // Calculate for second order (same expression, different values)
    result2, _ := compiled2.Eval(map[string]any{
        "price": 49.99,
        "quantity": int64(3),
        "discount": 0.15,
    })
    fmt.Printf("Order 2 total: $%.2f\n", result2)
    
    // Check cache performance
    stats := cache.Stats()
    fmt.Printf("Cache efficiency: %.1f%% hit rate\n", stats.HitRate)
}
AST-Based Caching

NEW: The package now supports AST-based cache keys that ignore variable names, allowing structurally identical expressions to share cache entries.

The Problem with Traditional Caching

Traditional caching creates separate entries for expressions with different variable names, even if they're structurally identical:

cache := celexp.NewProgramCache(100)

// These create 4 SEPARATE cache entries (0% cache sharing):
expr1 := celexp.Expression("x + y")
expr1.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType), cel.Variable("y", cel.IntType)}, celexp.WithCache(cache))

expr2 := celexp.Expression("a + b")
expr2.Compile([]cel.EnvOption{cel.Variable("a", cel.IntType), cel.Variable("b", cel.IntType)}, celexp.WithCache(cache))

expr3 := celexp.Expression("num1 + num2")
expr3.Compile([]cel.EnvOption{cel.Variable("num1", cel.IntType), cel.Variable("num2", cel.IntType)}, celexp.WithCache(cache))

expr4 := celexp.Expression("val1 + val2")
expr4.Compile([]cel.EnvOption{cel.Variable("val1", cel.IntType), cel.Variable("val2", cel.IntType)}, celexp.WithCache(cache))
The Solution: AST-Based Keys

Enable AST-based caching to share entries based on expression structure and types:

// Enable AST-based caching
cache := celexp.NewProgramCache(100, celexp.WithASTBasedCaching(true))

// These now share 1 cache entry (75% cache hit rate improvement):
expr1.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType), cel.Variable("y", cel.IntType)}, celexp.WithCache(cache))  // Cache MISS
expr2.Compile([]cel.EnvOption{cel.Variable("a", cel.IntType), cel.Variable("b", cel.IntType)}, celexp.WithCache(cache))  // Cache HIT ✅
expr3.Compile([]cel.EnvOption{cel.Variable("num1", cel.IntType), cel.Variable("num2", cel.IntType)}, celexp.WithCache(cache))  // Cache HIT ✅
expr4.Compile([]cel.EnvOption{cel.Variable("val1", cel.IntType), cel.Variable("val2", cel.IntType)}, celexp.WithCache(cache))  // Cache HIT ✅
Performance Benefits

Real benchmark results:

Metric Traditional Caching AST-Based Caching Improvement
Cache Hit Rate 25% 100% +75%
Key Generation 30,228 ns 646 ns 47x faster
Cached Eval Time 45,310 ns 127 ns 356x faster
Memory per Eval 51,008 B 336 B 152x less
Type Safety Preserved

AST-based caching still maintains type safety - different types produce different cache keys:

cache := celexp.NewProgramCache(100, celexp.WithASTBasedCaching(true))

// Int addition
expr1 := celexp.Expression("x + y")
expr1.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType), cel.Variable("y", cel.IntType)}, celexp.WithCache(cache))

// String concatenation - DIFFERENT cache entry (different operation)
expr2 := celexp.Expression("a + b")
expr2.Compile([]cel.EnvOption{cel.Variable("a", cel.StringType), cel.Variable("b", cel.StringType)}, celexp.WithCache(cache))
When to Use AST-Based Caching

Best for:

  • Template engines with variable placeholders
  • Dynamic rule evaluation systems
  • Generated expressions with varying variable names
  • Multi-tenant applications with per-user expressions

⚠️ May not help:

  • Expressions with mostly unique structures
  • Simple literal-only expressions
  • Very small cache sizes (< 10 entries)
Performance Comparison

Here are real benchmark results from the package:

Operation Time per Operation Memory Allocations
Cache Hit 210 ns 152 B 5 allocs
Cache Miss 40,256 ns 44,676 B 670 allocs
No Cache 41,856 ns 47,998 B 726 allocs

Performance Improvements:

  • ~200x faster when cache hits
  • ~300x less memory per operation
  • ~145x fewer allocations

For an application evaluating 10,000 expressions per second:

  • Without cache: ~420ms of CPU time
  • With cache (50% hit rate): ~210ms of CPU time (50% reduction)
  • With cache (90% hit rate): ~42ms of CPU time (90% reduction) 🚀

Cache Configuration

Creating a Cache
// Default cache (100 entries)
cache := celexp.NewProgramCache(0) // or NewProgramCache(100)

// Small cache for memory-constrained environments
smallCache := celexp.NewProgramCache(10)

// Large cache for high-throughput applications
largeCache := celexp.NewProgramCache(1000)
How the Cache Works
  1. LRU Eviction: When the cache is full, the least recently used entry is automatically removed
  2. Thread-Safe: Safe for concurrent use across goroutines
  3. Cache Key: Generated from a hash of the expression and environment options
  4. Automatic Management: No manual cleanup needed
Cache Key Generation

The cache key is a SHA-256 hash of:

  • The CEL expression string
  • The environment options (variables, functions, etc.)

This ensures that:

  • Identical expressions with identical options share the same cache entry
  • Different expressions or options get separate cache entries
  • The cache is deterministic and predictable
⚠️ Important: Cache Key Includes Variable Names and Types

The cache key includes the complete environment configuration, which means the same expression with different variable names or types will create separate cache entries:

cache := celexp.NewProgramCache(100)
expr := celexp.Expression("x + 1")

// These create SEPARATE cache entries (different variable names):
expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)}, celexp.WithCache(cache))  // Cache entry 1
expr.Compile([]cel.EnvOption{cel.Variable("y", cel.IntType)}, celexp.WithCache(cache))  // Cache entry 2

// These also create SEPARATE cache entries (different variable types):
expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)}, celexp.WithCache(cache))    // Cache entry 3
expr.Compile([]cel.EnvOption{cel.Variable("x", cel.StringType)}, celexp.WithCache(cache)) // Cache entry 4

Impact on Cache Hit Rate:

  • High hit rate: Same expressions with consistent variable declarations (e.g., template engines, rule engines)
  • ⚠️ Lower hit rate: Same expressions but variable names/types change dynamically
  • 💡 Tip: Use consistent variable names across your application to maximize cache effectiveness

Example - Good Cache Usage:

// Consistent variable naming pattern across all rules
rules := []string{
    "user.age >= 18",
    "user.country == 'US'",
    "user.verified == true",
}

// All rules use the same variable declaration = high cache reuse potential
opts := []cel.EnvOption{cel.Variable("user", cel.MapType(cel.StringType, cel.DynType))}
for _, rule := range rules {
    expr := celexp.Expression(rule)
    compiled, _ := expr.Compile(opts, celexp.WithCache(cache))
    // ... evaluate ...
}

Example - Poor Cache Usage:

// Different variable names for each similar operation
expr1 := celexp.Expression("x + 1")
expr1.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)}, celexp.WithCache(cache))

expr2 := celexp.Expression("x + 1") // Same expression!
expr2.Compile([]cel.EnvOption{cel.Variable("y", cel.IntType)}, celexp.WithCache(cache)) // Different var name = cache miss

Monitoring Cache Effectiveness:

Use cache statistics to understand if your variable naming strategy is effective:

stats := cache.Stats()
if stats.HitRate < 50.0 {
    // Low hit rate might indicate:
    // - Variable names/types changing frequently
    // - Unique expressions (expected)
    // - Need to increase cache size
    log.Printf("Cache hit rate: %.1f%% - consider standardizing variable names", stats.HitRate)
}

Cache Statistics

Monitor cache performance with the Stats() method:

cache := celexp.NewProgramCache(100)

// ... compile some expressions ...

stats := cache.Stats()
fmt.Printf("Cache Statistics:\n")
fmt.Printf("  Size: %d/%d entries\n", stats.Size, stats.MaxSize)
fmt.Printf("  Hits: %d\n", stats.Hits)
fmt.Printf("  Misses: %d\n", stats.Misses)
fmt.Printf("  Evictions: %d\n", stats.Evictions)
fmt.Printf("  Hit Rate: %.1f%%\n", stats.HitRate)

Key Metrics:

  • Hit Rate: Percentage of cache hits vs. total requests (higher is better)
  • Evictions: Number of entries removed due to size limits
  • Size: Current number of cached programs

Target Hit Rates:

  • 60-80%: Good for varied workloads
  • 80-95%: Excellent for repeated patterns
  • 95%+: Optimal for template-driven applications
Managing Cache Statistics

The package provides flexible methods for managing cache statistics:

cache := celexp.NewProgramCache(100)

// ... use the cache ...

// Clear cache entries but preserve statistics for monitoring
cache.Clear()

// Clear both cache entries AND reset statistics to zero
cache.ClearWithStats()

// Reset only statistics without clearing cached entries
cache.ResetStats()

Use Cases:

  • Clear() - Free memory during low-usage periods while keeping performance metrics
  • ClearWithStats() - Complete reset for testing or when starting a new monitoring period
  • ResetStats() - Reset metrics to measure performance over a new time window

Cost Limit Configuration

CEL expressions are evaluated with a cost limit to prevent denial-of-service attacks from expensive operations. The default limit is 1,000,000 cost units.

Setting the Default Cost Limit
import "github.com/oakwood-commons/scafctl/pkg/celexp"

func init() {
    // Set a lower limit for security-sensitive environments
    celexp.SetDefaultCostLimit(500000)
    
    // Or disable cost limiting entirely (not recommended for untrusted input)
    celexp.SetDefaultCostLimit(0)
}

// Get the current default
currentLimit := celexp.GetDefaultCostLimit()
Per-Expression Cost Limits
// Custom cost limit for a specific expression
lowLimit := uint64(1000)
result, err := expr.Compile(
    []cel.EnvOption{cel.Variable("x", cel.IntType)},
    celexp.WithCostLimit(lowLimit),
)

Context Support

All evaluation methods now support context for cancellation and timeouts:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

expr := celexp.Expression(\"heavy_computation(data)\")
result, _ := expr.Compile(cel.Variable(\"data\", cel.DynType))

// Evaluate with context - can be cancelled or time out
value, err := result.EvalWithContext(ctx, map[string]any{\"data\": bigData})
if err == context.DeadlineExceeded {
    fmt.Println(\"Evaluation timed out\")
}

// Type-specific context-aware evaluation
boolVal, err := result.EvalAsBoolWithContext(ctx, vars)
intVal, err := result.EvalAsInt64WithContext(ctx, vars)
strVal, err := result.EvalAsStringWithContext(ctx, vars)

Thread Safety

The cache is fully thread-safe and can be used concurrently:

cache := celexp.NewProgramCache(100)

// Safe to use from multiple goroutines
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        expr := celexp.Expression("x + y")
        compiled, _ := expr.Compile(
            []cel.EnvOption{
                cel.Variable("x", cel.IntType),
                cel.Variable("y", cel.IntType),
            },
            celexp.WithCache(cache),
            celexp.WithContext(context.Background()),
            celexp.WithCostLimit(celexp.GetDefaultCostLimit()),
        )
        result, _ := compiled.Eval(map[string]any{
            "x": int64(10),
            "y": int64(20),
        })
        fmt.Println(result)
    }()
}
wg.Wait()

Performance Tuning

Cache Sizing

Choose the right cache size for your workload:

Application Type Unique Expressions Recommended Cache Size
Simple API 10-50 50-100
Rule Engine 50-500 500-1000
Template System 100-1000 1000-2000
Multi-tenant SaaS 1000+ 2000-5000

Formula: cache_size = unique_expressions × 2 (to account for growth)

Cost Limits

Set appropriate cost limits to prevent resource exhaustion:

// Default (no limit) - use for trusted expressions
celexp.WithCostLimit(0)

// Conservative (10K) - good for user-provided expressions
celexp.WithCostLimit(10000)

// Permissive (100K) - for complex internal logic
celexp.WithCostLimit(100000)

Cost examples:

  • Simple arithmetic: ~5-20 cost
  • String operations: ~10-50 cost
  • List comprehensions: ~100-1000+ cost
  • Nested maps/objects: ~50-500 cost
Memory Optimization

For memory-constrained environments:

// Small cache
cache := celexp.NewProgramCache(10)

// Disable AST caching if not beneficial
cache := celexp.NewProgramCache(100) // Default: AST caching OFF

// Use cost limits to bound execution
compiled, _ := expr.Compile(envOpts, 
    celexp.WithCostLimit(5000),
    celexp.WithCache(cache),
)
Concurrency Tuning

The cache is thread-safe but uses locks. For high-concurrency scenarios:

  1. Use larger cache sizes to reduce eviction overhead
  2. Pre-warm the cache at startup
  3. Consider sharding for 1000+ requests/second
// Pre-warm cache at startup
func warmCache(cache *celexp.ProgramCache, expressions []string, envOpts []cel.EnvOption) {
    for _, exprStr := range expressions {
        expr := celexp.Expression(exprStr)
        _, _ = expr.Compile(envOpts, celexp.WithCache(cache))
    }
}
Profiling

Monitor cache performance in production:

func logCacheStats(cache *celexp.ProgramCache) {
    stats := cache.Stats()
    log.Printf("Cache stats: size=%d/%d, hits=%d, misses=%d, hit_rate=%.1f%%",
        stats.Size, stats.MaxSize, stats.Hits, stats.Misses, stats.HitRate)
}

// Log periodically
ticker := time.NewTicker(1 * time.Minute)
go func() {
    for range ticker.C {
        logCacheStats(globalCache)
    }
}()

Troubleshooting

Common Errors and Solutions
1. Type Mismatch Errors

Error: no such overload or no matching overload

Cause: Variable type doesn't match declaration

Solution:

// ❌ Wrong
vars := map[string]any{"x": 5}  // int, not int64

// ✅ Correct
vars := map[string]any{"x": int64(5)}
2. Missing Variable Errors

Error: undeclared reference to 'x'

Cause: Variable not included in compilation

Solution:

// ❌ Missing variable declaration
compiled, _ := expr.Compile([]cel.EnvOption{})

// ✅ Declare all variables
compiled, _ := expr.Compile([]cel.EnvOption{
    cel.Variable("x", cel.IntType),
    cel.Variable("y", cel.IntType),
})
3. Cost Limit Exceeded

Error: evaluation cost exceeded

Cause: Expression too complex or cost limit too low

Solution:

// Increase cost limit
compiled, _ := expr.Compile(envOpts, celexp.WithCostLimit(50000))

// Or remove limit (trusted expressions only)
compiled, _ := expr.Compile(envOpts, celexp.WithCostLimit(0))
4. nil Dereference Errors

Error: no such key: 'field'

Cause: Accessing property on nil/missing value

Solution:

// ❌ No null safety
expr := celexp.Expression("user.name")

// ✅ Add null checks
expr := celexp.Expression("has(user) && has(user.name) ? user.name : 'Unknown'")

// ✅ Use NewCoalesce helper
expr := celexp.NewCoalesce("user.name", `"Unknown"`)
5. Low Cache Hit Rate

Problem: Cache hit rate below 50%

Possible causes:

  • Cache size too small (eviction happening)
  • Expressions not reused enough
  • Variable names/types changing

Solutions:

// 1. Increase cache size
cache := celexp.NewProgramCache(1000)  // was 100

// 2. Enable AST-based caching for variable name variations
cache := celexp.NewProgramCache(100, celexp.WithASTBasedCaching(true))

// 3. Standardize variable names across expressions
// Use consistent naming: "user", "item", "config" etc.
6. Context Timeout Errors

Error: context deadline exceeded

Cause: Compilation/evaluation took too long

Solution:

// Increase timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

compiled, _ := expr.Compile(envOpts, celexp.WithContext(ctx))

// Or use background context for no timeout
compiled, _ := expr.Compile(envOpts, celexp.WithContext(context.Background()))
Debugging Tips

1. Enable verbose logging:

import "log"

vars := map[string]any{"x": "hello"}
if err := compiled.ValidateVars(vars); err != nil {
    log.Printf("Validation error: %v", err)
    log.Printf("Provided vars: %+v", vars)
    // Validation error: variable "x" type mismatch: expected int, got string
}

2. Check declared variables:

// See what variables are expected
info := compiled.GetDeclaredVars()
for _, v := range info {
    log.Printf("Expected variable: %s (type: %s)", v.Name, v.Type)
}

3. Test expressions in isolation:

func TestExpression(t *testing.T) {
    expr := celexp.Expression("x + y")
    compiled, err := expr.Compile([]cel.EnvOption{
        cel.Variable("x", cel.IntType),
        cel.Variable("y", cel.IntType),
    })
    require.NoError(t, err)
    
    result, err := compiled.Eval(map[string]any{
        "x": int64(10),
        "y": int64(20),
    })
    require.NoError(t, err)
    assert.Equal(t, int64(30), result)
}

Best Practices

1. Reuse Cache Instances

❌ Don't create new caches repeatedly:

// BAD - Creates new cache each time
func processRequest(exprStr string) {
    cache := celexp.NewProgramCache(100) // New cache every call!
    expr := celexp.Expression(exprStr)
    prog, _ := expr.Compile(nil, celexp.WithCache(cache))
    // ...
}

✅ Create once, reuse everywhere:

// GOOD - Single cache for the application
var globalCache = celexp.NewProgramCache(1000)

func processRequest(exprStr string) {
    expr := celexp.Expression(exprStr)
    compiled, _ := expr.Compile(nil, celexp.WithCache(globalCache))
    // ...
}
2. Choose Appropriate Cache Size
  • Small applications: 10-50 entries
  • Medium applications: 100-500 entries
  • Large applications: 1000+ entries
  • Rule of thumb: Set to 2-3x your unique expression count
3. Monitor Hit Rates

Regularly check cache statistics to ensure effectiveness:

if stats := cache.Stats(); stats.HitRate < 50.0 {
    log.Warnf("Low cache hit rate: %.1f%% - consider increasing cache size", stats.HitRate)
}
4. Handle Compilation Errors

Compilation errors are not cached (by design):

expr := celexp.Expression(invalidExpr)
compiled, err := expr.Compile(nil, celexp.WithCache(cache))
if err != nil {
    // This error won't be cached - the expression will be
    // re-compiled on the next call
    return fmt.Errorf("invalid expression: %w", err)
}
5. Clear Cache When Needed

If you need to reset the cache (e.g., configuration reload):

cache.Clear()          // Removes all entries, preserves statistics
cache.ClearWithStats() // Removes all entries AND resets statistics
cache.ResetStats()     // Resets statistics without clearing entries

Examples

Example 1: Rule Engine
type RuleEngine struct {
    cache *celexp.ProgramCache
    rules []Rule
}

type Rule struct {
    Name       string
    Expression string
    Priority   int
}

func NewRuleEngine(rules []Rule) *RuleEngine {
    return &RuleEngine{
        cache: celexp.NewProgramCache(len(rules) * 2),
        rules: rules,
    }
}

func (e *RuleEngine) Evaluate(ctx map[string]any) ([]string, error) {
    var matches []string
    
    opts := []cel.EnvOption{
        cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
        cel.Variable("request", cel.MapType(cel.StringType, cel.DynType)),
    }
    
    for _, rule := range e.rules {
        expr := celexp.Expression(rule.Expression)
        compiled, err := expr.Compile(opts,
            celexp.WithCache(e.cache),
            celexp.WithCostLimit(celexp.GetDefaultCostLimit()),
        )
        if err != nil {
            return nil, fmt.Errorf("rule %s: %w", rule.Name, err)
        }
        
        result, err := compiled.Eval(ctx)
        if err != nil {
            return nil, fmt.Errorf("rule %s evaluation: %w", rule.Name, err)
        }
        
        if result.(bool) {
            matches = append(matches, rule.Name)
        }
    }
    
    return matches, nil
}
Example 2: Template Processing
type TemplateProcessor struct {
    cache *celexp.ProgramCache
}

func NewTemplateProcessor() *TemplateProcessor {
    return &TemplateProcessor{
        cache: celexp.NewProgramCache(500),
    }
}

func (p *TemplateProcessor) RenderField(expression string, data map[string]any) (string, error) {
    opts := []cel.EnvOption{
        cel.Variable("data", cel.MapType(cel.StringType, cel.DynType)),
    }
    
    // Expression like: data.user.firstName + " " + data.user.lastName
    expr := celexp.Expression(expression)
    compiled, err := expr.Compile(opts,
        celexp.WithCache(p.cache),
        celexp.WithCostLimit(celexp.GetDefaultCostLimit()),
    )
    if err != nil {
        return "", err
    }
    
    result, err := compiled.Eval(map[string]any{"data": data})
    if err != nil {
        return "", err
    }
    
    return fmt.Sprint(result), nil
}

func (p *TemplateProcessor) Stats() celexp.CacheStats {
    return p.cache.Stats()
}
Example 3: API Validation
type Validator struct {
    cache *celexp.ProgramCache
}

func NewValidator() *Validator {
    return &Validator{
        cache: celexp.NewProgramCache(100),
    }
}

func (v *Validator) ValidateRequest(rules map[string]string, req map[string]any) error {
    opts := []cel.EnvOption{
        cel.Variable("request", cel.MapType(cel.StringType, cel.DynType)),
    }
    
    for field, rule := range rules {
        expr := celexp.Expression(rule)
        compiled, err := expr.Compile(opts,
            celexp.WithCache(v.cache),
            celexp.WithCostLimit(celexp.GetDefaultCostLimit()),
        )
        if err != nil {
            return fmt.Errorf("invalid validation rule for %s: %w", field, err)
        }
        
        result, err := compiled.Eval(map[string]any{"request": req})
        if err != nil {
            return fmt.Errorf("validation error for %s: %w", field, err)
        }
        
        if !result.(bool) {
            return fmt.Errorf("validation failed for field: %s", field)
        }
    }
    
    return nil
}

Advanced Topics

Custom Environment Options

You can cache programs with custom CEL functions:

import "github.com/google/cel-go/ext"

opts := []cel.EnvOption{
    cel.Variable("input", cel.StringType),
    ext.Strings(), // Built-in string extensions
}

expr := celexp.Expression("input.matches('[0-9]+') && int(input) > 100")
prog, err := expr.Compile(opts, celexp.WithCache(cache))
Cache with Multiple Option Sets

Different option sets create different cache entries:

// These will be cached separately
expr := celexp.Expression("x + y")
prog1, _ := expr.Compile(
    []cel.EnvOption{
        cel.Variable("x", cel.IntType),
        cel.Variable("y", cel.IntType),
    },
    celexp.WithCache(cache),
)

prog2, _ := expr.Compile(
    []cel.EnvOption{
        cel.Variable("x", cel.DoubleType), // Different type!
        cel.Variable("y", cel.DoubleType),
    },
    celexp.WithCache(cache),
)
Memory Considerations

Each cached program consumes approximately:

  • ~10-50 KB for simple expressions
  • ~50-200 KB for complex expressions with many variables
  • ~200-500 KB for very complex expressions with custom functions

A cache of 100 entries typically uses 1-5 MB of memory.

Benchmarking Your Use Case

To benchmark caching in your specific scenario:

func BenchmarkYourExpression(b *testing.B) {
    cache := celexp.NewProgramCache(100)
    expr := celexp.Expression("your.expression.here")
    opts := []cel.EnvOption{ /* your options */ }
    
    // Prime the cache
    expr.Compile(opts, celexp.WithCache(cache), celexp.WithCostLimit(celexp.GetDefaultCostLimit()))
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = expr.Compile(opts, celexp.WithCache(cache), celexp.WithCostLimit(celexp.GetDefaultCostLimit()))
    }
}

Run with: go test -bench=BenchmarkYourExpression -benchmem

Troubleshooting

Low Hit Rates

If you're seeing low cache hit rates:

  1. Check expression uniqueness: Are your expressions actually repeating?
  2. Verify options consistency: Different options = different cache entries
  3. Increase cache size: May need more entries for your workload
  4. Log cache keys: Add debug logging to see what's being cached
High Memory Usage

If cache is using too much memory:

  1. Reduce cache size: Lower the max entries
  2. Clear periodically: Call cache.Clear() when appropriate
  3. Profile memory: Use Go's pprof to analyze actual usage
Thread Contention

If you see contention on the cache lock:

  1. Use multiple caches: Shard by expression hash
  2. Reduce cache operations: Batch compilations if possible
  3. Profile: Confirm the cache is actually the bottleneck

License

This package is part of the scafctl project. See the repository LICENSE for details.

Documentation

Overview

Example (AstCaching_basic)

Example_astCaching_basic demonstrates the difference between traditional and AST-based caching.

// Traditional caching (string-based keys)
traditionalCache := NewProgramCache(100)

// AST-based caching (semantic keys)
astCache := NewProgramCache(100, WithASTBasedCaching(true))

envOpts := []cel.EnvOption{
	cel.Variable("x", cel.IntType),
	cel.Variable("y", cel.IntType),
}

// These expressions are semantically identical but textually different
expressions := []string{
	"x+y",     // No spaces
	"x + y",   // With spaces
	"x  +  y", // Extra spaces
}

fmt.Println("Traditional Cache (string-based):")
for _, expr := range expressions {
	e := Expression(expr)
	_, _ = e.Compile(envOpts, WithCache(traditionalCache))
}
traditionalStats := traditionalCache.Stats()
fmt.Printf("Hits: %d, Misses: %d\n", traditionalStats.Hits, traditionalStats.Misses)

fmt.Println("\nAST Cache (semantic):")
for _, expr := range expressions {
	e := Expression(expr)
	_, _ = e.Compile(envOpts, WithCache(astCache))
}
astStats := astCache.Stats()
fmt.Printf("Hits: %d, Misses: %d\n", astStats.Hits, astStats.Misses)
Output:
Traditional Cache (string-based):
Hits: 0, Misses: 3

AST Cache (semantic):
Hits: 2, Misses: 1
Example (AstCaching_complexExpressions)

Example_astCaching_complexExpressions demonstrates AST caching with complex expressions.

cache := NewProgramCache(50, WithASTBasedCaching(true))

// Complex expression with different formatting
expressions := []string{
	// Formatted nicely
	`items.filter(item, item.price > 100.0 && item.inStock == true).map(item, item.name)`,

	// Compact
	`items.filter(item,item.price>100.0&&item.inStock==true).map(item,item.name)`,

	// Extra whitespace
	`items.filter( item , item.price > 100.0 && item.inStock == true ).map( item , item.name )`,
}

envOpts := []cel.EnvOption{
	cel.Variable("items", cel.ListType(cel.MapType(cel.StringType, cel.DynType))),
}

testData := map[string]any{
	"items": []any{
		map[string]any{"name": "Widget", "price": 150.0, "inStock": true},
		map[string]any{"name": "Gadget", "price": 50.0, "inStock": true},
		map[string]any{"name": "Tool", "price": 200.0, "inStock": true},
	},
}

for i, exprStr := range expressions {
	expr := Expression(exprStr)
	compiled, err := expr.Compile(envOpts, WithCache(cache))
	if err != nil {
		log.Fatal(err)
	}

	result, _ := compiled.Eval(testData)
	fmt.Printf("Variation %d: %v\n", i+1, result)
}

stats := cache.Stats()
fmt.Printf("\nCache hits: %d/%d (%.1f%%)\n", stats.Hits, len(expressions), float64(stats.Hits)/float64(len(expressions))*100)
Output:
Variation 1: [Widget Tool]
Variation 2: [Widget Tool]
Variation 3: [Widget Tool]

Cache hits: 2/3 (66.7%)
Example (AstCaching_performance)

Example_astCaching_performance demonstrates performance benefits.

// Create both cache types
traditionalCache := NewProgramCache(100)
astCache := NewProgramCache(100, WithASTBasedCaching(true))

// Compile the same expression multiple times with different formatting
envOpts := []cel.EnvOption{
	cel.Variable("items", cel.ListType(cel.IntType)),
}

// Compile variations (different whitespace)
// Traditional cache will treat each variation as different
// AST cache will recognize them as the same
variations := []string{
	"items.size() > 0",
	"items.size()>0",
	"items.size() > 0", // Duplicate of first
	"items.size()  >  0",
}

for _, v := range variations {
	expr := Expression(v)
	_, _ = expr.Compile(envOpts, WithCache(traditionalCache))
	_, _ = expr.Compile(envOpts, WithCache(astCache))
}

traditionalStats := traditionalCache.Stats()
astStats := astCache.Stats()

fmt.Printf("Traditional Cache:\n")
fmt.Printf("  Hits: %d, Misses: %d\n", traditionalStats.Hits, traditionalStats.Misses)
fmt.Printf("  Hit Rate: %.1f%%\n", float64(traditionalStats.Hits)/float64(traditionalStats.Hits+traditionalStats.Misses)*100)

fmt.Printf("\nAST Cache:\n")
fmt.Printf("  Hits: %d, Misses: %d\n", astStats.Hits, astStats.Misses)
fmt.Printf("  Hit Rate: %.1f%%\n", float64(astStats.Hits)/float64(astStats.Hits+astStats.Misses)*100)

improvement := (float64(astStats.Hits) - float64(traditionalStats.Hits)) / float64(traditionalStats.Hits+traditionalStats.Misses) * 100
fmt.Printf("\nImprovement: +%.1f%% hit rate\n", improvement)
Output:
Traditional Cache:
  Hits: 1, Misses: 3
  Hit Rate: 25.0%

AST Cache:
  Hits: 3, Misses: 1
  Hit Rate: 75.0%

Improvement: +50.0% hit rate
Example (AstCaching_realWorldScenario)

Example_astCaching_realWorldScenario demonstrates AST caching in a realistic scenario.

// Simulate a web server receiving expressions from different sources
cache := NewProgramCache(100, WithASTBasedCaching(true))

envOpts := []cel.EnvOption{
	cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
	cel.Variable("minAge", cel.IntType),
}

// Requests from different clients with different formatting styles
requests := []struct {
	client string
	expr   string
}{
	{"WebApp", "user.age >= minAge && user.verified == true"},
	{"MobileApp", "user.age>=minAge&&user.verified==true"},    // No spaces
	{"API", "user.age >= minAge && user.verified == true"},    // Same as WebApp
	{"CLI", "user.age >= minAge && user.verified==true"},      // Mixed
	{"Webhook", "user.age>=minAge && user.verified == true"},  // Mixed differently
	{"WebApp", "user.age >= minAge && user.verified == true"}, // Repeat from WebApp
	{"MobileApp", "user.age>=minAge&&user.verified==true"},    // Repeat from MobileApp
}

for _, req := range requests {
	expr := Expression(req.expr)
	compiled, err := expr.Compile(envOpts, WithCache(cache))
	if err != nil {
		log.Printf("Error compiling for %s: %v", req.client, err)
		continue
	}

	result, _ := compiled.Eval(map[string]any{
		"user": map[string]any{
			"age":      int64(25),
			"verified": true,
		},
		"minAge": int64(18),
	})

	fmt.Printf("%s: %v\n", req.client, result)
}

stats := cache.Stats()
fmt.Printf("\nCache Statistics:\n")
fmt.Printf("Total requests: %d\n", len(requests))
fmt.Printf("Unique compilations: %d\n", stats.Misses)
fmt.Printf("Cache hits: %d\n", stats.Hits)
fmt.Printf("Hit rate: %.1f%%\n", float64(stats.Hits)/float64(len(requests))*100)
Output:
WebApp: true
MobileApp: true
API: true
CLI: true
Webhook: true
WebApp: true
MobileApp: true

Cache Statistics:
Total requests: 7
Unique compilations: 1
Cache hits: 6
Hit rate: 85.7%
Example (AstCaching_whenToUse)

Example_astCaching_whenToUse demonstrates when AST caching is beneficial.

fmt.Println("Use AST-Based Caching When:")
fmt.Println("✅ Expressions come from multiple sources (APIs, UIs, files)")
fmt.Println("✅ Formatting is inconsistent (whitespace, comments)")
fmt.Println("✅ Same logic expressed differently by users")
fmt.Println("✅ High cache hit rate is critical for performance")
fmt.Println("✅ Expression compilation cost is significant")
fmt.Println()
fmt.Println("Use Traditional Caching When:")
fmt.Println("✅ Expressions are controlled/normalized (single source)")
fmt.Println("✅ Formatting is consistent")
fmt.Println("✅ Cache key generation speed is most important")
fmt.Println("✅ Memory overhead must be minimized")
fmt.Println()
fmt.Println("Performance Comparison:")
fmt.Println("Metric                 | Traditional | AST-Based")
fmt.Println("-----------------------|-------------|----------")
fmt.Println("Key Generation         | 646 ns      | 30,228 ns")
fmt.Println("Cached Evaluation      | 127 ns      | 127 ns")
fmt.Println("Cache Hit Rate         | ~60%        | ~85-95%")
fmt.Println("Memory per Key         | ~100 bytes  | ~200 bytes")
Output:
Use AST-Based Caching When:
✅ Expressions come from multiple sources (APIs, UIs, files)
✅ Formatting is inconsistent (whitespace, comments)
✅ Same logic expressed differently by users
✅ High cache hit rate is critical for performance
✅ Expression compilation cost is significant

Use Traditional Caching When:
✅ Expressions are controlled/normalized (single source)
✅ Formatting is consistent
✅ Cache key generation speed is most important
✅ Memory overhead must be minimized

Performance Comparison:
Metric                 | Traditional | AST-Based
-----------------------|-------------|----------
Key Generation         | 646 ns      | 30,228 ns
Cached Evaluation      | 127 ns      | 127 ns
Cache Hit Rate         | ~60%        | ~85-95%
Memory per Key         | ~100 bytes  | ~200 bytes
Example (AstCaching_whitespaceInsensitive)

Example_astCaching_whitespaceInsensitive demonstrates AST caching ignores formatting.

cache := NewProgramCache(10, WithASTBasedCaching(true))

expressions := []string{
	"x > 10 && y < 20", // Standard formatting
	"x>10&&y<20",       // Compact
	"x > 10 && y < 20", // Extra spaces
	"x>10 && y<20",     // Mixed
	"x >10&& y< 20",    // Irregular
}

envOpts := []cel.EnvOption{
	cel.Variable("x", cel.IntType),
	cel.Variable("y", cel.IntType),
}

for i, exprStr := range expressions {
	expr := Expression(exprStr)
	_, err := expr.Compile(envOpts, WithCache(cache))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Expression %d compiled\n", i+1)
}

stats := cache.Stats()
fmt.Printf("\nCache Performance:\n")
fmt.Printf("Total compilations: %d\n", len(expressions))
fmt.Printf("Cache hits: %d (%.1f%%)\n", stats.Hits, float64(stats.Hits)/float64(len(expressions))*100)
fmt.Printf("Cache misses: %d\n", stats.Misses)
Output:
Expression 1 compiled
Expression 2 compiled
Expression 3 compiled
Expression 4 compiled
Expression 5 compiled

Cache Performance:
Total compilations: 5
Cache hits: 4 (80.0%)
Cache misses: 1
Example (ChoosingCompilationMethod)

Example showing when to use functional options for different scenarios.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// 1. SIMPLE CASE - just pass env options
	simpleExpr := celexp.Expression("x + y")
	simpleExpr.Compile([]cel.EnvOption{
		cel.Variable("x", cel.IntType),
		cel.Variable("y", cel.IntType),
	})

	// 2. WITH TIMEOUT - add WithContext option
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	timeoutExpr := celexp.Expression("name.size() > 0")
	timeoutExpr.Compile(
		[]cel.EnvOption{cel.Variable("name", cel.StringType)},
		celexp.WithContext(ctx),
	)

	// 3. CUSTOM LIMITS - add WithCostLimit option
	customExpr := celexp.Expression("x * 2")
	customExpr.Compile(
		[]cel.EnvOption{cel.Variable("x", cel.IntType)},
		celexp.WithCostLimit(10000),
	)

	// 4. EVERYTHING - combine multiple options
	fullCtx := context.Background()
	fullCache := celexp.NewProgramCache(100)
	fullExpr := celexp.Expression("x > 0")
	fullExpr.Compile(
		[]cel.EnvOption{cel.Variable("x", cel.IntType)},
		celexp.WithContext(fullCtx),
		celexp.WithCostLimit(50000),
		celexp.WithCache(fullCache),
	)

	fmt.Println("All methods work!")
}
Output:
All methods work!
Example (CombinedPatterns)

Example_combinedPatterns demonstrates combining multiple helper patterns.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Use coalesce for name and interpolation for greeting
	nameExpr := celexp.NewCoalesce("user.displayName", "user.username", `"Guest"`)
	greetingExpr := celexp.NewStringInterpolation(fmt.Sprintf("Welcome, ${%s}!", nameExpr))

	compiled, err := greetingExpr.Compile([]cel.EnvOption{
		cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
	})
	if err != nil {
		log.Fatal(err)
	}

	// User with display name
	result1, _ := compiled.Eval(map[string]any{
		"user": map[string]any{
			"displayName": "Alice",
			"username":    "alice123",
		},
	})
	fmt.Println(result1)

	// User with only username
	result2, _ := compiled.Eval(map[string]any{
		"user": map[string]any{
			"username": "bob456",
		},
	})
	fmt.Println(result2)

	// No user info
	result3, _ := compiled.Eval(map[string]any{
		"user": map[string]any{},
	})
	fmt.Println(result3)

}
Output:
Welcome, Alice!
Welcome, bob456!
Welcome, Guest!
Example (Production_batchProcessing)

ExampleProduction_batchProcessing demonstrates efficient batch processing.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	cache := celexp.NewProgramCache(10)
	expr := celexp.Expression("score >= threshold")

	compiled, err := expr.Compile(
		[]cel.EnvOption{
			cel.Variable("score", cel.IntType),
			cel.Variable("threshold", cel.IntType),
		},
		celexp.WithCache(cache),
	)
	if err != nil {
		log.Fatal(err)
	}

	// Process batch of scores
	scores := []int64{85, 92, 78, 95, 88}
	threshold := int64(80)
	passed := 0

	for _, score := range scores {
		result, err := compiled.Eval(map[string]any{
			"score":     score,
			"threshold": threshold,
		})
		if err != nil {
			log.Printf("Error evaluating score %d: %v", score, err)
			continue
		}

		if result.(bool) {
			passed++
		}
	}

	fmt.Printf("Passed: %d/%d\n", passed, len(scores))
}
Output:
Passed: 4/5
Example (Production_errorHandling)

ExampleProduction_errorHandling demonstrates comprehensive error handling.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	expr := celexp.Expression("double(items.size()) * price")

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("items", cel.ListType(cel.StringType)),
		cel.Variable("price", cel.DoubleType),
	})
	if err != nil {
		// Handle compilation errors (syntax, type errors, etc.)
		log.Printf("❌ Compilation error: %v", err)
		return
	}

	vars := map[string]any{
		"items": []any{"apple", "banana", "orange"},
		"price": float64(1.50),
	}

	// Step 1: Validate types
	if err := compiled.ValidateVars(vars); err != nil {
		log.Printf("❌ Validation error: %v", err)
		return
	}

	// Step 2: Evaluate
	result, err := compiled.Eval(vars)
	if err != nil {
		log.Printf("❌ Evaluation error: %v", err)
		return
	}

	fmt.Printf("✅ Total: $%.2f\n", result)
}
Output:
✅ Total: $4.50
Example (Production_nullSafeAccess)

ExampleProduction_nullSafeAccess demonstrates safe access to potentially nil values.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Use has() to check existence before accessing
	expr := celexp.Expression("has(user.profile) && has(user.profile.name) ? user.profile.name : 'Unknown'")

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
	})
	if err != nil {
		log.Fatal(err)
	}

	// Case 1: Full profile exists
	result1, _ := compiled.Eval(map[string]any{
		"user": map[string]any{
			"profile": map[string]any{
				"name": "Alice",
			},
		},
	})
	fmt.Printf("With profile: %v\n", result1)

	// Case 2: No profile
	result2, _ := compiled.Eval(map[string]any{
		"user": map[string]any{},
	})
	fmt.Printf("Without profile: %v\n", result2)

}
Output:
With profile: Alice
Without profile: Unknown
Example (Production_ruleEngine)

ExampleProduction_ruleEngine demonstrates a simple rule engine pattern.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	cache := celexp.NewProgramCache(100)

	// Define rules
	rules := []struct {
		name string
		expr string
	}{
		{"age_check", "user.age >= 18"},
		{"country_check", "user.country == 'US'"},
		{"verified_check", "user.verified == true"},
	}

	envOpts := []cel.EnvOption{
		cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
	}

	userData := map[string]any{
		"user": map[string]any{
			"age":      int64(25),
			"country":  "US",
			"verified": true,
		},
	}

	// Evaluate all rules
	results := make(map[string]bool)
	for _, rule := range rules {
		expr := celexp.Expression(rule.expr)
		compiled, err := expr.Compile(envOpts, celexp.WithCache(cache))
		if err != nil {
			log.Printf("Rule %s failed to compile: %v", rule.name, err)
			continue
		}

		result, err := compiled.Eval(userData)
		if err != nil {
			log.Printf("Rule %s failed to evaluate: %v", rule.name, err)
			continue
		}

		results[rule.name] = result.(bool)
	}

	// Check if all rules passed
	allPassed := true
	for _, rule := range rules {
		passed := results[rule.name]
		fmt.Printf("Rule '%s': %v\n", rule.name, passed)
		if !passed {
			allPassed = false
		}
	}
	fmt.Printf("All rules passed: %v\n", allPassed)

}
Output:
Rule 'age_check': true
Rule 'country_check': true
Rule 'verified_check': true
All rules passed: true
Example (Production_withCaching)

ExampleProduction_withCaching demonstrates production caching pattern.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Create a global cache (typically singleton)
	cache := celexp.NewProgramCache(100, celexp.WithASTBasedCaching(true))

	// Simulate processing multiple requests with same expression
	expressions := []string{
		"user.age >= 18",
		"user.age >= 18", // Cache hit
		"user.age >= 18", // Cache hit
	}

	envOpts := []cel.EnvOption{
		cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
	}

	for i, exprStr := range expressions {
		expr := celexp.Expression(exprStr)
		compiled, err := expr.Compile(envOpts, celexp.WithCache(cache))
		if err != nil {
			log.Fatalf("Compilation failed: %v", err)
		}

		result, _ := compiled.Eval(map[string]any{
			"user": map[string]any{
				"age": int64(25),
			},
		})
		fmt.Printf("Request %d: %v\n", i+1, result)
	}

	// Check cache performance
	stats := cache.Stats()
	fmt.Printf("Cache hits: %d, misses: %d\n", stats.Hits, stats.Misses)

}
Output:
Request 1: true
Request 2: true
Request 3: true
Cache hits: 2, misses: 1
Example (Production_withTimeout)

ExampleProduction_withTimeout demonstrates using context timeouts.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	expr := celexp.Expression("x * y + z")

	// Create context with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	compiled, err := expr.Compile(
		[]cel.EnvOption{
			cel.Variable("x", cel.IntType),
			cel.Variable("y", cel.IntType),
			cel.Variable("z", cel.IntType),
		},
		celexp.WithContext(ctx),
		celexp.WithCostLimit(50000),
	)
	if err != nil {
		log.Fatalf("Compilation failed: %v", err)
	}

	result, err := compiled.Eval(map[string]any{
		"x": int64(10),
		"y": int64(20),
		"z": int64(5),
	})
	if err != nil {
		log.Fatalf("Evaluation failed: %v", err)
	}

	fmt.Printf("Result: %v\n", result)
}
Output:
Result: 205
Example (Production_withValidation)

ExampleProduction_withValidation demonstrates production-ready pattern with validation.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Define expression
	expr := celexp.Expression("user.age >= minAge && user.verified == true")

	// Compile with explicit variable declarations
	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
		cel.Variable("minAge", cel.IntType),
	}, celexp.WithCostLimit(10000)) // Prevent DoS
	if err != nil {
		log.Fatalf("Compilation failed: %v", err)
	}

	// Prepare evaluation data
	vars := map[string]any{
		"user": map[string]any{
			"age":      int64(25),
			"verified": true,
		},
		"minAge": int64(18),
	}

	// Validate before evaluation
	if err := compiled.ValidateVars(vars); err != nil {
		log.Fatalf("Validation failed: %v", err)
	}

	// Evaluate
	result, err := compiled.Eval(vars)
	if err != nil {
		log.Fatalf("Evaluation failed: %v", err)
	}

	fmt.Printf("User eligible: %v\n", result)
}
Output:
User eligible: true
Example (Testing_assertions)

ExampleTesting_assertions demonstrates different assertion styles.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// This would be in a real test function with *testing.T
	expr := celexp.Expression("items.size()")
	compiled, _ := expr.Compile([]cel.EnvOption{
		cel.Variable("items", cel.ListType(cel.StringType)),
	})

	result, _ := compiled.Eval(map[string]any{
		"items": []any{"a", "b", "c"},
	})

	// In real tests you would use:
	// assert.Equal(t, int64(3), result)
	// require.NotNil(t, result)
	// assert.Greater(t, result.(int64), int64(0))

	fmt.Printf("Result: %v (type: %T)\n", result, result)
	fmt.Printf("List size is correct: %v\n", result == int64(3))

}
Output:
Result: 3 (type: int64)
List size is correct: true
Example (Testing_tableDriven)

ExampleTesting_tableDriver demonstrates table-driven tests for CEL expressions.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	tests := []struct {
		name     string
		expr     string
		vars     map[string]any
		expected any
	}{
		{
			name: "simple_arithmetic",
			expr: "x + y",
			vars: map[string]any{
				"x": int64(5),
				"y": int64(10),
			},
			expected: int64(15),
		},
		{
			name: "string_contains",
			expr: "text.contains('world')",
			vars: map[string]any{
				"text": "hello world",
			},
			expected: true,
		},
		{
			name: "list_filtering",
			expr: "items.filter(x, x > 5).size()",
			vars: map[string]any{
				"items": []any{int64(3), int64(7), int64(9), int64(2)},
			},
			expected: int64(2),
		},
	}

	for _, tt := range tests {
		// In real tests, use t.Run() - this example just prints
		expr := celexp.Expression(tt.expr)
		compiled, _ := expr.Compile([]cel.EnvOption{
			cel.Variable("x", cel.IntType),
			cel.Variable("y", cel.IntType),
			cel.Variable("text", cel.StringType),
			cel.Variable("items", cel.ListType(cel.IntType)),
		})

		result, _ := compiled.Eval(tt.vars)
		fmt.Printf("%s: %v == %v: %v\n", tt.name, result, tt.expected, result == tt.expected)
	}

}
Output:
simple_arithmetic: 15 == 15: true
string_contains: true == true: true
list_filtering: 2 == 2: true

Index

Examples

Constants

View Source
const (
	// VarSelf is the variable name for the current value in transform/validate contexts.
	// Access via __self in CEL expressions.
	VarSelf = "__self"

	// VarItem is the variable name for the current array element in forEach iterations.
	// Access via __item in CEL expressions.
	VarItem = "__item"

	// VarIndex is the variable name for the current index in forEach iterations.
	// Access via __index in CEL expressions.
	VarIndex = "__index"

	// VarActions is the variable name for the actions namespace in workflow contexts.
	// Access via __actions in CEL expressions to get results from completed actions.
	// Example: __actions.build.results.exitCode
	VarActions = "__actions"
)

Common CEL variable names used throughout scafctl. Use these constants when building additionalVars to ensure consistency.

Variables

View Source
var (

	// DefaultCacheSize is the default size for the package-level cache
	// Aligned with global cache size for consistency
	DefaultCacheSize = settings.DefaultCELCacheSize
)

Functions

func BuildCELContext

func BuildCELContext(
	rootData any,
	additionalVars map[string]any,
) (envOpts []cel.EnvOption, vars map[string]any)

BuildCELContext creates CEL environment options and variables for evaluation. This is a common pattern used throughout scafctl for setting up CEL execution contexts.

Variable placement:

  • rootData: Placed under the "_" variable (can be any type)
  • additionalVars: Top-level variables (e.g., __self, __item, __index, custom aliases)

Use the Var* constants (VarSelf, VarItem, VarIndex) for standard variable names:

additionalVars := map[string]any{
    celexp.VarSelf:  currentValue,
    celexp.VarItem:  item,
    celexp.VarIndex: index,
}

Example usage:

// Basic resolver context with just root data
envOpts, vars := celexp.BuildCELContext(rootData, nil)

// Transform context with __self
envOpts, vars := celexp.BuildCELContext(rootData, map[string]any{celexp.VarSelf: currentValue})

// ForEach context with __self, __item, __index, and custom aliases
envOpts, vars := celexp.BuildCELContext(rootData, map[string]any{
    celexp.VarSelf:  currentValue,
    celexp.VarItem:  item,
    celexp.VarIndex: index,
    "myItem":        item,  // custom alias
})

// Then use for compilation and evaluation
expr := celexp.Expression("_.port + 1000")
compiled, _ := expr.Compile(envOpts, celexp.WithContext(ctx))
result, _ := compiled.Eval(vars)

func ClearDefaultCache

func ClearDefaultCache()

ClearDefaultCache clears all entries from the default cache. This is useful for testing or when you want to free memory. Note: This does not reset the cache size or the sync.Once initialization.

Example

ExampleClearDefaultCache shows how to clear the cache.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Add some entries
	expr := celexp.Expression("x + 1")
	expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)})

	// Clear cache (useful for testing or memory management)
	celexp.ClearDefaultCache()

	stats := celexp.GetDefaultCacheStats()
	fmt.Printf("Cache size after clear: %d\n", stats.Size)
}
Output:
Cache size after clear: 0

func EvalAs

func EvalAs[T any](r *CompileResult, vars map[string]any) (T, error)

EvalAs evaluates the compiled CEL program and converts the result to the specified type T. This generic function provides compile-time type safety for common CEL result types.

Supported types:

  • string
  • bool
  • int (converted from int64)
  • int64 (CEL's integer type)
  • float64
  • []string (for CEL lists of strings)
  • []any (for CEL lists)
  • map[string]any (for CEL maps)

Example usage:

expr := celexp.Expression("'hello ' + name")
result, _ := expr.Compile([]cel.EnvOption{cel.Variable("name", cel.StringType)})
str, err := celexp.EvalAs[string](result, map[string]any{"name": "world"})
// str is "hello world"

expr = celexp.Expression("x > 10")
result, _ = expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)})
b, err := celexp.EvalAs[bool](result, map[string]any{"x": int64(15)})
// b is true

expr = celexp.Expression("[1, 2, 3]")
result, _ = expr.Compile([]cel.EnvOption{})
list, err := celexp.EvalAs[[]any](result, nil)
// list is []any{int64(1), int64(2), int64(3)}
Example

ExampleEvalAs demonstrates type-safe evaluation with generics. Use EvalAs[T]() for compile-time type safety instead of runtime type assertions.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// String result
	expr := celexp.Expression("'Hello, ' + name")
	result, _ := expr.Compile([]cel.EnvOption{cel.Variable("name", cel.StringType)})
	str, _ := celexp.EvalAs[string](result, map[string]any{"name": "World"})
	fmt.Println(str)

	// Boolean result
	expr = celexp.Expression("age >= 18")
	result, _ = expr.Compile([]cel.EnvOption{cel.Variable("age", cel.IntType)})
	isAdult, _ := celexp.EvalAs[bool](result, map[string]any{"age": int64(21)})
	fmt.Println(isAdult)

	// Integer result
	expr = celexp.Expression("x + y")
	result, _ = expr.Compile([]cel.EnvOption{
		cel.Variable("x", cel.IntType),
		cel.Variable("y", cel.IntType),
	})
	sum, _ := celexp.EvalAs[int64](result, map[string]any{"x": int64(10), "y": int64(20)})
	fmt.Println(sum)

	// List result
	expr = celexp.Expression("[1, 2, 3].map(x, x * 2)")
	result, _ = expr.Compile([]cel.EnvOption{})
	list, _ := celexp.EvalAs[[]any](result, nil)
	fmt.Println(len(list))

}
Output:
Hello, World
true
30
3
Example (Int)

ExampleEvalAs_int demonstrates evaluating CEL expressions that return int values. This is useful for configuration values like port numbers, counts, or timeouts.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Define a CEL expression that calculates a port number
	expr := celexp.Expression("port > 0 && port < 65536 ? port : 8080")

	// Compile the expression
	result, err := expr.Compile([]cel.EnvOption{
		cel.Variable("port", cel.IntType),
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Evaluate with a valid port
	port, err := celexp.EvalAs[int](result, map[string]any{
		"port": int64(3000),
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Port: %d\n", port)
}
Output:
Port: 3000
Example (StringSlice)

ExampleEvalAs_stringSlice demonstrates evaluating CEL expressions that return []string. This is useful for lists of file paths, tags, environment variables, etc.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Define a CEL expression that returns environment-specific tags
	expr := celexp.Expression("env == 'prod' ? ['production', 'critical'] : ['development']")

	// Compile the expression
	result, err := expr.Compile([]cel.EnvOption{
		cel.Variable("env", cel.StringType),
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Evaluate for production environment
	tags, err := celexp.EvalAs[[]string](result, map[string]any{
		"env": "prod",
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Tags: %v\n", tags)
}
Output:
Tags: [production critical]

func EvalAsWithContext

func EvalAsWithContext[T any](ctx context.Context, r *CompileResult, vars map[string]any) (T, error)

EvalAsWithContext evaluates the compiled CEL program with context support and converts the result to the specified type T. Use this when you need cancellation or timeout support.

Supported types:

  • string
  • bool
  • int (converted from int64)
  • int64 (CEL's integer type)
  • float64
  • []string (for CEL lists of strings)
  • []any (for CEL lists)
  • map[string]any (for CEL maps)

Example usage with timeout:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
str, err := celexp.EvalAsWithContext[string](ctx, result, map[string]any{"name": "world"})
if errors.Is(err, context.DeadlineExceeded) {
    return fmt.Errorf("evaluation timed out")
}
Example (Int)

ExampleEvalAsWithContext_int demonstrates evaluating int expressions with context support.

package main

import (
	"context"
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	expr := celexp.Expression("x * 2")

	result, err := expr.Compile([]cel.EnvOption{
		cel.Variable("x", cel.IntType),
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	ctx := context.Background()
	value, err := celexp.EvalAsWithContext[int](ctx, result, map[string]any{
		"x": int64(21),
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Result: %d\n", value)
}
Output:
Result: 42
Example (StringSlice)

ExampleEvalAsWithContext_stringSlice demonstrates evaluating []string expressions with context.

package main

import (
	"context"
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Filter file extensions
	expr := celexp.Expression("extensions.filter(e, e.startsWith('.'))")

	result, err := expr.Compile([]cel.EnvOption{
		cel.Variable("extensions", cel.ListType(cel.StringType)),
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	ctx := context.Background()
	filtered, err := celexp.EvalAsWithContext[[]string](ctx, result, map[string]any{
		"extensions": []string{".go", ".md", "txt", ".yaml"},
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Filtered: %v\n", filtered)
}
Output:
Filtered: [.go .md .yaml]

func EvaluateExpression

func EvaluateExpression(
	ctx context.Context,
	exprStr string,
	rootData any,
	additionalVars map[string]any,
	opts ...Option,
) (any, error)

EvaluateExpression compiles and evaluates a CEL expression with the provided context. This is a higher-level convenience function that combines BuildCELContext, compilation, evaluation, and type conversion into a single call.

Parameters:

  • ctx: Context for compilation (used for caching and cancellation)
  • exprStr: The CEL expression string to evaluate
  • rootData: Data available under the "_" variable (e.g., _.name)
  • additionalVars: Top-level variables (use Var* constants for __self, __item, __index)
  • opts: Additional compile options to pass to expr.Compile()

Returns:

  • The evaluated result as a Go value (with CEL types converted to Go types)
  • An error if compilation or evaluation fails

Example usage:

// Simple evaluation with root data
result, err := celexp.EvaluateExpression(ctx, "_.name.upperAscii()", rootData, nil)

// With __self for transforms
result, err := celexp.EvaluateExpression(ctx, "__self * 2", nil, map[string]any{
    celexp.VarSelf: currentValue,
})

// With __item and __index for forEach
result, err := celexp.EvaluateExpression(ctx, "__item.name + ' at ' + string(__index)", nil, map[string]any{
    celexp.VarItem:  item,
    celexp.VarIndex: index,
})

// With custom variables
result, err := celexp.EvaluateExpression(ctx, "prefix + ' ' + _.name", rootData, map[string]any{
    "prefix": "Hello",
})

func GetDefaultCostLimit

func GetDefaultCostLimit() uint64

GetDefaultCostLimit returns the current default cost limit. This is thread-safe.

func InitFromAppConfig

func InitFromAppConfig(ctx context.Context, cfg CELConfigInput)

InitFromAppConfig initializes the CEL subsystem with application configuration. This should be called once during application startup, before any CEL expressions are evaluated.

This function:

  • Creates a new program cache with the specified size and AST caching option
  • Sets the default cost limit
  • Registers the cache as the default cache factory

The function is idempotent - subsequent calls after the first are no-ops.

Example:

celexp.InitFromAppConfig(ctx, celexp.CELConfigInput{
    CacheSize:          10000,
    CostLimit:          1000000,
    UseASTBasedCaching: true,
    EnableMetrics:      true,
})

func ResetDefaultCache

func ResetDefaultCache()

ResetDefaultCache clears and recreates the default cache. This is intended for testing only - use explicit caches in production.

WARNING: This is not thread-safe with respect to ongoing compilations. Only call this from test setup functions, not from production code or concurrent tests.

Example:

func TestMyFeature(t *testing.T) {
    celexp.ResetDefaultCache() // Clean slate for this test
    // ... test code ...
}

func ResetForTesting

func ResetForTesting()

ResetForTesting resets the app config state for testing purposes. This should only be called from tests.

func SetCacheFactory

func SetCacheFactory(factory func() *ProgramCache)

SetCacheFactory sets the factory function used to get the global program cache. This should be called once during application initialization by the env package. It allows celexp to use the global cache without circular dependencies.

This function is thread-safe and uses sync.Once to ensure it's only set once.

Example (called by env package during initialization):

celexp.SetCacheFactory(env.GlobalCache)

func SetDefaultCacheSize

func SetDefaultCacheSize(size int)

SetDefaultCacheSize sets the size of the default cache. This must be called before the first call to GetDefaultCache() or Expression.Compile(). Once the cache is initialized, this function has no effect.

func SetDefaultCostLimit

func SetDefaultCostLimit(limit uint64)

SetDefaultCostLimit sets the default cost limit for all subsequent compilations that don't specify an explicit cost limit. This is thread-safe and can be called at runtime.

Example:

celexp.SetDefaultCostLimit(500000)  // Lower limit for security
celexp.SetDefaultCostLimit(0)       // Disable cost limiting

func SetEnvFactory

func SetEnvFactory(factory func(context.Context, ...cel.EnvOption) (*cel.Env, error))

SetEnvFactory sets the factory function used to create CEL environments. This should be called once during application initialization by the env package. It allows celexp to use environments with all custom extensions without circular dependencies.

This function is thread-safe and uses sync.Once to ensure it's only set once.

Types

type CELConfigInput

type CELConfigInput struct {
	// CacheSize is the maximum number of compiled programs to cache
	CacheSize int
	// CostLimit is the cost limit for expression evaluation (0 = disabled)
	CostLimit int64
	// UseASTBasedCaching enables AST-based cache key generation
	UseASTBasedCaching bool
	// EnableMetrics enables expression metrics collection
	EnableMetrics bool
}

CELConfigInput holds the configuration values for CEL initialization. This mirrors config.CELConfig but avoids circular dependencies.

type CacheOption

type CacheOption func(*ProgramCache)

CacheOption configures a ProgramCache.

func WithASTBasedCaching

func WithASTBasedCaching(enabled bool) CacheOption

WithASTBasedCaching enables AST-based cache key generation. When enabled, expressions with the same structure and types but different variable names will share cache entries.

For example, "x + y" and "a + b" (both int) will share the same cache entry, resulting in up to 75% better cache hit rates in typical workloads.

Example:

cache := celexp.NewProgramCache(100, celexp.WithASTBasedCaching(true))

type CacheStats

type CacheStats struct {
	Size           int              `json:"size"`
	MaxSize        int              `json:"max_size"`
	Hits           uint64           `json:"hits"`
	Misses         uint64           `json:"misses"`
	Evictions      uint64           `json:"evictions"`
	HitRate        float64          `json:"hit_rate"` // Percentage
	TotalAccesses  uint64           `json:"total_accesses"`
	TopExpressions []ExpressionStat `json:"top_expressions,omitempty"`
}

CacheStats contains cache performance statistics.

func GetDefaultCacheStats

func GetDefaultCacheStats() CacheStats

GetDefaultCacheStats returns statistics for the default cache. This is a convenience wrapper around GetDefaultCache().Stats().

Example:

stats := celexp.GetDefaultCacheStats()
fmt.Printf("Cache: %d/%d entries, %.1f%% hit rate\n",
	stats.Size, stats.MaxSize, stats.HitRate)
Example

ExampleGetDefaultCacheStats shows how to monitor cache performance.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Compile a few expressions
	expr1 := celexp.Expression("1 + 1")
	expr1.Compile([]cel.EnvOption{})

	expr2 := celexp.Expression("2 * 3")
	expr2.Compile([]cel.EnvOption{})

	// Compile the same expression again (cache hit)
	expr1.Compile([]cel.EnvOption{})

	// Get statistics
	stats := celexp.GetDefaultCacheStats()
	fmt.Printf("Cache size: %d/%d\n", stats.Size, stats.MaxSize)
	fmt.Printf("Hits: %d, Misses: %d\n", stats.Hits, stats.Misses)
	fmt.Printf("Hit rate: %.1f%%\n", stats.HitRate)
	// Output will vary, but shows cache is working
}

type CompileResult

type CompileResult struct {
	// Program is the compiled CEL program ready for evaluation
	Program cel.Program

	// Expression is the original expression that was compiled
	Expression Expression
	// contains filtered or unexported fields
}

CompileResult contains the compiled CEL program and metadata

func (*CompileResult) Eval

func (r *CompileResult) Eval(vars map[string]any) (any, error)

Eval evaluates the compiled CEL program with the provided variables. Variables should be a map where keys match the variable names declared during compilation. Returns the result as 'any' - use the generic EvalAs[T]() function for automatic type conversion and compile-time type safety.

Example usage:

expr := celexp.Expression("name.startsWith('hello')")
result, _ := expr.Compile([]cel.EnvOption{cel.Variable("name", cel.StringType)})
value, err := result.Eval(map[string]any{"name": "hello world"})
if err != nil {
    return err
}
fmt.Println(value) // true

For type-safe evaluation, use EvalAs[T]():

str, err := celexp.EvalAs[string](result, map[string]any{"name": "world"})

func (*CompileResult) EvalWithContext

func (r *CompileResult) EvalWithContext(ctx context.Context, vars map[string]any) (any, error)

EvalWithContext evaluates the compiled CEL program with context support. Use this when you need to cancel evaluation (e.g., HTTP request timeout).

Example usage:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
value, err := result.EvalWithContext(ctx, map[string]any{"name": "hello world"})
if errors.Is(err, context.DeadlineExceeded) {
    return fmt.Errorf("evaluation timed out")
}

func (*CompileResult) GetDeclaredVariables

func (r *CompileResult) GetDeclaredVariables() map[string]string

GetDeclaredVariables returns the variable declarations that were provided during compilation. This can be useful for debugging or documentation purposes.

Returns a map of variable name to CEL type string (e.g., "int", "string", "list"). Returns an empty map if no variables were declared or compiled with Compile() instead of CompileWithVarDecls().

func (*CompileResult) GetDeclaredVars

func (r *CompileResult) GetDeclaredVars() []VarInfo

GetDeclaredVars returns information about all variables declared during compilation. This is useful for debugging, documentation generation, and validation.

Returns a sorted slice of VarInfo for deterministic output. Returns nil if no variables were declared during compilation.

Example:

expr := celexp.Expression("x + y")
result, _ := expr.Compile([]cel.EnvOption{
    cel.Variable("x", cel.IntType),
    cel.Variable("name", cel.StringType),
})
vars := result.GetDeclaredVars()
// Returns: []VarInfo{
//   {Name: "name", Type: "string", CelType: cel.StringType},
//   {Name: "x", Type: "int", CelType: cel.IntType},
// }

func (*CompileResult) ValidateVars

func (r *CompileResult) ValidateVars(vars map[string]any) error

ValidateVars checks if the provided runtime variables match the types declared during compilation. This provides early type validation before evaluation, catching type mismatches with clear error messages.

Note: This only works if the expression was compiled using CompileWithVarDecls(). If compiled with Compile() directly, this will skip validation (no declarations available).

Returns an error if:

  • A required variable is missing
  • A variable's type doesn't match the declared type
  • A nil value is provided for a non-nullable type

Example:

decls := []celexp.VarDecl{
    celexp.NewVarDecl("x", cel.IntType),
    celexp.NewVarDecl("y", cel.IntType),
}
compiled, _ := expr.CompileWithVarDecls(decls)

// Valid - types match
err := compiled.ValidateVars(map[string]any{
    "x": int64(10),
    "y": int64(20),
})
// Returns: nil

// Invalid - wrong type
err = compiled.ValidateVars(map[string]any{
    "x": "string",
    "y": int64(20),
})
// Returns: error - variable "x": expected int, got string

// Invalid - missing variable
err = compiled.ValidateVars(map[string]any{
    "x": int64(10),
})
// Returns: error - missing required variable "y"

type Example

type Example struct {
	Description string   `json:"description,omitempty" yaml:"description,omitempty"`
	Expression  string   `json:"expression,omitempty" yaml:"expression,omitempty"`
	Links       []string `json:"links,omitempty" yaml:"links,omitempty"`
}

type Expression

type Expression string

func NewCoalesce

func NewCoalesce(values ...string) Expression

NewCoalesce creates a CEL expression that returns the first non-null value. Similar to SQL COALESCE or JavaScript ?? operator.

Note: For map property access (e.g., "user.name"), this uses the has() macro to check for existence. For simple variables, it checks against null.

Example:

expr := celexp.NewCoalesce("user.nickname", "user.name", `"Guest"`)
// Returns: has(user.nickname) ? user.nickname : has(user.name) ? user.name : "Guest"
Example

ExampleNewCoalesce demonstrates null coalescing for fallback values.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Returns first non-null value: nickname, then name, then "Guest"
	expr := celexp.NewCoalesce("user.nickname", "user.name", `"Guest"`)

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
	})
	if err != nil {
		log.Fatal(err)
	}

	// Case 1: Has nickname
	result1, _ := compiled.Eval(map[string]any{
		"user": map[string]any{
			"nickname": "Bobby",
			"name":     "Robert",
		},
	})
	fmt.Printf("With nickname: %v\n", result1)

	// Case 2: Only has name
	result2, _ := compiled.Eval(map[string]any{
		"user": map[string]any{
			"name": "Robert",
		},
	})
	fmt.Printf("With name only: %v\n", result2)

	// Case 3: Neither exists
	result3, _ := compiled.Eval(map[string]any{
		"user": map[string]any{},
	})
	fmt.Printf("Neither exists: %v\n", result3)

}
Output:
With nickname: Bobby
With name only: Robert
Neither exists: Guest
Example (MultipleFields)

ExampleNewCoalesce_multipleFields demonstrates coalescing across different fields.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Try multiple contact methods
	expr := celexp.NewCoalesce("contact.mobile", "contact.phone", "contact.email", `"No contact info"`)

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("contact", cel.MapType(cel.StringType, cel.DynType)),
	})
	if err != nil {
		log.Fatal(err)
	}

	// Has email but no phone
	result, _ := compiled.Eval(map[string]any{
		"contact": map[string]any{
			"email": "user@example.com",
		},
	})
	fmt.Println(result)

}
Output:
user@example.com

func NewConditional

func NewConditional(condition, trueExpr, falseExpr string) Expression

NewConditional creates a CEL expression for simple if/then/else logic. This is a convenience wrapper for the ternary operator.

Example:

expr := celexp.NewConditional("age >= 18", `"adult"`, `"minor"`)
// Equivalent to: age >= 18 ? "adult" : "minor"
Example

ExampleNewConditional demonstrates simple conditional expressions.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Create a ternary expression: age >= 18 ? "adult" : "minor"
	expr := celexp.NewConditional("age >= 18", `"adult"`, `"minor"`)

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("age", cel.IntType),
	})
	if err != nil {
		log.Fatal(err)
	}

	// Test with adult age
	result1, _ := compiled.Eval(map[string]any{"age": int64(25)})
	fmt.Printf("Age 25: %v\n", result1)

	// Test with minor age
	result2, _ := compiled.Eval(map[string]any{"age": int64(15)})
	fmt.Printf("Age 15: %v\n", result2)

}
Output:
Age 25: adult
Age 15: minor
Example (Nested)

ExampleNewConditional_nested demonstrates nested conditional logic.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Nested: score >= 90 ? "A" : (score >= 80 ? "B" : "C")
	inner := celexp.NewConditional("score >= 80", `"B"`, `"C"`)
	expr := celexp.NewConditional("score >= 90", `"A"`, string(inner))

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("score", cel.IntType),
	})
	if err != nil {
		log.Fatal(err)
	}

	grades := []int64{95, 85, 75}
	for _, score := range grades {
		result, _ := compiled.Eval(map[string]any{"score": score})
		fmt.Printf("Score %d: Grade %v\n", score, result)
	}

}
Output:
Score 95: Grade A
Score 85: Grade B
Score 75: Grade C

func NewStringInterpolation

func NewStringInterpolation(template string) Expression

NewStringInterpolation creates a CEL expression for string interpolation. Replaces ${var} placeholders with CEL variable references and automatically converts non-string expressions to strings.

Supported patterns:

  • Simple variables: ${name} → name
  • Nested expressions: ${user.name} → user.name
  • Auto string conversion: ${age} → string(age)
  • Escaping: \${literal} → literal "${literal}"

Example:

expr := celexp.NewStringInterpolation("Hello, ${name}! You are ${age} years old.")
// Converts to: "Hello, " + string(name) + "! You are " + string(age) + " years old."

expr := celexp.NewStringInterpolation("User: ${user.name} (${user.email})")
// Converts to: "User: " + string(user.name) + " (" + string(user.email) + ")"
Example

ExampleNewStringInterpolation demonstrates string interpolation.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Embed variables in a string template
	expr := celexp.NewStringInterpolation("Hello, ${name}! You are ${age} years old.")

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("name", cel.StringType),
		cel.Variable("age", cel.IntType),
	})
	if err != nil {
		log.Fatal(err)
	}

	result, _ := compiled.Eval(map[string]any{
		"name": "Alice",
		"age":  int64(30),
	})
	fmt.Println(result)

}
Output:
Hello, Alice! You are 30 years old.
Example (Escaping)

ExampleNewStringInterpolation_escaping demonstrates literal dollar signs.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Use \${ to include literal ${
	expr := celexp.NewStringInterpolation(`The price is \${price}, but the total is ${total}`)

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("price", cel.IntType),
		cel.Variable("total", cel.IntType),
	})
	if err != nil {
		log.Fatal(err)
	}

	result, _ := compiled.Eval(map[string]any{
		"price": int64(10),
		"total": int64(12),
	})
	fmt.Println(result)

}
Output:
The price is ${price}, but the total is 12
Example (NestedProperties)

ExampleNewStringInterpolation_nestedProperties demonstrates interpolation with nested objects.

package main

import (
	"fmt"
	"log"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	expr := celexp.NewStringInterpolation("User: ${user.name} (${user.email})")

	compiled, err := expr.Compile([]cel.EnvOption{
		cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)),
	})
	if err != nil {
		log.Fatal(err)
	}

	result, _ := compiled.Eval(map[string]any{
		"user": map[string]any{
			"name":  "Bob Smith",
			"email": "bob@example.com",
		},
	})
	fmt.Println(result)

}
Output:
User: Bob Smith (bob@example.com)

func (Expression) Compile

func (e Expression) Compile(envOpts []cel.EnvOption, opts ...Option) (*CompileResult, error)

Compile parses, checks, and compiles a CEL expression into an executable program.

This is the primary compilation method with a clean, flexible API. CEL environment options (variables, functions) are specified first, followed by optional configuration using With* functions.

FEATURES:

  • Uses package-level default cache automatically (lazy-initialized, size=1000)
  • Default cost limit of 1,000,000 to prevent DoS attacks (configurable via SetDefaultCostLimit)
  • Thread-safe and optimized for repeated compilations
  • Supports context cancellation, custom cache, and cost limits via options

Examples:

Simple usage:

expr := celexp.Expression("x + y")
result, err := expr.Compile([]cel.EnvOption{
    cel.Variable("x", cel.IntType),
    cel.Variable("y", cel.IntType),
})

With context and custom cost limit:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := expr.Compile(
    []cel.EnvOption{cel.Variable("x", cel.IntType)},
    WithContext(ctx),
    WithCostLimit(50000),
)

With custom cache:

myCache := celexp.NewProgramCache(500)
result, err := expr.Compile(
    []cel.EnvOption{cel.Variable("x", cel.IntType)},
    WithCache(myCache),
)
Example

ExampleExpression_Compile demonstrates the simplest way to compile CEL expressions. This is the recommended method for most use cases.

package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/oakwood-commons/scafctl/pkg/celexp"
)

func main() {
	// Simple compilation with default caching and cost limits
	expr := celexp.Expression("x * 2 + y")
	result, err := expr.Compile([]cel.EnvOption{
		cel.Variable("x", cel.IntType),
		cel.Variable("y", cel.IntType),
	})
	if err != nil {
		panic(err)
	}

	value, _ := result.Eval(map[string]any{"x": int64(10), "y": int64(5)})
	fmt.Println(value)
}
Output:
25
Example (WithCache)

ExampleExpression_Compile_withCache demonstrates basic usage of the CEL program cache.

// Create a cache with a maximum size of 100 programs
cache := NewProgramCache(100)

// Define the CEL expression and options
expr := Expression("x * 2 + y")
opts := []cel.EnvOption{
	cel.Variable("x", cel.IntType),
	cel.Variable("y", cel.IntType),
}

// First compilation - cache miss
compiled1, err := expr.Compile(opts, WithCache(cache), WithContext(context.Background()))
if err != nil {
	fmt.Printf("Error: %v\n", err)
	return
}

// Evaluate the program
result1, err := compiled1.Eval(map[string]any{"x": int64(10), "y": int64(5)})
if err != nil {
	fmt.Printf("Error: %v\n", err)
	return
}
fmt.Printf("Result 1: %v\n", result1)

// Second compilation - cache hit (much faster)
compiled2, err := expr.Compile(opts, WithCache(cache), WithContext(context.Background()))
if err != nil {
	fmt.Printf("Error: %v\n", err)
	return
}

// Evaluate again with different values
result2, err := compiled2.Eval(map[string]any{"x": int64(20), "y": int64(3)})
if err != nil {
	fmt.Printf("Error: %v\n", err)
	return
}
fmt.Printf("Result 2: %v\n", result2)

// Check cache statistics
stats := cache.Stats()
fmt.Printf("Cache hits: %d, misses: %d, hit rate: %.1f%%\n", stats.Hits, stats.Misses, stats.HitRate)
Output:
Result 1: 25
Result 2: 43
Cache hits: 1, misses: 1, hit rate: 50.0%

func (Expression) CompileWithVarDecls

func (e Expression) CompileWithVarDecls(varDecls []VarDecl, opts ...Option) (*CompileResult, error)

CompileWithVarDecls compiles a CEL expression with variable declarations that enable runtime type validation. This is a convenience method that wraps Compile() and enables ValidateVars() functionality.

Example:

decls := []celexp.VarDecl{
    celexp.NewVarDecl("x", cel.IntType),
    celexp.NewVarDecl("y", cel.IntType),
}
result, _ := expr.CompileWithVarDecls(decls)

// Now you can validate before evaluation
err := result.ValidateVars(map[string]any{"x": int64(10), "y": int64(20)})

func (Expression) GetUnderscoreVariables

func (e Expression) GetUnderscoreVariables() ([]string, error)

GetUnderscoreVariables is a convenience method that calls GetVariablesWithPrefix with "_." prefix.

Example:

expr := celexp.CelExpression("_.user.name + _.config.value")
vars, err := expr.GetUnderscoreVariables()
// Returns: []string{"user", "config"}, nil

func (Expression) GetVariablesWithPrefix

func (e Expression) GetVariablesWithPrefix(prefix string) ([]string, error)

GetVariablesWithPrefix parses the CEL expression and returns all variable references that start with the specified prefix. The returned variable names do not include the prefix. It returns a deduplicated, sorted list of variable names. If prefix is empty, it defaults to "_."

Example:

expr := celexp.CelExpression("_.user.name + _.config.value")
vars, err := expr.GetVariablesWithPrefix("_.")
// Returns: []string{"config", "user"}, nil (sorted)

expr := celexp.CelExpression("ctx.user.name + ctx.config.value")
vars, err := expr.GetVariablesWithPrefix("ctx.")
// Returns: []string{"config", "user"}, nil (sorted)

func (Expression) RequiredVariables

func (e Expression) RequiredVariables() ([]string, error)

RequiredVariables parses the CEL expression and returns all variable references found in the expression, regardless of prefix. This extracts ALL top-level identifiers that are not function names or comprehension variables. It returns a deduplicated, sorted list of variable names.

This is useful for:

  • Validating that all required variables are provided before evaluation
  • Auto-generating input prompts for missing variables
  • Documentation generation showing what inputs are needed
  • IDE autocomplete for configuration files

For expressions with prefixed variables (like _.x or ctx.y), use GetVariablesWithPrefix() instead.

Example:

expr := celexp.Expression("x + y + z")
vars, err := expr.RequiredVariables()
// Returns: []string{"x", "y", "z"}, nil (sorted)

expr = celexp.Expression("user.name + config.value")
vars, err = expr.RequiredVariables()
// Returns: []string{"config", "user"}, nil (sorted)

expr = celexp.Expression("[1, 2, 3].filter(x, x > 1)")
vars, err = expr.RequiredVariables()
// Returns: []string{}, nil (x is a comprehension variable, not external)

type ExpressionStat

type ExpressionStat struct {
	Expression string    `json:"expression"`
	Hits       uint64    `json:"hits"`
	LastAccess time.Time `json:"last_access"`
}

ExpressionStat contains statistics for a specific expression.

type ExtFunction

type ExtFunction struct {
	Name          string          `json:"name,omitempty" yaml:"name,omitempty"`
	Links         []string        `json:"links,omitempty" yaml:"links,omitempty"`
	Examples      []Example       `json:"examples,omitempty" yaml:"examples,omitempty"`
	Description   string          `json:"description,omitempty" yaml:"description,omitempty"`
	EnvOptions    []cel.EnvOption `json:"-" yaml:"-"`
	FunctionNames []string        `json:"function_names,omitempty" yaml:"function_names,omitempty"`
	Custom        bool            `json:"custom,omitempty" yaml:"custom,omitempty"`
}

type ExtFunctionList

type ExtFunctionList []ExtFunction

type Option

type Option func(*compileConfig)

Option is a functional option for configuring expression compilation. Use With* functions to create options.

func WithCache

func WithCache(cache *ProgramCache) Option

WithCache sets a custom cache instance for compiled programs. If not specified, uses the default package-level cache.

Example:

myCache := celexp.NewProgramCache(500)
expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)}, WithCache(myCache))

func WithContext

func WithContext(ctx context.Context) Option

WithContext sets the context for compilation, enabling cancellation and timeouts.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)}, WithContext(ctx))

func WithCostLimit

func WithCostLimit(limit uint64) Option

WithCostLimit sets a custom cost limit for expression evaluation. The cost limit prevents expensive expressions from consuming excessive resources. If not specified, uses the default cost limit (see GetDefaultCostLimit).

Example:

expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)}, WithCostLimit(50000))

func WithNoCostLimit

func WithNoCostLimit() Option

WithNoCostLimit disables cost limiting for the expression. Use with caution as this allows expressions to consume unlimited resources.

Example:

expr.Compile([]cel.EnvOption{cel.Variable("x", cel.IntType)}, WithNoCostLimit())

type ProgramCache

type ProgramCache struct {
	// contains filtered or unexported fields
}

ProgramCache is a thread-safe LRU cache for compiled CEL programs. It caches programs by a hash of the expression and environment options, allowing reuse of expensive compilation operations.

The cache also tracks detailed expression-level metrics for monitoring and debugging purposes.

When useASTKeys is enabled, the cache generates keys based on the AST structure rather than variable names, allowing expressions like "x + y" and "a + b" to share cache entries if they have the same structure and types.

func GetAppConfigCache

func GetAppConfigCache() *ProgramCache

GetAppConfigCache returns the cache created by InitFromAppConfig. Returns nil if InitFromAppConfig has not been called.

func GetDefaultCache

func GetDefaultCache() *ProgramCache

GetDefaultCache returns the package-level cache instance used by Expression.Compile(). The cache is lazily initialized on first access with DefaultCacheSize entries (10,000). All calls return the same cache instance (singleton pattern).

Use this when you need to access cache statistics:

stats := celexp.GetDefaultCache().Stats()
fmt.Printf("Cache hit rate: %.1f%%\n", stats.HitRate)

func NewProgramCache

func NewProgramCache(maxSize int, opts ...CacheOption) *ProgramCache

NewProgramCache creates a new program cache with the specified maximum size and optional configuration. When the cache reaches maxSize, the least recently used entry will be evicted. A maxSize of 0 or negative value defaults to 100.

Expression-level metrics are tracked for the top 1000 most accessed expressions to avoid unbounded memory growth.

Example with AST-based caching:

cache := celexp.NewProgramCache(100, celexp.WithASTBasedCaching(true))
Example

ExampleNewProgramCache demonstrates creating a cache with different sizes.

// Create a small cache for limited memory environments
smallCache := NewProgramCache(10)
fmt.Printf("Small cache max size: %d\n", smallCache.Stats().MaxSize)

// Create a larger cache for high-throughput scenarios
largeCache := NewProgramCache(1000)
fmt.Printf("Large cache max size: %d\n", largeCache.Stats().MaxSize)

// Default size (100) is used for invalid sizes
defaultCache := NewProgramCache(0)
fmt.Printf("Default cache max size: %d\n", defaultCache.Stats().MaxSize)
Output:
Small cache max size: 10
Large cache max size: 1000
Default cache max size: 100

func (*ProgramCache) Clear

func (c *ProgramCache) Clear()

Clear removes all entries from the cache but preserves statistics. Use ClearWithStats() to also reset hit/miss counters.

func (*ProgramCache) ClearWithStats

func (c *ProgramCache) ClearWithStats()

ClearWithStats removes all entries and resets all statistics to zero.

func (*ProgramCache) Get

func (c *ProgramCache) Get(key string) (cel.Program, bool)

Get retrieves a program from the cache if it exists. Returns the program and true if found, nil and false otherwise. Also updates expression-level metrics for monitoring.

func (*ProgramCache) GetDetailedStats

func (c *ProgramCache) GetDetailedStats(topN int) CacheStats

GetDetailedStats returns cache statistics including expression-level metrics. If topN is 0, returns all tracked expressions. Otherwise returns the top N most accessed expressions sorted by hit count (descending).

Note: Expression tracking is limited to the top 1000 expressions to prevent unbounded memory growth. If your cache tracks more than 1000 unique expressions, only the most frequently accessed ones will be included.

Example

ExampleProgramCache_GetDetailedStats demonstrates retrieving detailed cache metrics.

// Create a cache
cache := NewProgramCache(100)

// Compile some expressions
expressions := []Expression{
	"user.age >= 18",
	"config.enabled == true",
	"items.size() > 0",
}

for _, expr := range expressions {
	result, err := expr.Compile(nil, WithCache(cache))
	if err == nil {
		// Simulate accessing the cached expressions
		_ = result.Program
	}
}

// Access expressions with different frequencies
for i := 0; i < 10; i++ {
	cache.Get("some-key-for-user-age")
}
for i := 0; i < 5; i++ {
	cache.Get("some-key-for-config")
}
for i := 0; i < 2; i++ {
	cache.Get("some-key-for-items")
}

// Get detailed stats with top 2 expressions
stats := cache.GetDetailedStats(2)

fmt.Printf("Cache Size: %d\n", stats.Size)
fmt.Printf("Total Accesses: %d\n", stats.TotalAccesses)
fmt.Printf("Hit Rate: %.1f%%\n", stats.HitRate)
fmt.Printf("\nTop %d Expressions:\n", len(stats.TopExpressions))
for i, expr := range stats.TopExpressions {
	fmt.Printf("%d. %s (hits: %d)\n", i+1, expr.Expression, expr.Hits)
}

// Example output (exact values depend on cache keys):
// Cache Size: 3
// Total Accesses: 17
// Hit Rate: 100.0%
//
// Top 2 Expressions:
// 1. user.age >= 18 (hits: 10)
// 2. config.enabled == true (hits: 5)
Example (Monitoring)

ExampleProgramCache_GetDetailedStats_monitoring demonstrates production monitoring.

// Create a cache for production monitoring
cache := NewProgramCache(500)

// In production, expressions would be compiled and cached
// Here we simulate some activity
expr1 := Expression("request.path.startsWith('/api')")
result1, _ := expr1.Compile(nil, WithCache(cache))
_ = result1

expr2 := Expression("user.role == 'admin'")
result2, _ := expr2.Compile(nil, WithCache(cache))
_ = result2

// Get all tracked expressions (topN = 0 means all)
stats := cache.GetDetailedStats(0)

// Monitor cache performance
fmt.Printf("Cache Performance:\n")
fmt.Printf("  Cached Programs: %d/%d\n", stats.Size, stats.MaxSize)
fmt.Printf("  Cache Hit Rate: %.2f%%\n", stats.HitRate)
fmt.Printf("  Total Cache Accesses: %d\n", stats.TotalAccesses)

// Track most frequently accessed expressions
if len(stats.TopExpressions) > 0 {
	fmt.Printf("\nMost Accessed Expressions:\n")
	for _, expr := range stats.TopExpressions {
		fmt.Printf("  - %q: %d hits\n", expr.Expression, expr.Hits)
		fmt.Printf("    Last accessed: %v\n", expr.LastAccess.Format("2006-01-02 15:04:05"))
	}
}

// Example output:
// Cache Performance:
//   Cached Programs: 2/500
//   Cache Hit Rate: 0.00%
//   Total Cache Accesses: 0
//
// Most Accessed Expressions:
//   - "request.path.startsWith('/api')": 0 hits
//     Last accessed: 2024-01-15 10:30:45
//   - "user.role == 'admin'": 0 hits
//     Last accessed: 2024-01-15 10:30:45

func (*ProgramCache) Put

func (c *ProgramCache) Put(key string, program cel.Program, expression string)

Put adds a program to the cache with its expression for metrics tracking. If the cache is full, it evicts the least recently used entry before adding the new one.

func (*ProgramCache) ResetStats

func (c *ProgramCache) ResetStats()

ResetStats resets cache statistics (hits, misses, evictions) without removing cached entries.

func (*ProgramCache) Stats

func (c *ProgramCache) Stats() CacheStats

Stats returns cache statistics.

Example

ExampleProgramCache_Stats demonstrates monitoring cache performance.

cache := NewProgramCache(10)

// Compile several expressions
expressions := []Expression{"1 + 2", "3 * 4", "5 - 1"}
for _, expr := range expressions {
	_, _ = expr.Compile([]cel.EnvOption{}, WithCache(cache))
}

// Access some cached programs
_, _ = Expression("1 + 2").Compile([]cel.EnvOption{}, WithCache(cache)) // Hit
_, _ = Expression("3 * 4").Compile([]cel.EnvOption{}, WithCache(cache)) // Hit

// Get statistics
stats := cache.Stats()
fmt.Printf("Size: %d/%d\n", stats.Size, stats.MaxSize)
fmt.Printf("Hits: %d, Misses: %d\n", stats.Hits, stats.Misses)
fmt.Printf("Hit Rate: %.1f%%\n", stats.HitRate)
fmt.Printf("Evictions: %d\n", stats.Evictions)
Output:
Size: 3/10
Hits: 2, Misses: 3
Hit Rate: 40.0%
Evictions: 0

type VarDecl

type VarDecl struct {
	// contains filtered or unexported fields
}

VarDecl creates a variable declaration that can be used for both compilation and validation. This is a convenience wrapper around cel.Variable that also enables type validation.

Example:

decls := []celexp.VarDecl{
    celexp.NewVarDecl("x", cel.IntType),
    celexp.NewVarDecl("name", cel.StringType),
}
result, _ := expr.CompileWithVarDecls(decls)
err := result.ValidateVars(map[string]any{"x": int64(10), "name": "test"})

func NewVarDecl

func NewVarDecl(name string, celType *cel.Type) VarDecl

NewVarDecl creates a new variable declaration for use with CompileWithVarDecls.

func (VarDecl) ToEnvOption

func (v VarDecl) ToEnvOption() cel.EnvOption

ToEnvOption converts the variable declaration to a cel.EnvOption.

type VarInfo

type VarInfo struct {
	// Name is the variable name as it appears in expressions
	Name string

	// Type is a human-readable type name (e.g., "int", "string", "list", "map")
	Type string

	// CelType is the underlying CEL type object for advanced type checking
	CelType *cel.Type
}

VarInfo describes a declared variable with its name and type information. This is useful for debugging, documentation generation, and validation.

Directories

Path Synopsis
ext
map
out

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL