Преглед изворни кода

Proposal for adding empty op to filters

Niko Kovacevic пре 10 месеци
родитељ
комит
4d08c58a39

+ 4 - 0
core/pkg/filter/ast/lexer.go

@@ -29,6 +29,10 @@ const (
 	tildeEndColon       // '~>:'
 	bangTildeEndColon   // '!~>:'
 
+	// TODO consider adding these as "empty" and "notempty"
+	bangBackslack // '!\'
+	backslash     // '\'
+
 	parenOpen  // '('
 	parenClose // ')'
 

+ 20 - 0
core/pkg/filter/ast/ops.go

@@ -49,6 +49,14 @@ const (
 	// FilterOpNotContainsSuffix is the inverse of FilterOpContainsSuffix
 	FilterOpNotContainsSuffix = "notcontainssuffix"
 
+	// FilterOpEmpty supports string fields or slice fields.
+	// For strings, this is eqivalent to empty(), which returns true iff string is length 0
+	// For slices, this is equivalent to empty(), which returns true iff slice is length 0
+	FilterOpEmpty = "empty"
+
+	// FilterOpNotEmpty is the inverse of empty.
+	FilterOpNotEmpty = "notempty"
+
 	// FilterOpVoid is base-depth operator that is used for an empty filter
 	FilterOpVoid = "void"
 
@@ -192,6 +200,18 @@ func (_ *ContainsSuffixOp) Op() FilterOp {
 	return FilterOpContainsSuffix
 }
 
+// EmptyOp is a filter operation that checks to see if a resolvable identifier (Left) is empty
+type EmptyOp struct {
+	// Left contains a resolvable Identifier (property of an input type) which can be
+	// used to query against using the Right value.
+	Left Identifier
+}
+
+// Op returns the FilterOp enumeration value for the operator.
+func (_ *EmptyOp) Op() FilterOp {
+	return FilterOpEmpty
+}
+
 func Not(fn FilterNode) FilterNode {
 	return &NotOp{Operand: fn}
 }

+ 18 - 0
core/pkg/filter/ast/parser.go

@@ -522,6 +522,24 @@ func toFilterNode(field *Field, key string, op FilterOp, value string) (FilterNo
 			},
 		}, nil
 
+	case FilterOpEmpty:
+		return &EmptyOp{
+			Left: Identifier{
+				Field: field,
+				Key:   key,
+			},
+		}, nil
+
+	case FilterOpNotEmpty:
+		return &NotOp{
+			Operand: &EmptyOp{
+				Left: Identifier{
+					Field: field,
+					Key:   key,
+				},
+			},
+		}, nil
+
 	default:
 		return nil, fmt.Errorf("Failed to parse op: %s", op)
 	}

+ 26 - 0
core/pkg/filter/ast/walker.go

@@ -176,6 +176,8 @@ func OpStringFor(node FilterNode, traversalState TraversalState, depth int) stri
 		open += fmt.Sprintf("Left: %s, Right: %s }\n", n.Left.String(), n.Right)
 	case *ContainsSuffixOp:
 		open += fmt.Sprintf("Left: %s, Right: %s }\n", n.Left.String(), n.Right)
+	case *EmptyOp:
+		open += fmt.Sprintf("Left: %s }\n", n.Left.String())
 	default:
 		open += "}\n"
 	}
@@ -207,6 +209,8 @@ func ShortOpStringFor(node FilterNode, traversalState TraversalState) string {
 		open += fmt.Sprintf("%s,%s)", condenseIdent(n.Left), n.Right)
 	case *ContainsSuffixOp:
 		open += fmt.Sprintf("%s,%s)", condenseIdent(n.Left), n.Right)
+	case *EmptyOp:
+		open += fmt.Sprintf("%s)", condenseIdent(n.Left))
 	default:
 		open += ")"
 	}
@@ -350,6 +354,24 @@ func Clone(filter FilterNode) FilterNode {
 				Right: n.Right,
 			}
 
