From 80f76c21dd41bb9401a0dcd4688687eb11549df2 Mon Sep 17 00:00:00 2001 From: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com> Date: Fri, 2 Dec 2022 09:25:32 -0700 Subject: [PATCH] [pkg/ottl] Add negation to the grammar (#16553) * Add negation to the grammar * changelog * Reword --- .chloggen/ottl-not.yaml | 16 ++++++ pkg/ottl/README.md | 8 ++- pkg/ottl/boolean_value.go | 30 ++++++++-- pkg/ottl/boolean_value_test.go | 102 +++++++++++++++++++++++++++++++++ pkg/ottl/grammar.go | 2 + pkg/ottl/lexer_test.go | 6 ++ pkg/ottl/parser_test.go | 73 +++++++++++++++++++++++ 7 files changed, 230 insertions(+), 7 deletions(-) create mode 100755 .chloggen/ottl-not.yaml diff --git a/.chloggen/ottl-not.yaml b/.chloggen/ottl-not.yaml new file mode 100755 index 000000000000..a0e4d931baf0 --- /dev/null +++ b/.chloggen/ottl-not.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add ability to negate conditions with the `not` keyword + +# One or more tracking issues related to the change +issues: [16553] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/pkg/ottl/README.md b/pkg/ottl/README.md index c380e16bb091..97fdff6f4b4d 100644 --- a/pkg/ottl/README.md +++ b/pkg/ottl/README.md @@ -136,7 +136,8 @@ Boolean Expressions allow a decision to be made about whether an Invocation shou Boolean Expressions consist of the literal string `where` followed by one or more Booleans (see below). Booleans can be joined with the literal strings `and` and `or`. -Note that `and` Boolean Expressions have higher precedence than `or`. +Booleans can be negated with the literal string `not`. +Note that `not` has the highest precedence and `and` Boolean Expressions have higher precedence than `or`. Boolean Expressions can be grouped with parentheses to override evaluation precedence. ### Booleans @@ -156,6 +157,11 @@ The valid operators are: - Less Than or Equal To (`<=`). Tests if left is less than or equal to right. - Greater Than or Equal to (`>=`). Tests if left is greater than or equal to right. +Booleans can be negated with the `not` keyword such as +- `not true` +- `not name == "foo"` + `not (IsMatch(name, "http_.*") == true and kind > 0)` + ### Comparison Rules The table below describes what happens when two Values are compared. Value types are provided by the user of OTTL. All of the value types supported by OTTL are listed in this table. diff --git a/pkg/ottl/boolean_value.go b/pkg/ottl/boolean_value.go index 52c3b09a7481..2f166b793e45 100644 --- a/pkg/ottl/boolean_value.go +++ b/pkg/ottl/boolean_value.go @@ -30,6 +30,13 @@ func (e BoolExpr[K]) Eval(ctx context.Context, tCtx K) (bool, error) { return e.boolExpressionEvaluator(ctx, tCtx) } +func not[K any](original BoolExpr[K]) (BoolExpr[K], error) { + return BoolExpr[K]{func(ctx context.Context, tCtx K) (bool, error) { + result, err := original.Eval(ctx, tCtx) + return !result, err + }}, nil +} + func alwaysTrue[K any](context.Context, K) (bool, error) { return true, nil } @@ -144,21 +151,32 @@ func (p *Parser[K]) newBooleanValueEvaluator(value *booleanValue) (BoolExpr[K], if value == nil { return BoolExpr[K]{alwaysTrue[K]}, nil } + + var boolExpr BoolExpr[K] + var err error switch { case value.Comparison != nil: - comparison, err := p.newComparisonEvaluator(value.Comparison) + boolExpr, err = p.newComparisonEvaluator(value.Comparison) if err != nil { return BoolExpr[K]{}, err } - return comparison, nil case value.ConstExpr != nil: if *value.ConstExpr { - return BoolExpr[K]{alwaysTrue[K]}, nil + boolExpr = BoolExpr[K]{alwaysTrue[K]} + } else { + boolExpr = BoolExpr[K]{alwaysFalse[K]} } - return BoolExpr[K]{alwaysFalse[K]}, nil case value.SubExpr != nil: - return p.newBoolExpr(value.SubExpr) + boolExpr, err = p.newBoolExpr(value.SubExpr) + if err != nil { + return BoolExpr[K]{}, err + } + default: + return BoolExpr[K]{}, fmt.Errorf("unhandled boolean operation %v", value) } - return BoolExpr[K]{}, fmt.Errorf("unhandled boolean operation %v", value) + if value.Negation != nil { + return not(boolExpr) + } + return boolExpr, nil } diff --git a/pkg/ottl/boolean_value_test.go b/pkg/ottl/boolean_value_test.go index ad77179f6ae4..af914d80f8fc 100644 --- a/pkg/ottl/boolean_value_test.go +++ b/pkg/ottl/boolean_value_test.go @@ -351,6 +351,108 @@ func Test_newBooleanExpressionEvaluator(t *testing.T) { }, }, }, + {"i", true, + &booleanExpression{ + Left: &term{ + Left: &booleanValue{ + Negation: ottltest.Strp("not"), + ConstExpr: booleanp(false), + }, + }, + }, + }, + {"j", false, + &booleanExpression{ + Left: &term{ + Left: &booleanValue{ + Negation: ottltest.Strp("not"), + ConstExpr: booleanp(true), + }, + }, + }, + }, + {"k", true, + &booleanExpression{ + Left: &term{ + Left: &booleanValue{ + Negation: ottltest.Strp("not"), + Comparison: &comparison{ + Left: value{ + String: ottltest.Strp("test"), + }, + Op: EQ, + Right: value{ + String: ottltest.Strp("not test"), + }, + }, + }, + }, + }, + }, + {"l", false, + &booleanExpression{ + Left: &term{ + Left: &booleanValue{ + ConstExpr: booleanp(true), + }, + Right: []*opAndBooleanValue{ + { + Operator: "and", + Value: &booleanValue{ + Negation: ottltest.Strp("not"), + SubExpr: &booleanExpression{ + Left: &term{ + Left: &booleanValue{ + ConstExpr: booleanp(true), + }, + }, + Right: []*opOrTerm{ + { + Operator: "or", + Term: &term{ + Left: &booleanValue{ + ConstExpr: booleanp(false), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + {"m", false, + &booleanExpression{ + Left: &term{ + Left: &booleanValue{ + Negation: ottltest.Strp("not"), + ConstExpr: booleanp(true), + }, + Right: []*opAndBooleanValue{ + { + Operator: "and", + Value: &booleanValue{ + Negation: ottltest.Strp("not"), + ConstExpr: booleanp(false), + }, + }, + }, + }, + Right: []*opOrTerm{ + { + Operator: "or", + Term: &term{ + Left: &booleanValue{ + Negation: ottltest.Strp("not"), + ConstExpr: booleanp(true), + }, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/ottl/grammar.go b/pkg/ottl/grammar.go index 397eca5cb3bb..141fac76a772 100644 --- a/pkg/ottl/grammar.go +++ b/pkg/ottl/grammar.go @@ -30,6 +30,7 @@ type parsedStatement struct { // either an equality or inequality, explicit true or false, or // a parenthesized subexpression. type booleanValue struct { + Negation *string `parser:"@OpNot?"` Comparison *comparison `parser:"( @@"` ConstExpr *boolean `parser:"| @Boolean"` SubExpr *booleanExpression `parser:"| '(' @@ ')' )"` @@ -266,6 +267,7 @@ func buildLexer() *lexer.StatefulDefinition { {Name: `Float`, Pattern: `[-+]?\d*\.\d+([eE][-+]?\d+)?`}, {Name: `Int`, Pattern: `[-+]?\d+`}, {Name: `String`, Pattern: `"(\\"|[^"])*"`}, + {Name: `OpNot`, Pattern: `\b(not)\b`}, {Name: `OpOr`, Pattern: `\b(or)\b`}, {Name: `OpAnd`, Pattern: `\b(and)\b`}, {Name: `OpComparison`, Pattern: `==|!=|>=|<=|>|<`}, diff --git a/pkg/ottl/lexer_test.go b/pkg/ottl/lexer_test.go index 32dca17ffd3a..58272e0d2e15 100644 --- a/pkg/ottl/lexer_test.go +++ b/pkg/ottl/lexer_test.go @@ -83,6 +83,12 @@ func Test_lexer(t *testing.T) { {"OpOr", "or"}, {"Lowercase", "but"}, }}, + {"not", "true and not false", false, []result{ + {"Boolean", "true"}, + {"OpAnd", "and"}, + {"OpNot", "not"}, + {"Boolean", "false"}, + }}, {"nothing_recognizable", "{}", true, []result{ {"", ""}, }}, diff --git a/pkg/ottl/parser_test.go b/pkg/ottl/parser_test.go index 0ff928cc60dc..d8cfc2088b32 100644 --- a/pkg/ottl/parser_test.go +++ b/pkg/ottl/parser_test.go @@ -1116,6 +1116,79 @@ func Test_parseWhere(t *testing.T) { }, }), }, + { + statement: `true and not false`, + expected: setNameTest(&booleanExpression{ + Left: &term{ + Left: &booleanValue{ + ConstExpr: booleanp(true), + }, + Right: []*opAndBooleanValue{ + { + Operator: "and", + Value: &booleanValue{ + Negation: ottltest.Strp("not"), + ConstExpr: booleanp(false), + }, + }, + }, + }, + }), + }, + { + statement: `not name == "bar"`, + expected: setNameTest(&booleanExpression{ + Left: &term{ + Left: &booleanValue{ + Negation: ottltest.Strp("not"), + Comparison: &comparison{ + Left: value{ + Literal: &mathExprLiteral{ + Path: &Path{ + Fields: []Field{ + { + Name: "name", + }, + }, + }, + }, + }, + Op: EQ, + Right: value{ + String: ottltest.Strp("bar"), + }, + }, + }, + }, + }), + }, + { + statement: `not (true or false)`, + expected: setNameTest(&booleanExpression{ + Left: &term{ + Left: &booleanValue{ + Negation: ottltest.Strp("not"), + SubExpr: &booleanExpression{ + Left: &term{ + Left: &booleanValue{ + ConstExpr: booleanp(true), + }, + }, + Right: []*opOrTerm{ + { + Operator: "or", + Term: &term{ + Left: &booleanValue{ + ConstExpr: booleanp(false), + }, + }, + }, + }, + }, + }, + }, + }), + }, } // create a test name that doesn't confuse vscode so we can rerun tests with one click