134 lines
3.1 KiB
Go
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
|
|
}
|
|
}
|