Skip to content

Commit

Permalink
Merge pull request #2141 from josephschorr/rel-expiration-parser
Browse files Browse the repository at this point in the history
Parser changes for supporting relationship expiration
  • Loading branch information
josephschorr authored Nov 25, 2024
2 parents 7877329 + 9b89c57 commit 7ff8fb1
Show file tree
Hide file tree
Showing 24 changed files with 546 additions and 78 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.

59 changes: 59 additions & 0 deletions pkg/schemadsl/lexer/flaggablelexer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package lexer

// FlaggableLexler wraps a lexer, automatically translating tokens based on flags, if any.
type FlaggableLexler struct {
lex *Lexer // a reference to the lexer used for tokenization
enabledFlags map[string]transformer // flags that are enabled
seenDefinition bool
afterUseIdentifier bool
}

// NewFlaggableLexler returns a new FlaggableLexler for the given lexer.
func NewFlaggableLexler(lex *Lexer) *FlaggableLexler {
return &FlaggableLexler{
lex: lex,
enabledFlags: map[string]transformer{},
}
}

// Close stops the lexer from running.
func (l *FlaggableLexler) Close() {
l.lex.Close()
}

// NextToken returns the next token found in the lexer.
func (l *FlaggableLexler) NextToken() Lexeme {
nextToken := l.lex.nextToken()

// Look for `use somefeature`
if nextToken.Kind == TokenTypeIdentifier {
// Only allowed until we've seen a definition of some kind.
if !l.seenDefinition {
if l.afterUseIdentifier {
if transformer, ok := Flags[nextToken.Value]; ok {
l.enabledFlags[nextToken.Value] = transformer
}

l.afterUseIdentifier = false
} else {
l.afterUseIdentifier = nextToken.Value == "use"
}
}
}

if nextToken.Kind == TokenTypeKeyword && nextToken.Value == "definition" {
l.seenDefinition = true
}
if nextToken.Kind == TokenTypeKeyword && nextToken.Value == "caveat" {
l.seenDefinition = true
}

for _, handler := range l.enabledFlags {
updated, ok := handler(nextToken)
if ok {
return updated
}
}

return nextToken
}
74 changes: 74 additions & 0 deletions pkg/schemadsl/lexer/flaggablelexer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package lexer

import (
"slices"
"testing"

"github.com/authzed/spicedb/pkg/schemadsl/input"
)

var flaggableLexerTests = []lexerTest{
{"use expiration", "use expiration", []Lexeme{
{TokenTypeIdentifier, 0, "use", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeKeyword, 0, "expiration", ""},
tEOF,
}},
{"use expiration and", "use expiration and", []Lexeme{
{TokenTypeIdentifier, 0, "use", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeKeyword, 0, "expiration", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeKeyword, 0, "and", ""},
tEOF,
}},
{"expiration as non-keyword", "foo expiration", []Lexeme{
{TokenTypeIdentifier, 0, "foo", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeIdentifier, 0, "expiration", ""},
tEOF,
}},
{"and as non-keyword", "foo and", []Lexeme{
{TokenTypeIdentifier, 0, "foo", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeIdentifier, 0, "and", ""},
tEOF,
}},
{"invalid use flag", "use foobar", []Lexeme{
{TokenTypeIdentifier, 0, "use", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeIdentifier, 0, "foobar", ""},
tEOF,
}},
{"use flag after definition", "definition use expiration", []Lexeme{
{TokenTypeKeyword, 0, "definition", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeIdentifier, 0, "use", ""},
{TokenTypeWhitespace, 0, " ", ""},
{TokenTypeIdentifier, 0, "expiration", ""},
tEOF,
}},
}

func TestFlaggableLexer(t *testing.T) {
for _, test := range append(slices.Clone(lexerTests), flaggableLexerTests...) {
t.Run(test.name, func(t *testing.T) {
tokens := performFlaggedLex(&test)
if !equal(tokens, test.tokens) {
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, tokens, test.tokens)
}
})
}
}

func performFlaggedLex(t *lexerTest) (tokens []Lexeme) {
lexer := NewFlaggableLexler(Lex(input.Source(t.name), t.input))
for {
token := lexer.NextToken()
tokens = append(tokens, token)
if token.Kind == TokenTypeEOF || token.Kind == TokenTypeError {
break
}
}
return
}
26 changes: 26 additions & 0 deletions pkg/schemadsl/lexer/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package lexer

// FlagExpiration indicates that `expiration` is supported as a first-class
// feature in the schema.
const FlagExpiration = "expiration"

type transformer func(lexeme Lexeme) (Lexeme, bool)

// Flags is a map of flag names to their corresponding transformers.
var Flags = map[string]transformer{
FlagExpiration: func(lexeme Lexeme) (Lexeme, bool) {
// `expiration` becomes a keyword.
if lexeme.Kind == TokenTypeIdentifier && lexeme.Value == "expiration" {
lexeme.Kind = TokenTypeKeyword
return lexeme, true
}

// `and` becomes a keyword.
if lexeme.Kind == TokenTypeIdentifier && lexeme.Value == "and" {
lexeme.Kind = TokenTypeKeyword
return lexeme, true
}

return lexeme, false
},
}
35 changes: 0 additions & 35 deletions pkg/schemadsl/lexer/peekable_lex.go

This file was deleted.

Loading

0 comments on commit 7ff8fb1

Please sign in to comment.