diff --git a/pkg/dsl/parser/parser.go b/pkg/dsl/parser/parser.go index 64af42001..33a96d707 100644 --- a/pkg/dsl/parser/parser.go +++ b/pkg/dsl/parser/parser.go @@ -33,6 +33,8 @@ type Parser struct { l *lexer.Lexer // the current token being processed currentToken token.Token + // the token before currentToken + previousToken token.Token // the next token after currentToken peekToken token.Token // a slice of error messages that are generated during parsing @@ -82,6 +84,8 @@ func (p *Parser) next() { peek := p.l.NextToken() // if the token is not an ignored token (e.g. whitespace or comments), update the currentToken and peekToken fields and exit the loop if !token.IsIgnores(peek.Type) { + // set the previousToken before changing currentToken + p.previousToken = p.currentToken // set the currentToken field to the previous peekToken value p.currentToken = p.peekToken // set the peekToken field to the new peek value @@ -118,6 +122,18 @@ func (p *Parser) currentTokenIs(tokens ...token.Type) bool { return false } +// previousTokenIs checks if the Parser's previousToken type is any of the given types +func (p *Parser) previousTokenIs(tokens ...token.Type) bool { + for _, t := range tokens { + if p.previousToken.Type == t { + // if a match is found, return true + return true + } + } + // if no match is found, return false + return false +} + // peekTokenIs checks if the Parser's peekToken is any of the given token types func (p *Parser) peekTokenIs(tokens ...token.Type) bool { // iterate through the given token types and check if any of them match the peekToken's type @@ -588,6 +604,11 @@ func (p *Parser) parseExpression(precedence int) (ast.Expression, error) { var exp ast.Expression var err error + if p.currentTokenIs(token.NEWLINE) && p.previousTokenIs(token.LP, token.AND, token.OR, token.NOT, token.ASSIGN) { + // advance to the next token + p.next() + } + if p.currentTokenIs(token.LP) { p.next() // Consume the left parenthesis. exp, err = p.parseExpression(LOWEST) diff --git a/pkg/dsl/parser/parser_test.go b/pkg/dsl/parser/parser_test.go index f066c15af..df6e5a521 100644 --- a/pkg/dsl/parser/parser_test.go +++ b/pkg/dsl/parser/parser_test.go @@ -849,5 +849,197 @@ var _ = Describe("parser", func() { Expect(es.Expression.(*ast.InfixExpression).Right.(*ast.InfixExpression).Left.(*ast.Identifier).String()).Should(Equal("parent.admin")) Expect(es.Expression.(*ast.InfixExpression).Right.(*ast.InfixExpression).Right.(*ast.Identifier).String()).Should(Equal("parent.member")) }) + + It("Case 24 - Multi-line Permission Expression w/ Rule", func() { + pr := NewParser(` + entity account { + relation owner @user + attribute balance float + + permission withdraw = check_balance(request.amount, balance) and + owner + } + + rule check_balance(amount float, balance float) { + balance >= amount && amount <= 5000 + } + `) + + schema, err := pr.Parse() + Expect(err).ShouldNot(HaveOccurred()) + + st := schema.Statements[0].(*ast.EntityStatement) + + Expect(st.Name.Literal).Should(Equal("account")) + + r1 := st.RelationStatements[0].(*ast.RelationStatement) + Expect(r1.Name.Literal).Should(Equal("owner")) + + for _, a := range r1.RelationTypes { + Expect(a.Type.Literal).Should(Equal("user")) + } + + a1 := st.AttributeStatements[0].(*ast.AttributeStatement) + Expect(a1.Name.Literal).Should(Equal("balance")) + Expect(a1.AttributeType.Type.Literal).Should(Equal("float")) + + p1 := st.PermissionStatements[0].(*ast.PermissionStatement) + Expect(p1.Name.Literal).Should(Equal("withdraw")) + + es1 := p1.ExpressionStatement.(*ast.ExpressionStatement) + + Expect(es1.Expression.(*ast.InfixExpression).Left.(*ast.Call).String()).Should(Equal("check_balance(request.amount, balance)")) + Expect(es1.Expression.(*ast.InfixExpression).Right.(*ast.Identifier).String()).Should(Equal("owner")) + + rs1 := schema.Statements[1].(*ast.RuleStatement) + + Expect(rs1.Name.Literal).Should(Equal("check_balance")) + Expect(rs1.Expression).Should(Equal("\nbalance >= amount && amount <= 5000\n\t\t")) + }) + + It("Case 25 - Multi-line Permission Expression w/ Rule", func() { + pr := NewParser(` + entity account { + relation owner @user + attribute balance float + + permission withdraw = + check_balance(request.amount, balance) and owner + } + + rule check_balance(amount float, balance float) { + balance >= amount && amount <= 5000 + } + `) + + schema, err := pr.Parse() + Expect(err).ShouldNot(HaveOccurred()) + + st := schema.Statements[0].(*ast.EntityStatement) + + Expect(st.Name.Literal).Should(Equal("account")) + + r1 := st.RelationStatements[0].(*ast.RelationStatement) + Expect(r1.Name.Literal).Should(Equal("owner")) + + for _, a := range r1.RelationTypes { + Expect(a.Type.Literal).Should(Equal("user")) + } + + a1 := st.AttributeStatements[0].(*ast.AttributeStatement) + Expect(a1.Name.Literal).Should(Equal("balance")) + Expect(a1.AttributeType.Type.Literal).Should(Equal("float")) + + p1 := st.PermissionStatements[0].(*ast.PermissionStatement) + Expect(p1.Name.Literal).Should(Equal("withdraw")) + + es1 := p1.ExpressionStatement.(*ast.ExpressionStatement) + + Expect(es1.Expression.(*ast.InfixExpression).Left.(*ast.Call).String()).Should(Equal("check_balance(request.amount, balance)")) + Expect(es1.Expression.(*ast.InfixExpression).Right.(*ast.Identifier).String()).Should(Equal("owner")) + + rs1 := schema.Statements[1].(*ast.RuleStatement) + + Expect(rs1.Name.Literal).Should(Equal("check_balance")) + Expect(rs1.Expression).Should(Equal("\nbalance >= amount && amount <= 5000\n\t\t")) + }) + + It("Case 26 - Multi-line Permission Expression w/ Rule - should fail", func() { + pr := NewParser(` + entity account { + relation owner @user + attribute balance float + + permission withdraw = check_balance(request.amount, balance) + owner + } + + rule check_balance(amount float, balance float) { + balance >= amount && amount <= 5000 + } + `) + + _, err := pr.Parse() + Expect(err).Should(HaveOccurred()) + + // Ensure an error is returned + Expect(err).Should(HaveOccurred()) + + // Ensure the error message contains the expected string + Expect(err.Error()).Should(ContainSubstring("8:2:expected token to be RELATION, PERMISSION, ATTRIBUTE, got IDENT instead")) + }) + + It("Case 27 - Multi-line Permission Complex Expression w/ Rule", func() { + pr := NewParser(` +entity report { + relation parent @organization + relation team @team + attribute confidentiality_level double + + permission view = + confidentiality_level_high(confidentiality_level) and + parent.director or + confidentiality_level_medium_high(confidentiality_level) and + (parent.director or team.lead) or + confidentiality_level_medium(confidentiality_level) and (team.lead or team.member) or + confidentiality_level_low(confidentiality_level) and + parent.member + permission edit = team.lead +} + +rule confidentiality_level_high(confidentiality_level double) { + confidentiality_level == 4.0 +} + +rule confidentiality_level_medium_high(confidentiality_level double) { + confidentiality_level == 3.0 +} + +rule confidentiality_level_medium(confidentiality_level double) { + confidentiality_level == 2.0 +} + +rule confidentiality_level_low(confidentiality_level double) { + confidentiality_level == 1.0 +} + `) + + _, err := pr.Parse() + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("Case 28 - Multi-line Permission Expression w/ Rule", func() { + pr := NewParser(` + entity account { + relation owner @user + relation admin @user + + permission withdraw = admin or + owner + } + `) + + _, err := pr.Parse() + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("Case 29 - Multi-line Permission Expression w/ Rule - should fail", func() { + pr := NewParser(` + entity account { + relation owner @user + relation admin @user + + permission withdraw = admin + or owner + } + `) + + _, err := pr.Parse() + // Ensure an error is returned + Expect(err).Should(HaveOccurred()) + + // Ensure the error message contains the expected string + Expect(err.Error()).Should(ContainSubstring("7:15:expected token to be RELATION, PERMISSION, ATTRIBUTE, got OR instead")) + }) }) }) diff --git a/playground/public/play.wasm b/playground/public/play.wasm index eeb4ba6d1..99733d551 100644 Binary files a/playground/public/play.wasm and b/playground/public/play.wasm differ