Skip to content

Commit 7ff8fb1

Browse files
authored
Merge pull request #2141 from josephschorr/rel-expiration-parser
Parser changes for supporting relationship expiration
2 parents 7877329 + 9b89c57 commit 7ff8fb1

24 files changed

+546
-78
lines changed

pkg/schemadsl/dslshape/dslshape.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const (
1111
NodeTypeError NodeType = iota // error occurred; value is text of error
1212
NodeTypeFile // The file root node
1313
NodeTypeComment // A single or multiline comment
14+
NodeTypeUseFlag // A use flag
1415

1516
NodeTypeDefinition // A definition.
1617
NodeTypeCaveatDefinition // A caveat definition.
@@ -24,6 +25,7 @@ const (
2425
NodeTypeTypeReference // A type reference
2526
NodeTypeSpecificTypeReference // A reference to a specific type.
2627
NodeTypeCaveatReference // A caveat reference under a type.
28+
NodeTypeTraitReference // A trait reference under a typr.
2729

2830
NodeTypeUnionExpression
2931
NodeTypeIntersectExpression
@@ -71,6 +73,13 @@ const (
7173
// The value of the comment, including its delimeter(s)
7274
NodeCommentPredicateValue = "comment-value"
7375

76+
//
77+
// NodeTypeUseFlag
78+
//
79+
80+
// The name of the use flag.
81+
NodeUseFlagPredicateName = "use-flag-name"
82+
7483
//
7584
// NodeTypeDefinition
7685
//
@@ -155,13 +164,23 @@ const (
155164
// A caveat under a type reference.
156165
NodeSpecificReferencePredicateCaveat = "caveat"
157166

167+
// A trait under a type reference.
168+
NodeSpecificReferencePredicateTrait = "trait"
169+
158170
//
159171
// NodeTypeCaveatReference
160172
//
161173

162174
// The caveat name under the caveat.
163175
NodeCaveatPredicateCaveat = "caveat-name"
164176

177+
//
178+
// NodeTypeTraitReference
179+
//
180+
181+
// The trait name under the trait.
182+
NodeTraitPredicateTrait = "trait-name"
183+
165184
//
166185
// NodeTypePermission
167186
//

pkg/schemadsl/dslshape/zz_generated.nodetype_string.go

Lines changed: 20 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/schemadsl/lexer/flaggablelexer.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package lexer
2+
3+
// FlaggableLexler wraps a lexer, automatically translating tokens based on flags, if any.
4+
type FlaggableLexler struct {
5+
lex *Lexer // a reference to the lexer used for tokenization
6+
enabledFlags map[string]transformer // flags that are enabled
7+
seenDefinition bool
8+
afterUseIdentifier bool
9+
}
10+
11+
// NewFlaggableLexler returns a new FlaggableLexler for the given lexer.
12+
func NewFlaggableLexler(lex *Lexer) *FlaggableLexler {
13+
return &FlaggableLexler{
14+
lex: lex,
15+
enabledFlags: map[string]transformer{},
16+
}
17+
}
18+
19+
// Close stops the lexer from running.
20+
func (l *FlaggableLexler) Close() {
21+
l.lex.Close()
22+
}
23+
24+
// NextToken returns the next token found in the lexer.
25+
func (l *FlaggableLexler) NextToken() Lexeme {
26+
nextToken := l.lex.nextToken()
27+
28+
// Look for `use somefeature`
29+
if nextToken.Kind == TokenTypeIdentifier {
30+
// Only allowed until we've seen a definition of some kind.
31+
if !l.seenDefinition {
32+
if l.afterUseIdentifier {
33+
if transformer, ok := Flags[nextToken.Value]; ok {
34+
l.enabledFlags[nextToken.Value] = transformer
35+
}
36+
37+
l.afterUseIdentifier = false
38+
} else {
39+
l.afterUseIdentifier = nextToken.Value == "use"
40+
}
41+
}
42+
}
43+
44+
if nextToken.Kind == TokenTypeKeyword && nextToken.Value == "definition" {
45+
l.seenDefinition = true
46+
}
47+
if nextToken.Kind == TokenTypeKeyword && nextToken.Value == "caveat" {
48+
l.seenDefinition = true
49+
}
50+
51+
for _, handler := range l.enabledFlags {
52+
updated, ok := handler(nextToken)
53+
if ok {
54+
return updated
55+
}
56+
}
57+
58+
return nextToken
59+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package lexer
2+
3+
import (
4+
"slices"
5+
"testing"
6+
7+
"github.com/authzed/spicedb/pkg/schemadsl/input"
8+
)
9+
10+
var flaggableLexerTests = []lexerTest{
11+
{"use expiration", "use expiration", []Lexeme{
12+
{TokenTypeIdentifier, 0, "use", ""},
13+
{TokenTypeWhitespace, 0, " ", ""},
14+
{TokenTypeKeyword, 0, "expiration", ""},
15+
tEOF,
16+
}},
17+
{"use expiration and", "use expiration and", []Lexeme{
18+
{TokenTypeIdentifier, 0, "use", ""},
19+
{TokenTypeWhitespace, 0, " ", ""},
20+
{TokenTypeKeyword, 0, "expiration", ""},
21+
{TokenTypeWhitespace, 0, " ", ""},
22+
{TokenTypeKeyword, 0, "and", ""},
23+
tEOF,
24+
}},
25+
{"expiration as non-keyword", "foo expiration", []Lexeme{
26+
{TokenTypeIdentifier, 0, "foo", ""},
27+
{TokenTypeWhitespace, 0, " ", ""},
28+
{TokenTypeIdentifier, 0, "expiration", ""},
29+
tEOF,
30+
}},
31+
{"and as non-keyword", "foo and", []Lexeme{
32+
{TokenTypeIdentifier, 0, "foo", ""},
33+
{TokenTypeWhitespace, 0, " ", ""},
34+
{TokenTypeIdentifier, 0, "and", ""},
35+
tEOF,
36+
}},
37+
{"invalid use flag", "use foobar", []Lexeme{
38+
{TokenTypeIdentifier, 0, "use", ""},
39+
{TokenTypeWhitespace, 0, " ", ""},
40+
{TokenTypeIdentifier, 0, "foobar", ""},
41+
tEOF,
42+
}},
43+
{"use flag after definition", "definition use expiration", []Lexeme{
44+
{TokenTypeKeyword, 0, "definition", ""},
45+
{TokenTypeWhitespace, 0, " ", ""},
46+
{TokenTypeIdentifier, 0, "use", ""},
47+
{TokenTypeWhitespace, 0, " ", ""},
48+
{TokenTypeIdentifier, 0, "expiration", ""},
49+
tEOF,
50+
}},
51+
}
52+
53+
func TestFlaggableLexer(t *testing.T) {
54+
for _, test := range append(slices.Clone(lexerTests), flaggableLexerTests...) {
55+
t.Run(test.name, func(t *testing.T) {
56+
tokens := performFlaggedLex(&test)
57+
if !equal(tokens, test.tokens) {
58+
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, tokens, test.tokens)
59+
}
60+
})
61+
}
62+
}
63+
64+
func performFlaggedLex(t *lexerTest) (tokens []Lexeme) {
65+
lexer := NewFlaggableLexler(Lex(input.Source(t.name), t.input))
66+
for {
67+
token := lexer.NextToken()
68+
tokens = append(tokens, token)
69+
if token.Kind == TokenTypeEOF || token.Kind == TokenTypeError {
70+
break
71+
}
72+
}
73+
return
74+
}

pkg/schemadsl/lexer/flags.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package lexer
2+
3+
// FlagExpiration indicates that `expiration` is supported as a first-class
4+
// feature in the schema.
5+
const FlagExpiration = "expiration"
6+
7+
type transformer func(lexeme Lexeme) (Lexeme, bool)
8+
9+
// Flags is a map of flag names to their corresponding transformers.
10+
var Flags = map[string]transformer{
11+
FlagExpiration: func(lexeme Lexeme) (Lexeme, bool) {
12+
// `expiration` becomes a keyword.
13+
if lexeme.Kind == TokenTypeIdentifier && lexeme.Value == "expiration" {
14+
lexeme.Kind = TokenTypeKeyword
15+
return lexeme, true
16+
}
17+
18+
// `and` becomes a keyword.
19+
if lexeme.Kind == TokenTypeIdentifier && lexeme.Value == "and" {
20+
lexeme.Kind = TokenTypeKeyword
21+
return lexeme, true
22+
}
23+
24+
return lexeme, false
25+
},
26+
}

pkg/schemadsl/lexer/peekable_lex.go

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)