refactor: simplify criteria Expression interface

Replaced the Fields() type switch with a fields() method on the
Expression interface, eliminating the need to update a central switch
when adding new expression types. Removed the now-redundant
criteriaExpression() marker method since fields() alone suffices to
restrict the interface. Extracted a conjunction interface for the
ChildPlaylistIds() lookup used by All and Any.
This commit is contained in:
Deluan 2026-04-26 10:43:54 -04:00
parent 5d1c9530ab
commit 0ab10e819f
4 changed files with 47 additions and 77 deletions

View File

@ -9,7 +9,7 @@ import (
) )
type Expression interface { type Expression interface {
criteriaExpression() fields() map[string]any
} }
type Criteria struct { type Criteria struct {
@ -53,7 +53,7 @@ func (c Criteria) ChildPlaylistIds() []string {
return nil return nil
} }
if parent, ok := c.Expression.(interface{ ChildPlaylistIds() (ids []string) }); ok { if parent, ok := c.Expression.(conjunction); ok {
return parent.ChildPlaylistIds() return parent.ChildPlaylistIds()
} }

View File

@ -2,12 +2,17 @@ package criteria
import "time" import "time"
// Conjunctions need to implement this interface, to allow Criteria to extract child playlist IDs recursively
type conjunction interface {
ChildPlaylistIds() []string
}
type ( type (
All []Expression All []Expression
And = All And = All
) )
func (All) criteriaExpression() {} func (All) fields() map[string]any { return nil }
func (all All) MarshalJSON() ([]byte, error) { func (all All) MarshalJSON() ([]byte, error) {
return marshalConjunction("all", all) return marshalConjunction("all", all)
@ -22,7 +27,7 @@ type (
Or = Any Or = Any
) )
func (Any) criteriaExpression() {} func (Any) fields() map[string]any { return nil }
func (any Any) MarshalJSON() ([]byte, error) { func (any Any) MarshalJSON() ([]byte, error) {
return marshalConjunction("any", any) return marshalConjunction("any", any)
@ -35,128 +40,128 @@ func (any Any) ChildPlaylistIds() (ids []string) {
type Is map[string]any type Is map[string]any
type Eq = Is type Eq = Is
func (Is) criteriaExpression() {}
func (is Is) MarshalJSON() ([]byte, error) { func (is Is) MarshalJSON() ([]byte, error) {
return marshalExpression("is", is) return marshalExpression("is", is)
} }
func (is Is) fields() map[string]any { return is }
type IsNot map[string]any type IsNot map[string]any
func (IsNot) criteriaExpression() {} func (isn IsNot) MarshalJSON() ([]byte, error) {
return marshalExpression("isNot", isn)
func (in IsNot) MarshalJSON() ([]byte, error) {
return marshalExpression("isNot", in)
} }
type Gt map[string]any func (isn IsNot) fields() map[string]any { return isn }
func (Gt) criteriaExpression() {} type Gt map[string]any
func (gt Gt) MarshalJSON() ([]byte, error) { func (gt Gt) MarshalJSON() ([]byte, error) {
return marshalExpression("gt", gt) return marshalExpression("gt", gt)
} }
type Lt map[string]any func (gt Gt) fields() map[string]any { return gt }
func (Lt) criteriaExpression() {} type Lt map[string]any
func (lt Lt) MarshalJSON() ([]byte, error) { func (lt Lt) MarshalJSON() ([]byte, error) {
return marshalExpression("lt", lt) return marshalExpression("lt", lt)
} }
type Before map[string]any func (lt Lt) fields() map[string]any { return lt }
func (Before) criteriaExpression() {} type Before map[string]any
func (bf Before) MarshalJSON() ([]byte, error) { func (bf Before) MarshalJSON() ([]byte, error) {
return marshalExpression("before", bf) return marshalExpression("before", bf)
} }
type After Gt func (bf Before) fields() map[string]any { return bf }
func (After) criteriaExpression() {} type After Gt
func (af After) MarshalJSON() ([]byte, error) { func (af After) MarshalJSON() ([]byte, error) {
return marshalExpression("after", af) return marshalExpression("after", af)
} }
type Contains map[string]any func (af After) fields() map[string]any { return af }
func (Contains) criteriaExpression() {} type Contains map[string]any
func (ct Contains) MarshalJSON() ([]byte, error) { func (ct Contains) MarshalJSON() ([]byte, error) {
return marshalExpression("contains", ct) return marshalExpression("contains", ct)
} }
type NotContains map[string]any func (ct Contains) fields() map[string]any { return ct }
func (NotContains) criteriaExpression() {} type NotContains map[string]any
func (nct NotContains) MarshalJSON() ([]byte, error) { func (nct NotContains) MarshalJSON() ([]byte, error) {
return marshalExpression("notContains", nct) return marshalExpression("notContains", nct)
} }
type StartsWith map[string]any func (nct NotContains) fields() map[string]any { return nct }
func (StartsWith) criteriaExpression() {} type StartsWith map[string]any
func (sw StartsWith) MarshalJSON() ([]byte, error) { func (sw StartsWith) MarshalJSON() ([]byte, error) {
return marshalExpression("startsWith", sw) return marshalExpression("startsWith", sw)
} }
func (sw StartsWith) fields() map[string]any { return sw }
type EndsWith map[string]any type EndsWith map[string]any
func (EndsWith) criteriaExpression() {} func (ew EndsWith) MarshalJSON() ([]byte, error) {
return marshalExpression("endsWith", ew)
func (sw EndsWith) MarshalJSON() ([]byte, error) {
return marshalExpression("endsWith", sw)
} }
type InTheRange map[string]any func (ew EndsWith) fields() map[string]any { return ew }
func (InTheRange) criteriaExpression() {} type InTheRange map[string]any
func (itr InTheRange) MarshalJSON() ([]byte, error) { func (itr InTheRange) MarshalJSON() ([]byte, error) {
return marshalExpression("inTheRange", itr) return marshalExpression("inTheRange", itr)
} }
type InTheLast map[string]any func (itr InTheRange) fields() map[string]any { return itr }
func (InTheLast) criteriaExpression() {} type InTheLast map[string]any
func (itl InTheLast) MarshalJSON() ([]byte, error) { func (itl InTheLast) MarshalJSON() ([]byte, error) {
return marshalExpression("inTheLast", itl) return marshalExpression("inTheLast", itl)
} }
type NotInTheLast map[string]any func (itl InTheLast) fields() map[string]any { return itl }
func (NotInTheLast) criteriaExpression() {} type NotInTheLast map[string]any
func (nitl NotInTheLast) MarshalJSON() ([]byte, error) { func (nitl NotInTheLast) MarshalJSON() ([]byte, error) {
return marshalExpression("notInTheLast", nitl) return marshalExpression("notInTheLast", nitl)
} }
func (nitl NotInTheLast) fields() map[string]any { return nitl }
func startOfPeriod(numDays int64, from time.Time) string { func startOfPeriod(numDays int64, from time.Time) string {
return from.Add(time.Duration(-24*numDays) * time.Hour).Format("2006-01-02") return from.Add(time.Duration(-24*numDays) * time.Hour).Format("2006-01-02")
} }
type InPlaylist map[string]any type InPlaylist map[string]any
func (InPlaylist) criteriaExpression() {}
func (ipl InPlaylist) MarshalJSON() ([]byte, error) { func (ipl InPlaylist) MarshalJSON() ([]byte, error) {
return marshalExpression("inPlaylist", ipl) return marshalExpression("inPlaylist", ipl)
} }
func (ipl InPlaylist) fields() map[string]any { return ipl }
type NotInPlaylist map[string]any type NotInPlaylist map[string]any
func (NotInPlaylist) criteriaExpression() {} func (nipl NotInPlaylist) MarshalJSON() ([]byte, error) {
return marshalExpression("notInPlaylist", nipl)
func (ipl NotInPlaylist) MarshalJSON() ([]byte, error) {
return marshalExpression("notInPlaylist", ipl)
} }
func (nipl NotInPlaylist) fields() map[string]any { return nipl }
func extractPlaylistIds(inputRule any) (ids []string) { func extractPlaylistIds(inputRule any) (ids []string) {
var id string var id string
var ok bool var ok bool

View File

@ -32,41 +32,6 @@ func Walk(expr Expression, visit Visitor) error {
return nil return nil
} }
// Fields returns field values for leaf expressions only.
// Use Walk to traverse All and Any expressions before calling Fields.
func Fields(expr Expression) map[string]any { func Fields(expr Expression) map[string]any {
switch e := expr.(type) { return expr.fields()
case Is:
return map[string]any(e)
case IsNot:
return map[string]any(e)
case Gt:
return map[string]any(e)
case Lt:
return map[string]any(e)
case Before:
return map[string]any(e)
case After:
return map[string]any(Gt(e))
case Contains:
return map[string]any(e)
case NotContains:
return map[string]any(e)
case StartsWith:
return map[string]any(e)
case EndsWith:
return map[string]any(e)
case InTheRange:
return map[string]any(e)
case InTheLast:
return map[string]any(e)
case NotInTheLast:
return map[string]any(e)
case InPlaylist:
return map[string]any(e)
case NotInPlaylist:
return map[string]any(e)
default:
return nil
}
} }

View File

@ -9,7 +9,7 @@ import (
type unknownExpression struct{} type unknownExpression struct{}
func (unknownExpression) criteriaExpression() {} func (unknownExpression) fields() map[string]any { return nil }
var _ = Describe("Walk", func() { var _ = Describe("Walk", func() {
It("visits the expression tree depth-first", func() { It("visits the expression tree depth-first", func() {