package rules import ( "context" "fmt" "log/slog" ) // Engine evaluates rules against URL metadata type Engine struct { config *RuleConfig logger *slog.Logger } // NewEngine creates a new rule engine with the given configuration func NewEngine(config *RuleConfig) *Engine { return &Engine{ config: config, logger: slog.Default(), } } // NewEngineWithLogger creates a new rule engine with the given configuration and logger func NewEngineWithLogger(config *RuleConfig, logger *slog.Logger) *Engine { return &Engine{ config: config, logger: logger, } } // Evaluate evaluates rules against URL metadata and returns archivers // Returns archivers from the first matching rule, or default archiver if no rules match func (e *Engine) Evaluate(ctx context.Context, metadata *URLMetadata) ([]ArchiverConfig, error) { if e.config == nil { return nil, fmt.Errorf("rule engine: configuration is nil") } // Validate all rules for i, rule := range e.config.Rules { if err := rule.IsValid(); err != nil { return nil, fmt.Errorf("rule engine: rule %d is invalid: %w", i, err) } } // Evaluate rules in order for i, rule := range e.config.Rules { archivers, err := rule.Matches(ctx, metadata) if err != nil { return nil, fmt.Errorf("rule engine: error evaluating rule %d: %w", i, err) } // If rule matched (returned archivers, including empty slice), use them and STOP if archivers != nil { e.logger.Info("Rule matched, stopping evaluation", "ruleIndex", i, "archiverCount", len(archivers)) return archivers, nil } } // No rules matched, use default archivers if len(e.config.DefaultArchivers) == 0 { return nil, fmt.Errorf("rule engine: no rules matched and no default archivers configured") } e.logger.Info("No rules matched, using default archivers", "archivers", e.config.DefaultArchivers) return e.config.DefaultArchivers, nil }