Files
rsspipe/internal/rules/evaluator.go
T

134 lines
3.1 KiB
Go

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
}
}