Skip to content

Commit

Permalink
Add basic parser support for with expiration on subject types
Browse files Browse the repository at this point in the history
  • Loading branch information
josephschorr committed Nov 25, 2024
1 parent 2cb6d8c commit 9b89c57
Show file tree
Hide file tree
Showing 20 changed files with 387 additions and 43 deletions.
19 changes: 19 additions & 0 deletions pkg/schemadsl/dslshape/dslshape.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
NodeTypeError NodeType = iota // error occurred; value is text of error
NodeTypeFile // The file root node
NodeTypeComment // A single or multiline comment
NodeTypeUseFlag // A use flag

NodeTypeDefinition // A definition.
NodeTypeCaveatDefinition // A caveat definition.
Expand All @@ -24,6 +25,7 @@ const (
NodeTypeTypeReference // A type reference
NodeTypeSpecificTypeReference // A reference to a specific type.
NodeTypeCaveatReference // A caveat reference under a type.
NodeTypeTraitReference // A trait reference under a typr.

NodeTypeUnionExpression
NodeTypeIntersectExpression
Expand Down Expand Up @@ -71,6 +73,13 @@ const (
// The value of the comment, including its delimeter(s)
NodeCommentPredicateValue = "comment-value"

//
// NodeTypeUseFlag
//

// The name of the use flag.
NodeUseFlagPredicateName = "use-flag-name"

//
// NodeTypeDefinition
//
Expand Down Expand Up @@ -155,13 +164,23 @@ const (
// A caveat under a type reference.
NodeSpecificReferencePredicateCaveat = "caveat"

// A trait under a type reference.
NodeSpecificReferencePredicateTrait = "trait"

//
// NodeTypeCaveatReference
//

// The caveat name under the caveat.
NodeCaveatPredicateCaveat = "caveat-name"

//
// NodeTypeTraitReference
//

// The trait name under the trait.
NodeTraitPredicateTrait = "trait-name"

//
// NodeTypePermission
//
Expand Down
38 changes: 20 additions & 18 deletions pkg/schemadsl/dslshape/zz_generated.nodetype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 76 additions & 11 deletions pkg/schemadsl/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package parser
import (
"strings"

"golang.org/x/exp/maps"

"github.com/authzed/spicedb/pkg/schemadsl/dslshape"
"github.com/authzed/spicedb/pkg/schemadsl/input"
"github.com/authzed/spicedb/pkg/schemadsl/lexer"
Expand Down Expand Up @@ -38,12 +40,20 @@ func (p *sourceParser) consumeTopLevel() AstNode {
return rootNode
}

hasSeenDefinition := false

Loop:
for {
if p.isToken(lexer.TokenTypeEOF) {
break Loop
}

if !hasSeenDefinition {
if p.isIdentifier("use") {
rootNode.Connect(dslshape.NodePredicateChild, p.consumeUseFlag())
}
}

// Consume a statement terminator if one was found.
p.tryConsumeStatementTerminator()

Expand All @@ -57,9 +67,11 @@ Loop:

switch {
case p.isKeyword("definition"):
hasSeenDefinition = true
rootNode.Connect(dslshape.NodePredicateChild, p.consumeDefinition())

case p.isKeyword("caveat"):
hasSeenDefinition = true
rootNode.Connect(dslshape.NodePredicateChild, p.consumeCaveat())

default:
Expand Down Expand Up @@ -231,6 +243,35 @@ func (p *sourceParser) consumeCaveatTypeReference() AstNode {
return typeRefNode
}

// consumeUseFlag attempts to consume a use flag.
// ``` use flagname ```
func (p *sourceParser) consumeUseFlag() AstNode {
useNode := p.startNode(dslshape.NodeTypeUseFlag)
defer p.mustFinishNode()

// consume the `use`
p.consumeIdentifier()

var useFlag string
if p.isToken(lexer.TokenTypeIdentifier) {
useFlag, _ = p.consumeIdentifier()
} else {
useName, ok := p.consumeVariableKeyword()
if !ok {
return useNode
}
useFlag = useName
}

if _, ok := lexer.Flags[useFlag]; !ok {
p.emitErrorf("Unknown use flag: `%s`. Options are: %s", useFlag, strings.Join(maps.Keys(lexer.Flags), ", "))
return useNode
}

useNode.MustDecorate(dslshape.NodeUseFlagPredicateName, useFlag)
return useNode
}

// consumeDefinition attempts to consume a single schema definition.
// ```definition somedef { ... }```
func (p *sourceParser) consumeDefinition() AstNode {
Expand Down Expand Up @@ -323,17 +364,9 @@ func (p *sourceParser) consumeTypeReference() AstNode {

// tryConsumeWithCaveat tries to consume a caveat `with` expression.
func (p *sourceParser) tryConsumeWithCaveat() (AstNode, bool) {
if !p.isKeyword("with") {
return nil, false
}

caveatNode := p.startNode(dslshape.NodeTypeCaveatReference)
defer p.mustFinishNode()

if ok := p.consumeKeyword("with"); !ok {
return nil, ok
}

consumed, ok := p.consumeTypePath()
if !ok {
return caveatNode, true
Expand All @@ -348,14 +381,46 @@ func (p *sourceParser) consumeSpecificTypeWithCaveat() AstNode {
specificNode := p.consumeSpecificTypeWithoutFinish()
defer p.mustFinishNode()

caveatNode, ok := p.tryConsumeWithCaveat()
if ok {
specificNode.Connect(dslshape.NodeSpecificReferencePredicateCaveat, caveatNode)
// Check for a caveat and/or supported trait.
if !p.isKeyword("with") {
return specificNode
}

p.consumeKeyword("with")

if !p.isKeyword("expiration") {
caveatNode, ok := p.tryConsumeWithCaveat()
if ok {
specificNode.Connect(dslshape.NodeSpecificReferencePredicateCaveat, caveatNode)
}

if !p.tryConsumeKeyword("and") {
return specificNode
}
}

if p.isKeyword("expiration") {
// Check for expiration trait.
traitNode := p.consumeExpirationTrait()

// Decorate with the expiration trait.
specificNode.Connect(dslshape.NodeSpecificReferencePredicateTrait, traitNode)
}

return specificNode
}

// consumeExpirationTrait consumes an expiration trait.
func (p *sourceParser) consumeExpirationTrait() AstNode {
expirationTraitNode := p.startNode(dslshape.NodeTypeTraitReference)
p.consumeKeyword("expiration")

expirationTraitNode.MustDecorate(dslshape.NodeTraitPredicateTrait, "expiration")
defer p.mustFinishNode()

return expirationTraitNode
}

// consumeSpecificTypeOpen consumes an identifier as a specific type reference.
func (p *sourceParser) consumeSpecificTypeWithoutFinish() AstNode {
specificNode := p.startNode(dslshape.NodeTypeSpecificTypeReference)
Expand Down
33 changes: 25 additions & 8 deletions pkg/schemadsl/parser/parser_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ type commentedLexeme struct {

// sourceParser holds the state of the parser.
type sourceParser struct {
source input.Source // the name of the input; used only for error reports
input string // the input string itself
lex *lexer.PeekableLexer // a reference to the lexer used for tokenization
builder NodeBuilder // the builder function for creating AstNode instances
nodes *nodeStack // the stack of the current nodes
currentToken commentedLexeme // the current token
previousToken commentedLexeme // the previous token
source input.Source // the name of the input; used only for error reports
input string // the input string itself
lex *lexer.FlaggableLexler // a reference to the lexer used for tokenization
builder NodeBuilder // the builder function for creating AstNode instances
nodes *nodeStack // the stack of the current nodes
currentToken commentedLexeme // the current token
previousToken commentedLexeme // the previous token
}

// buildParser returns a new sourceParser instance.
func buildParser(lx *lexer.Lexer, builder NodeBuilder, source input.Source, input string) *sourceParser {
l := lexer.NewPeekableLexer(lx)
l := lexer.NewFlaggableLexler(lx)
return &sourceParser{
source: source,
input: input,
Expand Down Expand Up @@ -163,6 +163,11 @@ func (p *sourceParser) isToken(types ...lexer.TokenType) bool {
return false
}

// isIdentifier returns true if the current token is an identifier matching that given.
func (p *sourceParser) isIdentifier(identifier string) bool {
return p.isToken(lexer.TokenTypeIdentifier) && p.currentToken.Value == identifier
}

// isKeyword returns true if the current token is a keyword matching that given.
func (p *sourceParser) isKeyword(keyword string) bool {
return p.isToken(lexer.TokenTypeKeyword) && p.currentToken.Value == keyword
Expand All @@ -178,6 +183,18 @@ func (p *sourceParser) emitErrorf(format string, args ...interface{}) {
p.currentNode().Connect(dslshape.NodePredicateChild, errorNode)
}

// consumeVariableKeyword consumes an expected keyword token or adds an error node.
func (p *sourceParser) consumeVariableKeyword() (string, bool) {
if !p.isToken(lexer.TokenTypeKeyword) {
p.emitErrorf("Expected keyword, found token %v", p.currentToken.Kind)
return "", false
}

token := p.currentToken
p.consumeToken()
return token.Value, true
}

// consumeKeyword consumes an expected keyword token or adds an error node.
func (p *sourceParser) consumeKeyword(keyword string) bool {
if !p.tryConsumeKeyword(keyword) {
Expand Down
6 changes: 6 additions & 0 deletions pkg/schemadsl/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ func TestParser(t *testing.T) {
{"arrow illegal operations test", "arrowillegalops"},
{"arrow illegal function test", "arrowillegalfunc"},
{"caveat with keyword parameter test", "caveatwithkeywordparam"},
{"use expiration test", "useexpiration"},
{"use expiration keyword test", "useexpirationkeyword"},
{"expiration non-keyword test", "expirationnonkeyword"},
{"invalid use", "invaliduse"},
{"use after definition", "useafterdef"},
{"invalid use expiration test", "invaliduseexpiration"},
}

for _, test := range parserTests {
Expand Down
8 changes: 4 additions & 4 deletions pkg/schemadsl/parser/tests/caveatstype.zed.expected
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ NodeTypeFile
caveat-name = somecaveat
end-rune = 67
input-source = caveats type test
start-rune = 53
start-rune = 58
NodeTypeSpecificTypeReference
end-rune = 100
input-source = caveats type test
Expand All @@ -47,7 +47,7 @@ NodeTypeFile
caveat-name = anothercaveat
end-rune = 100
input-source = caveats type test
start-rune = 83
start-rune = 88
NodeTypeRelation
end-rune = 187
input-source = caveats type test
Expand Down Expand Up @@ -75,7 +75,7 @@ NodeTypeFile
caveat-name = wildcardcaveat
end-rune = 153
input-source = caveats type test
start-rune = 135
start-rune = 140
NodeTypeSpecificTypeReference
end-rune = 187
input-source = caveats type test
Expand All @@ -86,4 +86,4 @@ NodeTypeFile
caveat-name = someprefix/somecaveat
end-rune = 187
input-source = caveats type test
start-rune = 162
start-rune = 167
3 changes: 3 additions & 0 deletions pkg/schemadsl/parser/tests/expirationnonkeyword.zed
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
caveat expiration(someparam int) {
someparam == 42
}
29 changes: 29 additions & 0 deletions pkg/schemadsl/parser/tests/expirationnonkeyword.zed.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
NodeTypeFile
end-rune = 52
input-source = expiration non-keyword test
start-rune = 0
child-node =>
NodeTypeCaveatDefinition
caveat-definition-name = expiration
end-rune = 52
input-source = expiration non-keyword test
start-rune = 0
caveat-definition-expression =>
NodeTypeCaveatExpression
caveat-expression-expressionstr = someparam == 42

end-rune = 51
input-source = expiration non-keyword test
start-rune = 36
parameters =>
NodeTypeCaveatParameter
caveat-parameter-name = someparam
end-rune = 30
input-source = expiration non-keyword test
start-rune = 18
caveat-parameter-type =>
NodeTypeCaveatTypeReference
end-rune = 30
input-source = expiration non-keyword test
start-rune = 28
type-name = int
3 changes: 3 additions & 0 deletions pkg/schemadsl/parser/tests/invaliduse.zed
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
use something

definition resource {}
Loading

0 comments on commit 9b89c57

Please sign in to comment.