Skip to content

Commit 68bc889

Browse files
feat: Implement use import (#2919)
Co-authored-by: Maria Ines Parnisari <maria.ines.parnisari@authzed.com>
1 parent 96f92c7 commit 68bc889

File tree

71 files changed

+1221
-40
lines changed

Some content is hidden

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

71 files changed

+1221
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1212
### Changed
1313
- Begin deprecation of library "github.com/dlmiddlecote/sqlstats" (https://github.com/authzed/spicedb/pull/2904).
1414
NOTE: in a future release, MySQL metrics will change.
15+
- Add support for imports and partials to the schemadsl package that drives the LSP and development server (https://github.com/authzed/spicedb/pull/2919).
1516

1617
### Fixed
1718
- enforce graceful shutdown on serve and serve-testing (https://github.com/authzed/spicedb/pull/2888)

internal/services/shared/errors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func RewriteError(ctx context.Context, err error, config *ConfigForErrors) error
135135
return rerr
136136
}
137137

138-
func rewriteError(ctx context.Context, err error, config *ConfigForErrors) error {
138+
func rewriteError(ctx context.Context, err error, _ *ConfigForErrors) error {
139139
// Check if the error can be directly used.
140140
if _, ok := status.FromError(err); ok {
141141
return err

internal/services/v1/schema.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ func (ss *schemaServer) WriteSchema(ctx context.Context, in *v1.WriteSchemaReque
127127
}
128128

129129
opts = append(opts, compiler.CaveatTypeSet(ss.caveatTypeSet))
130+
// Imports don't make sense in a schema written directly to SpiceDB;
131+
// the user must first compile them with `zed`
132+
opts = append(opts, compiler.DisallowImportFlag())
130133

131134
compiled, err := compiler.Compile(compiler.InputSchema{
132135
Source: input.Source("schema"),

internal/services/v1/schema_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ func TestSchemaWriteInvalidNamespace(t *testing.T) {
6161
grpcutil.RequireStatus(t, codes.FailedPrecondition, err)
6262
}
6363

64+
// NOTE: imports must be handled by precompilation;
65+
// a write of a schema with an import statement is an error.
66+
func TestSchemaWriteImportsDisallowed(t *testing.T) {
67+
conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore)
68+
t.Cleanup(cleanup)
69+
client := v1.NewSchemaServiceClient(conn)
70+
71+
_, err := client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
72+
Schema: `
73+
use import
74+
75+
import "foo/bar/baz.zed"
76+
77+
definition document {
78+
relation viewer: user
79+
}
80+
`,
81+
})
82+
grpcutil.RequireStatus(t, codes.InvalidArgument, err)
83+
}
84+
6485
func TestSchemaWriteAndReadBack(t *testing.T) {
6586
conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore)
6687
t.Cleanup(cleanup)

pkg/composableschemadsl/compiler/compiler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ type ObjectPrefixOption func(*config)
117117
// Compile compilers the input schema into a set of namespace definition protos.
118118
func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*CompiledSchema, error) {
119119
cfg := &config{
120-
allowedFlags: mapz.NewSet[string](expirationFlag, selfFlag),
120+
allowedFlags: mapz.NewSet(expirationFlag, selfFlag),
121121
}
122122

123123
prefix(cfg) // required option

pkg/schemadsl/compiler/compiler.go

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package compiler
33
import (
44
"errors"
55
"fmt"
6+
"io/fs"
7+
"os"
68

79
"google.golang.org/protobuf/proto"
810

@@ -56,6 +58,10 @@ type config struct {
5658
objectTypePrefix *string
5759
allowedFlags *mapz.Set[string]
5860
caveatTypeSet *caveattypes.TypeSet
61+
62+
// In an import context, this is the FS containing
63+
// the importing schema (as opposed to imported schemas)
64+
sourceFS fs.FS
5965
}
6066

6167
func SkipValidation() Option { return func(cfg *config) { cfg.skipValidation = true } }
@@ -76,29 +82,50 @@ func CaveatTypeSet(cts *caveattypes.TypeSet) Option {
7682
return func(cfg *config) { cfg.caveatTypeSet = cts }
7783
}
7884

85+
// Config that supplies the root source folder for compilation. Required
86+
// for relative import syntax to work properly.
87+
func SourceFolder(sourceFolder string) Option {
88+
return func(cfg *config) { cfg.sourceFS = os.DirFS(sourceFolder) }
89+
}
90+
91+
// Config that supplies the fs.FS for compilation as an alternative to
92+
// SourceFolder.
93+
func SourceFS(fsys fs.FS) Option {
94+
return func(cfg *config) { cfg.sourceFS = fsys }
95+
}
96+
7997
const (
8098
expirationFlag = "expiration"
8199
selfFlag = "self"
82100
typeCheckingFlag = "typechecking"
83101
partialFlag = "partial"
102+
importFlag = "import"
84103
)
85104

86-
var allowedFlags = mapz.NewSet(expirationFlag, selfFlag, typeCheckingFlag, partialFlag)
105+
func allowedFlags() *mapz.Set[string] {
106+
return mapz.NewSet(expirationFlag, selfFlag, typeCheckingFlag, partialFlag, importFlag)
107+
}
87108

88109
func DisallowExpirationFlag() Option {
89110
return func(cfg *config) {
90111
cfg.allowedFlags.Delete(expirationFlag)
91112
}
92113
}
93114

115+
func DisallowImportFlag() Option {
116+
return func(cfg *config) {
117+
cfg.allowedFlags.Delete(importFlag)
118+
}
119+
}
120+
94121
type Option func(*config)
95122

96123
type ObjectPrefixOption func(*config)
97124

98125
// Compile compilers the input schema into a set of namespace definition protos.
99126
func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*CompiledSchema, error) {
100127
cfg := &config{
101-
allowedFlags: allowedFlags,
128+
allowedFlags: allowedFlags(),
102129
}
103130

104131
prefix(cfg) // required option
@@ -107,14 +134,36 @@ func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*Co
107134
fn(cfg)
108135
}
109136

110-
mapper := newPositionMapper(schema)
111-
root := parser.Parse(createAstNode, schema.Source, schema.SchemaString).(*dslNode)
112-
errs := root.FindAll(dslshape.NodeTypeError)
113-
if len(errs) > 0 {
114-
err := errorNodeToError(errs[0], mapper)
137+
root, mapper, err := parseSchema(schema)
138+
if err != nil {
115139
return nil, err
116140
}
117141

142+
present, err := validateImportPresence(cfg.allowedFlags.Has(importFlag), root)
143+
if err != nil {
144+
// This condition should basically always be satisfied (we trigger errors off of the node),
145+
// but we're defensive here in case the implementation changes.
146+
var withNodeError withNodeError
147+
if errors.As(err, &withNodeError) {
148+
return nil, toContextError(withNodeError.Error(), withNodeError.errorSourceCode, withNodeError.node, mapper)
149+
}
150+
return nil, err
151+
}
152+
153+
if present {
154+
// NOTE: import translation is done separately so that partial references
155+
// and definitions defined in separate files can correctly resolve.
156+
err = translateImports(importResolutionContext{
157+
globallyVisitedFiles: mapz.NewSet[string](),
158+
locallyVisitedFiles: mapz.NewSet[string](),
159+
sourceFS: cfg.sourceFS,
160+
mapper: mapper,
161+
}, root)
162+
if err != nil {
163+
return nil, err
164+
}
165+
}
166+
118167
initialCompiledPartials := make(map[string][]*core.Relation)
119168
caveatTypeSet := caveattypes.TypeSetOrDefault(cfg.caveatTypeSet)
120169
compiled, err := translate(&translationContext{
@@ -141,6 +190,34 @@ func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*Co
141190
return compiled, nil
142191
}
143192

193+
func parseSchema(schema InputSchema) (*dslNode, input.PositionMapper, error) {
194+
mapper := newPositionMapper(schema)
195+
root := parser.Parse(createAstNode, schema.Source, schema.SchemaString).(*dslNode)
196+
errs := root.FindAll(dslshape.NodeTypeError)
197+
if len(errs) > 0 {
198+
err := errorNodeToError(errs[0], mapper)
199+
return nil, nil, err
200+
}
201+
return root, mapper, nil
202+
}
203+
204+
// validateImportPresence validates whether a given AST is valid based on whether
205+
// imports are allowed in the context. if they're present and disallowed it returns
206+
// a validation error; otherwise it returns the presence.
207+
func validateImportPresence(allowed bool, root *dslNode) (present bool, err error) {
208+
present = false
209+
for _, topLevelNode := range root.GetChildren() {
210+
// Process import nodes; ignore the others
211+
if topLevelNode.GetType() == dslshape.NodeTypeImport {
212+
if !allowed {
213+
return false, topLevelNode.Errorf("import statements are not allowed in this context")
214+
}
215+
present = true
216+
}
217+
}
218+
return present, nil
219+
}
220+
144221
func errorNodeToError(node *dslNode, mapper input.PositionMapper) error {
145222
if node.GetType() != dslshape.NodeTypeError {
146223
return errors.New("given none error node")

pkg/schemadsl/compiler/compiler_test.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,19 +1327,6 @@ func TestCompile(t *testing.T) {
13271327
),
13281328
},
13291329
},
1330-
{
1331-
"duplicate use of expiration pragmas",
1332-
withTenantPrefix,
1333-
`
1334-
use expiration
1335-
use expiration
1336-
1337-
definition simple {
1338-
relation viewer: user with expiration
1339-
}`,
1340-
`found duplicate use flag`,
1341-
[]SchemaDefinition{},
1342-
},
13431330
{
13441331
"self is allowed when used in arrow and interpreted as relation name",
13451332
withTenantPrefix,
@@ -1424,19 +1411,6 @@ func TestCompile(t *testing.T) {
14241411
"Expected end of statement or definition, found: TokenTypeRightArrow",
14251412
[]SchemaDefinition{},
14261413
},
1427-
{
1428-
"duplicate use of self pragmas errors",
1429-
withTenantPrefix,
1430-
`
1431-
use self
1432-
use self
1433-
1434-
definition expressioned {
1435-
permission foos = ((arel->brel) + self) - drel
1436-
}`,
1437-
`found duplicate use flag`,
1438-
[]SchemaDefinition{},
1439-
},
14401414
{
14411415
"relation with expiration trait and caveat",
14421416
withTenantPrefix,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Test Structure
2+
3+
Every folder should have a `root.zed`. This is the target for compilation.
4+
5+
Every folder will have an `expected.zed`, which is the output of the compilation process.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
definition user {}
2+
3+
definition persona {}
4+
5+
definition resource {
6+
relation viewer: user
7+
permission view = viewer
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use import
2+
3+
import "subjects.zed"
4+
5+
definition resource {
6+
relation viewer: user
7+
permission view = viewer
8+
}

0 commit comments

Comments
 (0)