package rules import ( "fmt" "regexp" "strings" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models" ) type Evaluator struct{} func NewEvaluator() *Evaluator { return &Evaluator{} } // Evaluate checks whether a FeedEntry satisfies a Rule's conditions. func (e *Evaluator) Evaluate(rule models.Rule, entry models.FeedEntry) bool { if len(rule.Conditions) == 0 { return true } matchMode := strings.ToLower(rule.MatchMode) if matchMode == "" || matchMode == "all" { for _, cond := range rule.Conditions { if !e.evaluateCondition(cond, entry) { return false } } return true } // matchMode == "any" for _, cond := range rule.Conditions { if e.evaluateCondition(cond, entry) { return true } } return false } func (e *Evaluator) evaluateCondition(cond models.RuleCondition, entry models.FeedEntry) bool { actualValue := e.resolveField(cond.Field, entry) return e.compare(actualValue, cond.Operator, cond.Value) } func (e *Evaluator) resolveField(field string, entry models.FeedEntry) interface{} { switch strings.ToLower(field) { case "feed_id": return entry.FeedID case "feed_title": return entry.FeedTitle case "title": return entry.Title case "author": return entry.Author case "url": return entry.URL case "content", "rss_content": return entry.Content case "is_read": return entry.IsRead case "is_starred": return entry.IsStarred } // Variant field resolution: "variants.keywords", "variants.readability_full_text", "variants.tags" if strings.HasPrefix(field, "variants.") { variantKey := strings.TrimPrefix(field, "variants.") if entry.Variants != nil { if v, ok := entry.Variants[variantKey]; ok { return v.TextValue } } } return nil } func (e *Evaluator) compare(actual interface{}, op string, expected interface{}) bool { if actual == nil { return op == "neq" } actualStr := fmt.Sprintf("%v", actual) expectedStr := fmt.Sprintf("%v", expected) switch strings.ToLower(op) { case "eq", "==": return strings.EqualFold(actualStr, expectedStr) case "neq", "!=": return !strings.EqualFold(actualStr, expectedStr) case "contains": return strings.Contains(strings.ToLower(actualStr), strings.ToLower(expectedStr)) case "not_contains": return !strings.Contains(strings.ToLower(actualStr), strings.ToLower(expectedStr)) case "in": // expected can be []interface{}, []string, or comma-separated string switch v := expected.(type) { case []interface{}: for _, item := range v { if strings.EqualFold(actualStr, fmt.Sprintf("%v", item)) { return true } } return false case []string: for _, item := range v { if strings.EqualFold(actualStr, item) { return true } } return false case string: items := strings.Split(v, ",") for _, item := range items { if strings.EqualFold(strings.TrimSpace(actualStr), strings.TrimSpace(item)) { return true } } return false } return false case "matches", "regex": re, err := regexp.Compile(expectedStr) if err != nil { return false } return re.MatchString(actualStr) default: return false } }