From 7d15e2780e4fdcbe07f25583e7c0f2f263f54d22 Mon Sep 17 00:00:00 2001 From: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:40:55 -0700 Subject: [PATCH 1/3] Add negation to the grammar --- 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 +++++++++++++++++++++++ 6 files changed, 214 insertions(+), 7 deletions(-) diff --git a/pkg/ottl/README.md b/pkg/ottl/README.md index c380e16bb091..ce532504cd12 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 `and` Boolean Expressions have higher precedence than `or` and `not` has the highest precedence. 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 From fd76b802e0d56338442feb598a24c4073a2deede Mon Sep 17 00:00:00 2001 From: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com> Date: Wed, 30 Nov 2022 13:12:25 -0700 Subject: [PATCH 2/3] changelog --- .chloggen/ottl-not.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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: From 2ca47498c0926d18532be656cbad19ffe9a9a089 Mon Sep 17 00:00:00 2001 From: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com> Date: Thu, 1 Dec 2022 08:46:23 -0700 Subject: [PATCH 3/3] Reword --- pkg/ottl/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/ottl/README.md b/pkg/ottl/README.md index ce532504cd12..97fdff6f4b4d 100644 --- a/pkg/ottl/README.md +++ b/pkg/ottl/README.md @@ -137,7 +137,7 @@ 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`. Booleans can be negated with the literal string `not`. -Note that `and` Boolean Expressions have higher precedence than `or` and `not` has the highest precedence. +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