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

feat: multi-line formatting for permission expressions #1629

Merged
21 changes: 21 additions & 0 deletions pkg/dsl/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
158 changes: 158 additions & 0 deletions pkg/dsl/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,5 +849,163 @@ 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())
})
})
})
Binary file modified playground/public/play.wasm
Binary file not shown.
Loading