Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

operators: Add inequality operator #115

Merged
merged 1 commit into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ linters-settings:
- whyNoLint
- wrapperFunc
gocyclo:
min-complexity: 17
min-complexity: 30
goheader:
values:
regexp:
Expand Down
4 changes: 4 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ It uses the `description` field to filter between primary and secondary NIC.

{% include_relative examples/example.md example="all-ethernet-up" %}

## Turn LLDP to true at non ethernet interfaces

{% include_relative examples/example.md example="non-ethernet-lldp-enabled" %}

## Create a linux-bridge with all the interfaces matching description

{% include_relative examples/example.md example="bridge-interfaces-by-description" %}
4 changes: 4 additions & 0 deletions nmpolicy/internal/ast/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Terminal struct {
type Node struct {
Meta
EqFilter *TernaryOperator `json:"eqfilter,omitempty"`
NeFilter *TernaryOperator `json:"nefilter,omitempty"`
Replace *TernaryOperator `json:"replace,omitempty"`
Path *VariadicOperator `json:"path,omitempty"`
Terminal
Expand All @@ -43,6 +44,9 @@ func (n Node) String() string {
if n.EqFilter != nil {
return fmt.Sprintf("EqFilter(%s)", *n.EqFilter)
}
if n.NeFilter != nil {
return fmt.Sprintf("NeFilter(%s)", *n.NeFilter)
}
if n.Replace != nil {
return fmt.Sprintf("Replace(%s)", *n.Replace)
}
Expand Down
2 changes: 2 additions & 0 deletions nmpolicy/internal/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func (l *lexer) lexCurrentRune() (*Token, error) {
return l.lexEqualAs(REPLACE)
} else if l.isEqual() {
return l.lexEqualAs(EQFILTER)
} else if l.isExclamationMark() {
return l.lexEqualAs(NEFILTER)
} else if l.isPlus() {
return &Token{l.scn.Position(), MERGE, string(l.scn.Rune())}, nil
} else if l.isPipe() {
Expand Down
6 changes: 4 additions & 2 deletions nmpolicy/internal/lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestLexer(t *testing.T) {
testLinuxBridgeAtDefaultGwScenario(t)
}

//nolint:dupl
func testBasicExpressions(t *testing.T) {
t.Run("basic expressions", func(t *testing.T) {
runTest(t, []test{
Expand Down Expand Up @@ -97,7 +98,7 @@ func testBasicExpressions(t *testing.T) {
{47, lexer.IDENTITY, "doo3"},
{50, lexer.EOF, ""}},
}},
{" . foo1.dar1:=foo2 . dar2 ... moo3+boo3|doo3 == := :=", expected{tokens: []lexer.Token{
{" . foo1.dar1:=foo2 . dar2 ... moo3+boo3|doo3 == := := !=", expected{tokens: []lexer.Token{
{1, lexer.DOT, "."},
{3, lexer.IDENTITY, "foo1"},
{7, lexer.DOT, "."},
Expand All @@ -117,7 +118,8 @@ func testBasicExpressions(t *testing.T) {
{45, lexer.EQFILTER, "=="},
{48, lexer.REPLACE, ":="},
{51, lexer.REPLACE, ":="},
{52, lexer.EOF, ""}},
{54, lexer.NEFILTER, "!="},
{55, lexer.EOF, ""}},
}},
{"foo1.3|foo2", expected{tokens: []lexer.Token{
{0, lexer.IDENTITY, "foo1"},
Expand Down
6 changes: 5 additions & 1 deletion nmpolicy/internal/lexer/rune.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func (l *lexer) isPipe() bool {
return l.scn.Rune() == '|'
}

func (l *lexer) isExclamationMark() bool {
return l.scn.Rune() == '!'
}

func (l *lexer) isDelimiter() bool {
return l.isEOF() || l.isSpace() || l.isDot() || l.isEqual() || l.isColon() || l.isPlus() || l.isPipe()
return l.isEOF() || l.isSpace() || l.isDot() || l.isEqual() || l.isColon() || l.isPlus() || l.isPipe() || l.isExclamationMark()
}
2 changes: 2 additions & 0 deletions nmpolicy/internal/lexer/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
PIPE // |
REPLACE // :=
EQFILTER // ==
NEFILTER // !=
MERGE // +
operatorsEnd
)
Expand All @@ -47,6 +48,7 @@ var tokens = []string{

REPLACE: "REPLACE",
EQFILTER: "EQFILTER",
NEFILTER: "NEFILTER",
MERGE: "MERGE",
}

Expand Down
7 changes: 7 additions & 0 deletions nmpolicy/internal/parser/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ func wrapWithInvalidEqualityFilterError(err error) *parserError {
}
}

func wrapWithInvalidInequalityFilterError(err error) *parserError {
return &parserError{
prefix: "invalid inequality filter",
inner: err,
}
}

func wrapWithInvalidReplaceError(err error) *parserError {
return &parserError{
prefix: "invalid replace",
Expand Down
16 changes: 16 additions & 0 deletions nmpolicy/internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ func (p *parser) parse() (ast.Node, error) {
if err := p.parseEqFilter(); err != nil {
return ast.Node{}, err
}
} else if p.currentToken().Type == lexer.NEFILTER {
if err := p.parseNeFilter(); err != nil {
return ast.Node{}, err
}
} else if p.currentToken().Type == lexer.REPLACE {
if err := p.parseReplace(); err != nil {
return ast.Node{}, err
Expand Down Expand Up @@ -227,6 +231,18 @@ func (p *parser) parseEqFilter() error {
return nil
}

func (p *parser) parseNeFilter() error {
operator := &ast.Node{
Meta: ast.Meta{Position: p.currentToken().Position},
NeFilter: &ast.TernaryOperator{},
}
if err := p.fillInTernaryOperator(operator.NeFilter); err != nil {
return wrapWithInvalidInequalityFilterError(err)
}
p.lastNode = operator
return nil
}

func (p *parser) parseReplace() error {
operator := &ast.Node{
Meta: ast.Meta{Position: p.currentToken().Position},
Expand Down
136 changes: 136 additions & 0 deletions nmpolicy/internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ import (
func TestParser(t *testing.T) {
testParsePath(t)
testParseEqFilter(t)
testParseNeFilter(t)
testParseReplace(t)
testParseReplaceWithPath(t)
testParseCapturePipeReplace(t)

testParseBasicFailures(t)
testParsePathFailures(t)
testParseEqFilterFailure(t)
testParseNeFilterFailure(t)
testParseReplaceFailure(t)

testParserReuse(t)
Expand Down Expand Up @@ -188,6 +190,59 @@ func testParseEqFilterFailure(t *testing.T) {
runTest(t, tests)
}

func testParseNeFilterFailure(t *testing.T) {
var tests = []test{
expectError(`invalid inequality filter: missing left hand argument
| !=0.0.0.0/0
| ^`,
fromTokens(
nefilter(),
str("0.0.0.0/0"),
eof(),
),
),
expectError(`invalid inequality filter: left hand argument is not a path
| foo!=0.0.0.0/0
| ...^`,
fromTokens(
str("foo"),
nefilter(),
str("0.0.0.0/0"),
eof(),
),
),
expectError(`invalid inequality filter: missing right hand argument
| routes.running.destination!=
| ...........................^`,
fromTokens(
identity("routes"),
dot(),
identity("running"),
dot(),
identity("destination"),
nefilter(),
eof(),
),
),

expectError(`invalid inequality filter: right hand argument is not a string or identity
| routes.running.destination!=!=
| ............................^`,
fromTokens(
identity("routes"),
dot(),
identity("running"),
dot(),
identity("destination"),
nefilter(),
nefilter(),
eof(),
),
),
}
runTest(t, tests)
}

func testParseReplaceFailure(t *testing.T) {
var tests = []test{
expectError(`invalid replace: missing left hand argument
Expand Down Expand Up @@ -344,6 +399,83 @@ eqfilter:
runTest(t, tests)
}

func testParseNeFilter(t *testing.T) {
var tests = []test{
expectAST(t, `
pos: 26
nefilter:
- pos: 0
identity: currentState
- pos: 0
path:
- pos: 0
identity: routes
- pos: 7
identity: running
- pos: 15
identity: destination
- pos: 28
string: 0.0.0.0/0`,
fromTokens(
identity("routes"),
dot(),
identity("running"),
dot(),
identity("destination"),
nefilter(),
str("0.0.0.0/0"),
eof(),
),
),
expectAST(t, `
pos: 33
nefilter:
- pos: 0
identity: currentState
- pos: 0
path:
- pos: 0
identity: routes
- pos: 7
identity: running
- pos: 15
identity: next-hop-interface
- pos: 35
path:
- pos: 35
identity: capture
- pos: 43
identity: default-gw
- pos: 54
identity: routes
- pos: 61
number: 0
- pos: 63
identity: next-hop-interface
`,
fromTokens(
identity("routes"),
dot(),
identity("running"),
dot(),
identity("next-hop-interface"),
nefilter(),
identity("capture"),
dot(),
identity("default-gw"),
dot(),
identity("routes"),
dot(),
number(0),
dot(),
identity("next-hop-interface"),
eof(),
),
),
}
runTest(t, tests)
}

func testParseReplace(t *testing.T) {
var tests = []test{
expectAST(t, `
Expand Down Expand Up @@ -641,6 +773,10 @@ func eqfilter() lexer.Token {
return lexer.Token{Type: lexer.EQFILTER, Literal: "=="}
}

func nefilter() lexer.Token {
return lexer.Token{Type: lexer.NEFILTER, Literal: "!="}
}

func replace() lexer.Token {
return lexer.Token{Type: lexer.REPLACE, Literal: ":="}
}
Expand Down
4 changes: 4 additions & 0 deletions nmpolicy/internal/resolver/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ func wrapWithEqFilterError(err error) error {
return fmt.Errorf("eqfilter error: %w", err)
}

func wrapWithNeFilterError(err error) error {
return fmt.Errorf("nefilter error: %w", err)
}

func replaceError(format string, a ...interface{}) error {
return wrapWithReplaceError(fmt.Errorf(format, a...))
}
Expand Down
31 changes: 27 additions & 4 deletions nmpolicy/internal/resolver/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@ import (
"github.com/nmstate/nmpolicy/nmpolicy/internal/ast"
)

func filter(inputState map[string]interface{}, pathSteps ast.VariadicOperator, expectedValue interface{}) (map[string]interface{}, error) {
filtered, err := visitState(newPath(pathSteps), inputState, &filterVisitor{expectedValue: expectedValue})
func filter(
inputState map[string]interface{},
pathSteps ast.VariadicOperator,
operator func(interface{}, interface{}) bool,
expectedValue interface{}) (map[string]interface{}, error) {
filtered, err := visitState(newPath(pathSteps), inputState, &filterVisitor{
operator: operator,
expectedValue: expectedValue,
})

if err != nil {
return nil, fmt.Errorf("failed applying operation on the path: %w", err)
Expand All @@ -40,9 +47,22 @@ func filter(inputState map[string]interface{}, pathSteps ast.VariadicOperator, e
}
return filteredMap, nil
}
func eqfilter(
inputState map[string]interface{},
pathSteps ast.VariadicOperator,
expectedValue interface{}) (map[string]interface{}, error) {
return filter(inputState, pathSteps, func(lhs, rhs interface{}) bool { return lhs == rhs }, expectedValue)
}
func nefilter(
inputState map[string]interface{},
pathSteps ast.VariadicOperator,
expectedValue interface{}) (map[string]interface{}, error) {
return filter(inputState, pathSteps, func(lhs, rhs interface{}) bool { return lhs != rhs }, expectedValue)
}

type filterVisitor struct {
mergeVisitResult bool
operator func(interface{}, interface{}) bool
expectedValue interface{}
}

Expand All @@ -61,7 +81,7 @@ func (e filterVisitor) visitLastMap(p path, mapToFilter map[string]interface{})
return nil, pathError(p.currentStep, `type missmatch: the value in the path doesn't match the value to filter. `+
`"%T" != "%T" -> %+v != %+v`, obtainedValue, e.expectedValue, obtainedValue, e.expectedValue)
}
if obtainedValue == e.expectedValue {
if e.operator(obtainedValue, e.expectedValue) {
return mapToFilter, nil
}
return nil, nil
Expand Down Expand Up @@ -109,7 +129,10 @@ func (e filterVisitor) visitSlice(p path, sliceToVisit []interface{}) (interface
for _, interfaceToVisit := range sliceToVisit {
// Filter only the first slice by forcing "mergeVisitResult" to true
// for the the following ones.
visitResult, err := visitState(p, interfaceToVisit, &filterVisitor{mergeVisitResult: true, expectedValue: e.expectedValue})
visitResult, err := visitState(p, interfaceToVisit, &filterVisitor{
mergeVisitResult: true,
operator: e.operator,
expectedValue: e.expectedValue})
if err != nil {
return nil, err
}
Expand Down
Loading