Skip to content

Commit ec216dc

Browse files
authored
Merge pull request authzed#2207 from authzed/implement-simpler-import-syntax
Implement simpler import syntax
2 parents a80f596 + c1756e6 commit ec216dc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+392
-588
lines changed

pkg/composableschemadsl/compiler/importer-test/circular-import/root.zed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .subjects import user
1+
import "subjects.zed"
22

33
definition resource {
44
relation viewer: user
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from .user import user
1+
import "user.zed"
22

33
definition persona {}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from .subjects import persona
1+
import "subjects.zed"
22

33
definition user {}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .subjects import user
1+
import "subjects.zed"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .subjects import persona
1+
import "subjects.zed"

pkg/composableschemadsl/compiler/importer-test/diamond-shaped/root.zed

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from .left import user
2-
from .right import persona
1+
import "left.zed"
2+
import "right.zed"
33

44
definition resource {
55
relation viewer: user
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "../user.zed"

pkg/composableschemadsl/compiler/importer-test/nested-local-with-hop/root.zed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .transitive.transitive import user
1+
import "transitive/transitive.zed"
22

33
definition resource {
44
relation viewer: user
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .user.user import user
1+
import "user/user.zed"

pkg/composableschemadsl/compiler/importer-test/nested-local/root.zed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .user.user import user
1+
import "user/user.zed"
22

33
definition resource {
44
relation viewer: user

pkg/composableschemadsl/compiler/importer-test/nested-two-layer-local/root.zed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .definitions.user.user import user
1+
import "definitions/user/user.zed"
22

33
definition resource {
44
relation viewer: user
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .user import user
1+
import "user.zed"

pkg/composableschemadsl/compiler/importer-test/simple-local-with-hop/root.zed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .indirection import user
1+
import "indirection.zed"
22

33
definition resource {
44
relation viewer: user

pkg/composableschemadsl/compiler/importer-test/simple-local/root.zed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .user import user
1+
import "user.zed"
22

33
definition resource {
44
relation viewer: user

pkg/composableschemadsl/compiler/importer.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path"
77
"path/filepath"
8+
"strings"
89

910
"github.com/rs/zerolog/log"
1011

@@ -13,23 +14,23 @@ import (
1314
)
1415

1516
type importContext struct {
16-
pathSegments []string
17+
path string
1718
sourceFolder string
1819
names *mapz.Set[string]
1920
locallyVisitedFiles *mapz.Set[string]
2021
globallyVisitedFiles *mapz.Set[string]
2122
}
2223

23-
const SchemaFileSuffix = ".zed"
24-
2524
type CircularImportError struct {
2625
error
2726
filePath string
2827
}
2928

3029
func importFile(importContext importContext) (*CompiledSchema, error) {
31-
relativeFilepath := constructFilePath(importContext.pathSegments)
32-
filePath := path.Join(importContext.sourceFolder, relativeFilepath)
30+
if err := validateFilepath(importContext.path); err != nil {
31+
return nil, err
32+
}
33+
filePath := path.Join(importContext.sourceFolder, importContext.path)
3334

3435
newSourceFolder := filepath.Dir(filePath)
3536

@@ -84,6 +85,18 @@ func importFile(importContext importContext) (*CompiledSchema, error) {
8485
)
8586
}
8687

87-
func constructFilePath(segments []string) string {
88-
return path.Join(segments...) + SchemaFileSuffix
88+
// Take a filepath and ensure that it's local to the current context.
89+
func validateFilepath(path string) error {
90+
if strings.Contains(path, "..") {
91+
return fmt.Errorf("path %s contains '..'; paths must stay within their directory and this is likely an error", path)
92+
}
93+
// NOTE: This is slightly overly restrictive; it should theoretically be possible
94+
// to take a given filepath and figure out whether it's local to the context where
95+
// the compiler is being invoked, rather than whether it's local to the source
96+
// folder of the current context. The assumption is that that won't matter
97+
// right now, and we can fix it if we need to.
98+
if !filepath.IsLocal(path) {
99+
return fmt.Errorf("import path %s does not stay within its folder", path)
100+
}
101+
return nil
89102
}

pkg/composableschemadsl/compiler/importer_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,23 @@ func TestImportCycleCausesError(t *testing.T) {
110110

111111
require.ErrorContains(t, err, "circular import")
112112
}
113+
114+
func TestEscapeAttemptCausesError(t *testing.T) {
115+
t.Parallel()
116+
117+
workingDir, err := os.Getwd()
118+
require.NoError(t, err)
119+
test := importerTest{"", "escape-attempt"}
120+
121+
sourceFolder := path.Join(workingDir, test.relativePath())
122+
123+
inputSchema := test.input()
124+
125+
_, err = compiler.Compile(compiler.InputSchema{
126+
Source: input.Source("schema"),
127+
SchemaString: inputSchema,
128+
}, compiler.AllowUnprefixedObjectType(),
129+
compiler.SourceFolder(sourceFolder))
130+
131+
require.ErrorContains(t, err, "must stay within")
132+
}

pkg/composableschemadsl/compiler/translator.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -697,23 +697,14 @@ func addWithCaveats(tctx translationContext, typeRefNode *dslNode, ref *core.All
697697
}
698698

699699
func translateImport(tctx translationContext, importNode *dslNode, names *mapz.Set[string]) (*CompiledSchema, error) {
700-
// NOTE: this function currently just grabs everything that's in the target file.
701-
// TODO: only grab the requested definitions
702-
pathNodes := importNode.List(dslshape.NodeImportPredicatePathSegment)
703-
pathSegments := make([]string, 0, len(pathNodes))
704-
705-
// Get the filepath segments out of the AST nodes
706-
for _, pathSegmentNode := range pathNodes {
707-
segment, err := pathSegmentNode.GetString(dslshape.NodeIdentiferPredicateValue)
708-
if err != nil {
709-
return nil, err
710-
}
711-
pathSegments = append(pathSegments, segment)
700+
path, err := importNode.GetString(dslshape.NodeImportPredicatePath)
701+
if err != nil {
702+
return nil, err
712703
}
713704

714705
compiledSchema, err := importFile(importContext{
715706
names: names,
716-
pathSegments: pathSegments,
707+
path: path,
717708
sourceFolder: tctx.sourceFolder,
718709
globallyVisitedFiles: tctx.globallyVisitedFiles,
719710
locallyVisitedFiles: tctx.locallyVisitedFiles,

pkg/composableschemadsl/dslshape/dslshape.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,5 @@ const (
194194
//
195195
// NodeTypeImport
196196
//
197-
// TODO: still need to figure out what form this should take - full path? relative path?
198-
NodeImportPredicateSource = "import-source"
199-
NodeImportPredicatePathSegment = "path-segment"
200-
NodeImportPredicateDefinitionName = "imported-definition"
197+
NodeImportPredicatePath = "import-path"
201198
)

pkg/composableschemadsl/lexer/lex_def.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ var keywords = map[string]struct{}{
7878
"permission": {},
7979
"nil": {},
8080
"with": {},
81-
"from": {},
8281
"import": {},
8382
"all": {},
8483
"any": {},

pkg/composableschemadsl/lexer/lex_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ var lexerTests = []lexerTest{
6262
{"keyword", "permission", []Lexeme{{TokenTypeKeyword, 0, "permission", ""}, tEOF}},
6363
{"keyword", "nil", []Lexeme{{TokenTypeKeyword, 0, "nil", ""}, tEOF}},
6464
{"keyword", "with", []Lexeme{{TokenTypeKeyword, 0, "with", ""}, tEOF}},
65-
{"keyword", "from", []Lexeme{{TokenTypeKeyword, 0, "from", ""}, tEOF}},
6665
{"keyword", "import", []Lexeme{{TokenTypeKeyword, 0, "import", ""}, tEOF}},
6766
{"keyword", "all", []Lexeme{{TokenTypeKeyword, 0, "all", ""}, tEOF}},
6867
{"keyword", "nil", []Lexeme{{TokenTypeKeyword, 0, "nil", ""}, tEOF}},

pkg/composableschemadsl/parser/parser.go

Lines changed: 7 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Loop:
6262
case p.isKeyword("caveat"):
6363
rootNode.Connect(dslshape.NodePredicateChild, p.consumeCaveat())
6464

65-
case p.isKeyword("from"):
65+
case p.isKeyword("import"):
6666
rootNode.Connect(dslshape.NodePredicateChild, p.consumeImport())
6767

6868
default:
@@ -586,20 +586,6 @@ func (p *sourceParser) tryConsumeIdentifierLiteral() (AstNode, bool) {
586586
return identNode, true
587587
}
588588

589-
// consumeIdentifierLiteral is similar to the above, but attempts and errors
590-
// rather than checking the token type beforehand
591-
func (p *sourceParser) consumeIdentifierLiteral() (AstNode, bool) {
592-
identNode := p.startNode(dslshape.NodeTypeIdentifier)
593-
defer p.mustFinishNode()
594-
595-
identifier, ok := p.consumeIdentifier()
596-
if !ok {
597-
return identNode, false
598-
}
599-
identNode.MustDecorate(dslshape.NodeIdentiferPredicateValue, identifier)
600-
return identNode, true
601-
}
602-
603589
func (p *sourceParser) tryConsumeNilExpression() (AstNode, bool) {
604590
if !p.isKeyword("nil") {
605591
return nil, false
@@ -615,53 +601,17 @@ func (p *sourceParser) consumeImport() AstNode {
615601
importNode := p.startNode(dslshape.NodeTypeImport)
616602
defer p.mustFinishNode()
617603

618-
// from ...
604+
// import ...
619605
// NOTE: error handling isn't necessary here because this function is only
620-
// invoked if the `from` keyword is found in the function above.
621-
p.consumeKeyword("from")
622-
623-
// Consume alternating periods and identifiers
624-
for {
625-
if _, ok := p.consume(lexer.TokenTypePeriod); !ok {
626-
return importNode
627-
}
628-
629-
segmentNode, ok := p.consumeIdentifierLiteral()
630-
// We connect the node so that the error information is retained, then break the loop
631-
// so that we aren't continuing to attempt to consume.
632-
importNode.Connect(dslshape.NodeImportPredicatePathSegment, segmentNode)
633-
if !ok {
634-
break
635-
}
636-
637-
if !p.isToken(lexer.TokenTypePeriod) {
638-
// If we don't have a period as our next token, we move
639-
// to the next step of parsing.
640-
break
641-
}
642-
}
606+
// invoked if the `import` keyword is found in the function above.
607+
p.consumeKeyword("import")
643608

644-
if ok := p.consumeKeyword("import"); !ok {
609+
importPath, ok := p.consumeStringLiteral()
610+
if !ok {
645611
return importNode
646612
}
647613

648-
// Consume alternating identifiers and commas until we reach the end of the import statement
649-
for {
650-
definitionNode, ok := p.consumeIdentifierLiteral()
651-
// We connect the node so that the error information is retained, then break the loop
652-
// so that we aren't continuing to attempt to consume.
653-
importNode.Connect(dslshape.NodeImportPredicateDefinitionName, definitionNode)
654-
if !ok {
655-
break
656-
}
657-
658-
if _, ok := p.tryConsumeStatementTerminator(); ok {
659-
break
660-
}
661-
if _, ok := p.consume(lexer.TokenTypeComma); !ok {
662-
return importNode
663-
}
664-
}
614+
importNode.MustDecorate(dslshape.NodeImportPredicatePath, importPath)
665615

666616
return importNode
667617
}

pkg/composableschemadsl/parser/parser_impl.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,37 @@ func (p *sourceParser) tryConsumeKeyword(keyword string) bool {
198198
return true
199199
}
200200

201+
// consumeString consumes a string token and returns the unwrapped string or adds an error node.
202+
func (p *sourceParser) consumeStringLiteral() (string, bool) {
203+
consumedString, ok := p.tryConsumeStringLiteral()
204+
if !ok {
205+
p.emitErrorf("Expected quote-delimited string, found token %v", p.currentToken.Kind)
206+
return "", false
207+
}
208+
return consumedString, true
209+
}
210+
211+
// tryConsumeString attempts to consume an expected string token and return the unwrapped string.
212+
func (p *sourceParser) tryConsumeStringLiteral() (string, bool) {
213+
wrappedStringToken, ok := p.tryConsume(lexer.TokenTypeString)
214+
if !ok {
215+
return "", false
216+
}
217+
wrappedString := wrappedStringToken.Value
218+
219+
// NOTE: We can't just trim here, because a user may use a combination of
220+
// single and double quotes to escape.
221+
// If we have a string wrapped in singlequotes (singular or plural),
222+
// strip those specifically.
223+
if strings.Index(wrappedString, `'`) == 0 {
224+
return strings.Trim(wrappedString, `'`), true
225+
}
226+
227+
// Else strip doublequotes, because the set of string delimiters is limited
228+
// by the lexer.
229+
return strings.Trim(wrappedString, `"`), true
230+
}
231+
201232
// consumeKeywords consumes any of a set of keywords or adds an error node
202233
func (p *sourceParser) consumeKeywords(keywords ...string) (string, bool) {
203234
keyword, ok := p.tryConsumeKeywords(keywords...)

pkg/composableschemadsl/parser/parser_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,11 @@ func TestParser(t *testing.T) {
123123
{"arrow illegal function test", "arrowillegalfunc"},
124124
{"caveat with keyword parameter test", "caveatwithkeywordparam"},
125125
{"local imports test", "localimport"},
126+
{"local imports with singlequotes on import test", "localimport_with_singlequotes"},
127+
{"local imports with quotes within quotes on import test", "localimport_with_quotes_in_quotes"},
128+
{"local imports with unterminated string on import test", "localimport_with_unterminated_string"},
129+
{"local imports with mismatched quotes on import test", "localimport_with_mismatched_quotes"},
126130
{"local imports with keyword in import path test", "localimport_import_path_with_keyword"},
127-
{"local imports with keyword in identifiers test", "localimport_keyword_in_identifiers"},
128-
{"local imports with malformed identifiers set test", "localimport_malformed_identifier_set"},
129-
{"local imports with malformed import path test", "localimport_malformed_import_path"},
130-
{"local imports with path missing leading period test", "localimport_path_missing_leading_period"},
131-
{"local imports with typo in import separator test", "localimport_typo_in_import_separator"},
132131
}
133132

134133
for _, test := range parserTests {

pkg/composableschemadsl/parser/tests/localimport.zed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .path.to.user import user, persona
1+
import "path/to/user.zed"
22

33
definition resource {
44
relation user: user

0 commit comments

Comments
 (0)