Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/abiosoft/colima v0.8.4
github.com/briandowns/spinner v1.23.2
github.com/compose-spec/compose-go/v2 v2.8.2
github.com/expr-lang/expr v1.17.6
github.com/fluxcd/helm-controller/api v1.3.0
github.com/fluxcd/kustomize-controller/api v1.6.1
github.com/fluxcd/pkg/apis/kustomize v1.11.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfU
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
Expand Down
169 changes: 169 additions & 0 deletions pkg/blueprint/feature_evaluator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package blueprint

import (
"fmt"

"github.com/expr-lang/expr"
)

// FeatureEvaluator provides lightweight expression evaluation for blueprint feature conditions.
// It uses the expr library for fast compilation and evaluation of simple comparison expressions
// without the overhead of a full expression language like CEL for basic equality checks.
// The FeatureEvaluator enables efficient feature activation based on user configuration values.

// =============================================================================
// Types
// =============================================================================

// FeatureEvaluator provides lightweight expression evaluation for feature conditions.
type FeatureEvaluator struct{}

// =============================================================================
// Constructor
// =============================================================================

// NewFeatureEvaluator creates a new lightweight feature evaluator for expression evaluation.
func NewFeatureEvaluator() *FeatureEvaluator {
return &FeatureEvaluator{}
}

// =============================================================================
// Public Methods
// =============================================================================

// EvaluateExpression evaluates an expression string against the provided configuration data.
// The expression should use simple comparison syntax supported by expr:
// - Equality/inequality: ==, !=
// - Logical operators: &&, ||
// - Parentheses for grouping: (expression)
// - Nested object access: provider, observability.enabled, vm.driver
// Returns true if the expression evaluates to true, false otherwise.
func (e *FeatureEvaluator) EvaluateExpression(expression string, config map[string]any) (bool, error) {
if expression == "" {
return false, fmt.Errorf("expression cannot be empty")
}

program, err := expr.Compile(expression)
if err != nil {
return false, fmt.Errorf("failed to compile expression '%s': %w", expression, err)
}

result, err := expr.Run(program, config)
if err != nil {
return false, fmt.Errorf("failed to evaluate expression '%s': %w", expression, err)
}

boolResult, ok := result.(bool)
if !ok {
return false, fmt.Errorf("expression '%s' must evaluate to boolean, got %T", expression, result)
}

return boolResult, nil
}

// MatchConditions evaluates whether all conditions in a map match the provided configuration.
// This provides a simple key-value matching interface for basic feature conditions.
// Each condition key-value pair must match for the overall result to be true.
func (e *FeatureEvaluator) MatchConditions(conditions map[string]any, config map[string]any) bool {
if len(conditions) == 0 {
return true
}

for key, expectedValue := range conditions {
if !e.matchCondition(key, expectedValue, config) {
return false
}
}

return true
}

// =============================================================================
// Private Methods
// =============================================================================

// matchCondition checks if a single condition matches the configuration.
// Supports dot notation for nested field access and array matching for OR logic.
func (e *FeatureEvaluator) matchCondition(key string, expectedValue any, config map[string]any) bool {
actualValue := e.getNestedValue(key, config)

if expectedArray, ok := expectedValue.([]any); ok {
for _, expected := range expectedArray {
if e.valuesEqual(actualValue, expected) {
return true
}
}
return false
}

return e.valuesEqual(actualValue, expectedValue)
}

// getNestedValue retrieves a value from a nested map using dot notation.
// For example, "observability.enabled" retrieves config["observability"]["enabled"].
func (e *FeatureEvaluator) getNestedValue(key string, config map[string]any) any {
keys := e.splitKey(key)
current := config

for i, k := range keys {
if current == nil {
return nil
}

value, exists := current[k]
if !exists {
return nil
}

if i == len(keys)-1 {
return value
}

if nextMap, ok := value.(map[string]any); ok {
current = nextMap
} else {
return nil
}
}

return nil
}

// splitKey splits a dot-notation key into its component parts.
func (e *FeatureEvaluator) splitKey(key string) []string {
if key == "" {
return []string{}
}

var parts []string
current := ""

for _, char := range key {
if char == '.' {
if current != "" {
parts = append(parts, current)
current = ""
}
} else {
current += string(char)
}
}

if current != "" {
parts = append(parts, current)
}

return parts
}

// valuesEqual compares two values for equality, handling type conversion.
func (e *FeatureEvaluator) valuesEqual(actual, expected any) bool {
if actual == nil && expected == nil {
return true
}
if actual == nil || expected == nil {
return false
}

return fmt.Sprintf("%v", actual) == fmt.Sprintf("%v", expected)
}
Loading
Loading