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 ¶
- Constants
- Variables
- func BuildCELContext(rootData any, additionalVars map[string]any) (envOpts []cel.EnvOption, vars map[string]any)
- func ClearDefaultCache()
- func EvalAs[T any](r *CompileResult, vars map[string]any) (T, error)
- func EvalAsWithContext[T any](ctx context.Context, r *CompileResult, vars map[string]any) (T, error)
- func EvaluateExpression(ctx context.Context, exprStr string, rootData any, ...) (any, error)
- func GetDefaultCostLimit() uint64
- func InitFromAppConfig(ctx context.Context, cfg CELConfigInput)
- func ResetDefaultCache()
- func ResetForTesting()
- func SetCacheFactory(factory func() *ProgramCache)
- func SetDefaultCacheSize(size int)
- func SetDefaultCostLimit(limit uint64)
- func SetEnvFactory(factory func(context.Context, ...cel.EnvOption) (*cel.Env, error))
- type CELConfigInput
- type CacheOption
- type CacheStats
- type CompileResult
- func (r *CompileResult) Eval(vars map[string]any) (any, error)
- func (r *CompileResult) EvalWithContext(ctx context.Context, vars map[string]any) (any, error)
- func (r *CompileResult) GetDeclaredVariables() map[string]string
- func (r *CompileResult) GetDeclaredVars() []VarInfo
- func (r *CompileResult) ValidateVars(vars map[string]any) error
- type Example
- type Expression
- func (e Expression) Compile(envOpts []cel.EnvOption, opts ...Option) (*CompileResult, error)
- func (e Expression) CompileWithVarDecls(varDecls []VarDecl, opts ...Option) (*CompileResult, error)
- func (e Expression) GetUnderscoreVariables() ([]string, error)
- func (e Expression) GetVariablesWithPrefix(prefix string) ([]string, error)
- func (e Expression) RequiredVariables() ([]string, error)
- type ExpressionStat
- type ExtFunction
- type ExtFunctionList
- type Option
- type ProgramCache
- func (c *ProgramCache) Clear()
- func (c *ProgramCache) ClearWithStats()
- func (c *ProgramCache) Get(key string) (cel.Program, bool)
- func (c *ProgramCache) GetDetailedStats(topN int) CacheStats
- func (c *ProgramCache) Put(key string, program cel.Program, expression string)
- func (c *ProgramCache) ResetStats()
- func (c *ProgramCache) Stats() CacheStats
- type VarDecl
- type VarInfo
Examples ¶
- Package (AstCaching_basic)
- Package (AstCaching_complexExpressions)
- Package (AstCaching_performance)
- Package (AstCaching_realWorldScenario)
- Package (AstCaching_whenToUse)
- Package (AstCaching_whitespaceInsensitive)
- Package (ChoosingCompilationMethod)
- Package (CombinedPatterns)
- Package (Production_batchProcessing)
- Package (Production_errorHandling)
- Package (Production_nullSafeAccess)
- Package (Production_ruleEngine)
- Package (Production_withCaching)
- Package (Production_withTimeout)
- Package (Production_withValidation)
- Package (Testing_assertions)
- Package (Testing_tableDriven)
- ClearDefaultCache
- EvalAs
- EvalAs (Int)
- EvalAs (StringSlice)
- EvalAsWithContext (Int)
- EvalAsWithContext (StringSlice)
- Expression.Compile
- Expression.Compile (WithCache)
- GetDefaultCacheStats
- NewCoalesce
- NewCoalesce (MultipleFields)
- NewConditional
- NewConditional (Nested)
- NewProgramCache
- NewStringInterpolation
- NewStringInterpolation (Escaping)
- NewStringInterpolation (NestedProperties)
- ProgramCache.GetDetailedStats
- ProgramCache.GetDetailedStats (Monitoring)
- ProgramCache.Stats
Constants ¶
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 ¶
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 ¶
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
}
Output:
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 ¶
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 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 ¶
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 ¶
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 ¶
NewVarDecl creates a new variable declaration for use with CompileWithVarDecls.
func (VarDecl) ToEnvOption ¶
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.