Skip to content

Commit 3998ee0

Browse files
tstirrat15miparnisari
authored andcommitted
chore: make development system filesystem-aware
1 parent b554261 commit 3998ee0

File tree

5 files changed

+113
-8
lines changed

5 files changed

+113
-8
lines changed

internal/lsp/handlers.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ func (s *Server) computeDiagnostics(ctx context.Context, uri lsp.DocumentURI) ([
5757
return &jsonrpc2.Error{Code: jsonrpc2.CodeInternalError, Message: "file not found"}
5858
}
5959

60+
overlayFS := newLSPOverlayFS(uriToSourceDir(uri), files)
6061
devCtx, devErrs, err := development.NewDevContext(ctx, &developerv1.RequestContext{
6162
Schema: file.contents,
6263
Relationships: nil,
63-
})
64+
}, development.WithSourceFS(overlayFS))
6465
if err != nil {
6566
return err
6667
}
@@ -197,7 +198,8 @@ func (s *Server) getCompiledContents(path lsp.DocumentURI, files *persistent.Map
197198
return compiled, nil
198199
}
199200

200-
justCompiled, derr, err := development.CompileSchema(file.contents)
201+
overlayFS := newLSPOverlayFS(uriToSourceDir(path), files)
202+
justCompiled, derr, err := development.CompileSchema(file.contents, development.WithSourceFS(overlayFS))
201203
if err != nil {
202204
return nil, err
203205
}

internal/lsp/overlay.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package lsp
2+
3+
import (
4+
"io/fs"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"time"
9+
10+
"github.com/jzelinskie/persistent"
11+
"github.com/sourcegraph/go-lsp"
12+
)
13+
14+
// lspOverlayFS is an fs.FS that serves open editor files from memory
15+
// and falls back to disk for everything else. It is rooted at sourceDir.
16+
type lspOverlayFS struct {
17+
base fs.FS
18+
sourceDir string
19+
files *persistent.Map[lsp.DocumentURI, trackedFile]
20+
}
21+
22+
var _ fs.FS = &lspOverlayFS{}
23+
24+
func newLSPOverlayFS(sourceDir string, files *persistent.Map[lsp.DocumentURI, trackedFile]) fs.FS {
25+
return &lspOverlayFS{
26+
base: os.DirFS(sourceDir),
27+
sourceDir: sourceDir,
28+
files: files,
29+
}
30+
}
31+
32+
func (o *lspOverlayFS) Open(name string) (fs.File, error) {
33+
absPath := filepath.Join(o.sourceDir, filepath.FromSlash(name))
34+
uri := lsp.DocumentURI("file://" + absPath)
35+
if file, ok := o.files.Get(uri); ok {
36+
return newMemFile(name, file.contents), nil
37+
}
38+
return o.base.Open(name)
39+
}
40+
41+
// memFile implements fs.File for an in-memory string.
42+
type memFile struct {
43+
name string
44+
content string
45+
reader *strings.Reader
46+
}
47+
48+
var _ fs.File = &memFile{}
49+
50+
func newMemFile(name, content string) *memFile {
51+
return &memFile{name: name, content: content, reader: strings.NewReader(content)}
52+
}
53+
54+
func (m *memFile) Read(b []byte) (int, error) { return m.reader.Read(b) }
55+
func (m *memFile) Close() error { return nil }
56+
func (m *memFile) Stat() (fs.FileInfo, error) {
57+
return memFileInfo{name: m.name, size: int64(len(m.content))}, nil
58+
}
59+
60+
type memFileInfo struct {
61+
name string
62+
size int64
63+
}
64+
65+
func (i memFileInfo) Name() string { return filepath.Base(i.name) }
66+
func (i memFileInfo) Size() int64 { return i.size }
67+
func (i memFileInfo) Mode() fs.FileMode { return 0o444 }
68+
func (i memFileInfo) ModTime() time.Time { return time.Time{} }
69+
func (i memFileInfo) IsDir() bool { return false }
70+
func (i memFileInfo) Sys() any { return nil }
71+
72+
// uriToSourceDir extracts the containing directory from a file:// URI.
73+
func uriToSourceDir(uri lsp.DocumentURI) string {
74+
path := strings.TrimPrefix(string(uri), "file://")
75+
return filepath.Dir(path)
76+
}

