veylant/internal/routing/engine.go
2026-02-23 13:35:04 +01:00

72 lines
2.1 KiB
Go

package routing
import (
"context"
"time"
"go.uber.org/zap"
)
// Engine evaluates routing rules from the RuleCache against a RoutingContext.
// It is the central component of the intelligent routing system.
type Engine struct {
cache *RuleCache
logger *zap.Logger
}
// New creates an Engine backed by store with the given cache TTL.
func New(store RuleStore, ttl time.Duration, logger *zap.Logger) *Engine {
return &Engine{
cache: NewRuleCache(store, ttl, logger),
logger: logger,
}
}
// Start launches the cache background refresh goroutine.
func (e *Engine) Start() { e.cache.Start() }
// Stop shuts down the cache refresh goroutine.
func (e *Engine) Stop() { e.cache.Stop() }
// Cache returns the underlying RuleCache so callers (e.g. admin API) can
// invalidate entries after mutations.
func (e *Engine) Cache() *RuleCache { return e.cache }
// Evaluate finds the first matching rule for rctx and returns its Action.
// Rules are evaluated in priority order (ASC). All conditions within a rule
// must match (AND logic). An empty Conditions slice matches everything (catch-all).
//
// Returns (action, true, nil) on match.
// Returns (Action{}, false, nil) when no rule matches.
// Returns (Action{}, false, err) on cache/store errors.
func (e *Engine) Evaluate(ctx context.Context, rctx *RoutingContext) (Action, bool, error) {
rules, err := e.cache.Get(ctx, rctx.TenantID)
if err != nil {
return Action{}, false, err
}
for _, rule := range rules { // already sorted ASC by priority
if matchesAll(rule.Conditions, rctx) {
e.logger.Debug("routing rule matched",
zap.String("rule_id", rule.ID),
zap.String("rule_name", rule.Name),
zap.String("provider", rule.Action.Provider),
zap.String("tenant_id", rctx.TenantID),
)
return rule.Action, true, nil
}
}
return Action{}, false, nil
}
// matchesAll returns true if every condition in the slice holds for rctx.
// An empty slice is a catch-all — it always matches.
func matchesAll(conditions []Condition, rctx *RoutingContext) bool {
for _, c := range conditions {
if !c.Evaluate(rctx) {
return false
}
}
return true
}