+			if currentOps.Length() == 0 {
+				result = sm
+			} else {
+				currentOps.Top().Add(sm)
+			}
+
+		case *EmptyOp:
+			var field Field
+			if n.Left.Field != nil {
+				field = *n.Left.Field
+			}
+			sm := &EmptyOp{
+				Left: Identifier{
+					Field: &field,
+					Key:   n.Left.Key,
+				},
+			}
+
 			if currentOps.Length() == 0 {
 				result = sm
 			} else {
@@ -393,6 +415,10 @@ func Fields(filter FilterNode) []Field {
 			if n.Left.Field != nil {
 				fields[*n.Left.Field] = true
 			}
+		case *EmptyOp:
+			if n.Left.Field != nil {
+				fields[*n.Left.Field] = true
+			}
 		}
 	})
 

+ 19 - 0
core/pkg/filter/matcher/compiler.go

@@ -171,6 +171,25 @@ func (mc *MatchCompiler[T]) Compile(filter ast.FilterNode) (Matcher[T], error) {
 				sm = mc.stringMatcher.NewStringMatcher(n.Op(), n.Left, n.Right)
 			}
 
+			if currentOps.Length() == 0 {
+				result = sm
+			} else {
+				currentOps.Top().Add(sm)
+			}
+
+		case *ast.EmptyOp:
+			f := n.Left.Field
+			key := n.Left.Key
+
+			var sm Matcher[T]
+			if f.IsSlice() {
+				sm = mc.sliceMatcher.NewStringSliceMatcher(n.Op(), n.Left, "")
+			} else if f.IsMap() && key == "" {
+				sm = mc.mapMatcher.NewStringMapMatcher(n.Op(), n.Left, "")
+			} else {
+				sm = mc.stringMatcher.NewStringMatcher(n.Op(), n.Left, "")
+			}
+
 			if currentOps.Length() == 0 {
 				result = sm
 			} else {

+ 3 - 0
core/pkg/filter/matcher/stringmapmatcher.go

@@ -72,6 +72,9 @@ func (smm *StringMapMatcher[T]) Matches(that T) bool {
 		}
 		return false
 
+	case ast.FilterOpEmpty:
+		return len(thatMap[smm.Key]) == 0
+
 	default:
 		log.Errorf("Filter: StringMapMatcher: Unhandled matcher op. This is a filter implementation error and requires immediate patching. Op: %s", smm.Op)
 		return false

+ 3 - 0
core/pkg/filter/matcher/stringmatcher.go

@@ -66,6 +66,9 @@ func (sm *StringMatcher[T]) Matches(that T) bool {
 	case ast.FilterOpContainsSuffix:
 		return strings.HasSuffix(thatString, sm.Value)
 
+	case ast.FilterOpEmpty:
+		return len(thatString) == 0
+
 	default:
 		log.Errorf("Filter: StringMatcher: Unhandled filter op. This is a filter implementation error and requires immediate patching. Op: %s", sm.Op)
 		return false

+ 3 - 0
core/pkg/filter/matcher/stringslicematcher.go

@@ -82,6 +82,9 @@ func (ssp *StringSliceMatcher[T]) Matches(that T) bool {
 		}
 		return false
 
+	case ast.FilterOpEmpty:
+		return len(thatSlice) == 0
+
 	default:
 		log.Errorf("Filter: StringSliceMatcher: Unhandled filter op. This is a filter implementation error and requires immediate patching. Op: %s", ssp.Op)
 		return false

+ 10 - 0
core/pkg/filter/ops/ops.go

@@ -232,3 +232,13 @@ func ContainsSuffix[T ~string](field T, value string) ast.FilterNode {
 func NotContainsSuffix[T ~string](field T, value string) ast.FilterNode {
 	return Not(ContainsSuffix(field, value))
 }
+
+func Empty[T ~string](field T) ast.FilterNode {
+	return &ast.EmptyOp{
+		Left: identifier(field),
+	}
+}
+
+func NotEmpty[T ~string](field T) ast.FilterNode {
+	return Not(Empty(field))
+}

+ 3 - 0
core/pkg/opencost/allocationmatcher.go

@@ -178,6 +178,9 @@ func (p *allocationAliasPass) Exec(filter ast.FilterNode) (ast.FilterNode, error
 			field = concrete.Left.Field
 			filterValue = concrete.Right
 			filterOp = ast.FilterOpContainsSuffix
+		case *ast.EmptyOp:
+			field = concrete.Left.Field
+			filterOp = ast.FilterOpEmpty
 		default:
 			transformErr = fmt.Errorf("unknown op '%s' during alias pass", concrete.Op())
 			return node