pkg/development/devcontext.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ type DevContext struct {
5252

5353
// NewDevContext creates a new DevContext from the specified request context, parsing and populating
5454
// the datastore as needed.
55-
func NewDevContext(ctx context.Context, requestContext *devinterface.RequestContext) (*DevContext, *devinterface.DeveloperErrors, error) {
55+
func NewDevContext(ctx context.Context, requestContext *devinterface.RequestContext, opts ...CompileOption) (*DevContext, *devinterface.DeveloperErrors, error) {
5656
ds, err := memdb.NewMemdbDatastore(0, 0*time.Second, memdb.DisableGC)
5757
if err != nil {
5858
return nil, nil, err
5959
}
6060
dl := datalayer.NewDataLayer(ds)
6161
ctx = datalayer.ContextWithDataLayer(ctx, dl)
6262

63-
dctx, devErrs, nerr := newDevContextWithDataLayer(ctx, requestContext, dl)
63+
dctx, devErrs, nerr := newDevContextWithDataLayer(ctx, requestContext, dl, opts...)
6464
if nerr != nil || devErrs != nil {
6565
// If any form of error occurred, immediately close the data layer
6666
derr := dl.Close()
@@ -74,9 +74,9 @@ func NewDevContext(ctx context.Context, requestContext *devinterface.RequestCont
7474
return dctx, nil, nil
7575
}
7676

77-
func newDevContextWithDataLayer(ctx context.Context, requestContext *devinterface.RequestContext, dl datalayer.DataLayer) (*DevContext, *devinterface.DeveloperErrors, error) {
77+
func newDevContextWithDataLayer(ctx context.Context, requestContext *devinterface.RequestContext, dl datalayer.DataLayer, opts ...CompileOption) (*DevContext, *devinterface.DeveloperErrors, error) {
7878
// Compile the schema and load its caveats and namespaces into the datastore.
79-
compiled, devError, err := CompileSchema(requestContext.Schema)
79+
compiled, devError, err := CompileSchema(requestContext.Schema, opts...)
8080
if err != nil {
8181
return nil, nil, err
8282
}

pkg/development/schema.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package development
22

33
import (
44
"errors"
5+
"io/fs"
56

67
"github.com/ccoveille/go-safecast/v2"
78

@@ -11,14 +12,37 @@ import (
1112
"github.com/authzed/spicedb/pkg/schemadsl/input"
1213
)
1314

15+
// CompileOption configures schema compilation.
16+
type CompileOption func(*compileConfig)
17+
18+
type compileConfig struct {
19+
fsys fs.FS
20+
}
21+
22+
// WithSourceFS enables import resolution using the given filesystem.
23+
// The filesystem should be rooted at the directory containing the schema file.
24+
func WithSourceFS(fsys fs.FS) CompileOption {
25+
return func(cfg *compileConfig) { cfg.fsys = fsys }
26+
}
27+
1428
// CompileSchema compiles a schema into its caveat and namespace definition(s), returning a developer
1529
// error if the schema could not be compiled. The non-developer error is returned only if an
1630
// internal errors occurred.
17-
func CompileSchema(schema string) (*compiler.CompiledSchema, *devinterface.DeveloperError, error) {
31+
func CompileSchema(schema string, opts ...CompileOption) (*compiler.CompiledSchema, *devinterface.DeveloperError, error) {
32+
cfg := &compileConfig{}
33+
for _, o := range opts {
34+
o(cfg)
35+
}
36+
37+
var compilerOpts []compiler.Option
38+
if cfg.fsys != nil {
39+
compilerOpts = append(compilerOpts, compiler.SourceFS(cfg.fsys))
40+
}
41+
1842
compiled, err := compiler.Compile(compiler.InputSchema{
1943
Source: input.Source("schema"),
2044
SchemaString: schema,
21-
}, compiler.AllowUnprefixedObjectType())
45+
}, compiler.AllowUnprefixedObjectType(), compilerOpts...)
2246

2347
var contextError compiler.WithContextError
2448
if errors.As(err, &contextError) {

pkg/schemadsl/compiler/translator.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,9 @@ func translateImports(itctx importResolutionContext, root *dslNode) error {
840840
if err := validateFilepath(importPath); err != nil {
841841
return err
842842
}
843+
if itctx.sourceFS == nil {
844+
return fmt.Errorf("import statement found but no source filesystem was configured for compilation")
845+
}
843846
filePath := filepath.Join(itctx.sourcePrefix, importPath)
844847

845848
newSourcePrefix := filepath.Dir(filePath)

0 commit comments

Comments
 (0)