diff --git a/nmpolicy/internal/ast/types.go b/nmpolicy/internal/ast/types.go index b4c82c49..a2c2ce15 100644 --- a/nmpolicy/internal/ast/types.go +++ b/nmpolicy/internal/ast/types.go @@ -31,6 +31,7 @@ type Terminal struct { type Node struct { Meta EqFilter *TernaryOperator `json:"eqfilter,omitempty"` + Replace *TernaryOperator `json:"replace,omitempty"` Path *VariadicOperator `json:"path,omitempty"` Terminal } diff --git a/nmpolicy/internal/parser/errors.go b/nmpolicy/internal/parser/errors.go index 02f5af63..259a4819 100644 --- a/nmpolicy/internal/parser/errors.go +++ b/nmpolicy/internal/parser/errors.go @@ -65,6 +65,13 @@ func invalidEqualityFilterError(msg string) *parserError { } } +func invalidReplaceError(msg string) *parserError { + return &parserError{ + prefix: "invalid replace", + msg: msg, + } +} + func invalidExpressionError(msg string) *parserError { return &parserError{ prefix: "invalid expression", diff --git a/nmpolicy/internal/parser/parser.go b/nmpolicy/internal/parser/parser.go index 1685762d..b32cf0c8 100644 --- a/nmpolicy/internal/parser/parser.go +++ b/nmpolicy/internal/parser/parser.go @@ -71,6 +71,10 @@ func (p *parser) parse() (ast.Node, error) { if err := p.parseEqFilter(); err != nil { return ast.Node{}, err } + } else if p.currentToken().Type == lexer.REPLACE { + if err := p.parseReplace(); err != nil { + return ast.Node{}, err + } } else { return ast.Node{}, invalidExpressionError(fmt.Sprintf("unexpected token `%+v`", p.currentToken().Literal)) } @@ -165,7 +169,7 @@ func (p *parser) parsePath() error { } path := append(*operator.Path, *p.lastNode) operator.Path = &path - } else if p.currentToken().Type != lexer.EOF && p.currentToken().Type != lexer.EQFILTER { + } else if p.currentToken().Type != lexer.EOF && p.currentToken().Type != lexer.EQFILTER && p.currentToken().Type != lexer.REPLACE { return invalidPathError("missing dot") } else { // Token has not being consumed let's go back. @@ -204,9 +208,40 @@ func (p *parser) parseEqFilter() error { return err } operator.EqFilter[2] = *p.lastNode - } else if p.currentToken().Type != lexer.EOF { + } else if p.currentToken().Type == lexer.EOF { + return invalidEqualityFilterError("missing right hand argument") + } else { return invalidEqualityFilterError("right hand argument is not a string or identity") } p.lastNode = operator return nil } + +func (p *parser) parseReplace() error { + operator := &ast.Node{ + Meta: ast.Meta{Position: p.currentToken().Position}, + Replace: &ast.TernaryOperator{}, + } + if p.lastNode == nil { + return invalidReplaceError("missing left hand argument") + } + if p.lastNode.Path == nil { + return invalidReplaceError("left hand argument is not a path") + } + operator.Replace[0].Terminal = ast.CurrentStateIdentity() + operator.Replace[1] = *p.lastNode + + p.nextToken() + if p.currentToken().Type == lexer.STRING { + if err := p.parseString(); err != nil { + return err + } + operator.Replace[2] = *p.lastNode + } else if p.currentToken().Type == lexer.EOF { + return invalidReplaceError("missing right hand argument") + } else { + return invalidReplaceError("right hand argument is not a string") + } + p.lastNode = operator + return nil +} diff --git a/nmpolicy/internal/parser/parser_test.go b/nmpolicy/internal/parser/parser_test.go index c5a7fc3e..7f1ca67d 100644 --- a/nmpolicy/internal/parser/parser_test.go +++ b/nmpolicy/internal/parser/parser_test.go @@ -29,12 +29,19 @@ import ( ) func TestParser(t *testing.T) { - testParseFailures(t) - testParseSuccess(t) + testParsePath(t) + testParseEqFilter(t) + testParseReplace(t) + + testParseBasicFailures(t) + testParsePathFailures(t) + testParseEqFilterFailure(t) + testParseReplaceFailure(t) + testParserReuse(t) } -func testParseFailures(t *testing.T) { +func testParseBasicFailures(t *testing.T) { var tests = []test{ expectError("invalid expression: unexpected token `.`", fromTokens( @@ -42,6 +49,12 @@ func testParseFailures(t *testing.T) { eof(), ), ), + } + runTest(t, tests) +} + +func testParsePathFailures(t *testing.T) { + var tests = []test{ expectError(`invalid path: missing identity or number after dot`, fromTokens( identity("routes"), @@ -65,6 +78,12 @@ func testParseFailures(t *testing.T) { eof(), ), ), + } + runTest(t, tests) +} + +func testParseEqFilterFailure(t *testing.T) { + var tests = []test{ expectError(`invalid equality filter: missing left hand argument`, fromTokens( eqfilter(), @@ -80,6 +99,18 @@ func testParseFailures(t *testing.T) { eof(), ), ), + expectError(`invalid equality filter: missing right hand argument`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("destination"), + eqfilter(), + eof(), + ), + ), + expectError(`invalid equality filter: right hand argument is not a string or identity`, fromTokens( identity("routes"), @@ -96,7 +127,52 @@ func testParseFailures(t *testing.T) { runTest(t, tests) } -func testParseSuccess(t *testing.T) { +func testParseReplaceFailure(t *testing.T) { + var tests = []test{ + expectError(`invalid replace: missing left hand argument`, + fromTokens( + replace(), + str("0.0.0.0/0"), + eof(), + ), + ), + expectError(`invalid replace: left hand argument is not a path`, + fromTokens( + str("foo"), + replace(), + str("0.0.0.0/0"), + eof(), + ), + ), + expectError(`invalid replace: missing right hand argument`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("destination"), + replace(), + eof(), + ), + ), + + expectError(`invalid replace: right hand argument is not a string`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("destination"), + replace(), + replace(), + eof(), + ), + ), + } + runTest(t, tests) +} + +func testParsePath(t *testing.T) { var tests = []test{ expectEmptyAST(fromTokens()), expectEmptyAST(fromTokens(eof())), @@ -118,6 +194,12 @@ path: eof(), ), ), + } + runTest(t, tests) +} + +func testParseEqFilter(t *testing.T) { + var tests = []test{ expectAST(t, ` pos: 26 eqfilter: @@ -193,6 +275,39 @@ eqfilter: runTest(t, tests) } +func testParseReplace(t *testing.T) { + var tests = []test{ + expectAST(t, ` +pos: 33 +replace: +- pos: 0 + identity: currentState +- pos: 0 + path: + - pos: 0 + identity: routes + - pos: 7 + identity: running + - pos: 15 + identity: next-hop-interface +- pos: 35 + string: br1 +`, + fromTokens( + identity("routes"), + dot(), + identity("running"), + dot(), + identity("next-hop-interface"), + replace(), + str("br1"), + eof(), + ), + ), + } + runTest(t, tests) +} + func testParserReuse(t *testing.T) { p := parser.New() testToRun1 := expectAST(t, ` @@ -333,3 +448,7 @@ func eof() lexer.Token { func eqfilter() lexer.Token { return lexer.Token{Type: lexer.EQFILTER, Literal: "=="} } + +func replace() lexer.Token { + return lexer.Token{Type: lexer.REPLACE, Literal: ":="} +}