fs: simplify adapter to type aliases only

- Remove wrapper functions from adapter.go (212 → 91 lines)
- Delete redundant adapter_test.go (fsedsl has 94.8% coverage)
- Add local function aliases in spec.go for clean EDSL usage
- Update handlers.go, newroot.go, tree_test.go to use fsedsl directly
- Update fsedsl README with cleaner integration pattern
This commit is contained in:
Levi Neely 2026-08-04 12:47:17 +02:00
parent 19e255c869
commit aaacae40a8
7 changed files with 66 additions and 676 deletions

View File

@ -1,13 +1,10 @@
package fs
// This file provides type aliases and wrapper functions that specialize
// fsedsl for ollie's HandlerCtx. This provides a clean API for the rest
// of the fs package without requiring explicit type parameters everywhere.
// This file provides type aliases that specialize fsedsl for ollie's HandlerCtx.
// The fs package uses fsedsl directly (e.g., fsedsl.DirNode[HandlerCtx]).
// These aliases allow other packages to use fs.FsNodeDecl, fs.Tree, etc.
import (
"context"
"os"
"ollie/agent"
"ollie/elevate"
"ollie/fsedsl"
@ -87,126 +84,8 @@ const (
)
// BindTarget declares what data an Each block binds to handlers.
// This is advisory — the actual binding is done by Binding.Applier.
type BindTarget = fsedsl.BindTarget
const (
BindData = fsedsl.BindData
)
// ── EDSL constructors ────────────────────────────────────────────
// DirNode declares a static directory node.
func DirNode(name string, args ...any) FsNodeDecl {
return fsedsl.DirNode[HandlerCtx](name, args...)
}
// Each declares a directory that is instantiated for each binding in a collection.
func Each(name string, bindings func(HandlerCtx) ([]Binding, error), args ...any) FsNodeDecl {
return fsedsl.Each(name, bindings, args...)
}
// FileNode declares a file node (non-directory).
func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
return fsedsl.FileNode(name, mode, opts...)
}
// ── Options ──────────────────────────────────────────────────────
func Read(fn func(HandlerCtx) ([]byte, error)) NodeOption {
return fsedsl.Read(fn)
}
func Write(fn func(HandlerCtx, []byte) error) NodeOption {
return fsedsl.Write(fn)
}
func Stream(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption {
return fsedsl.Stream(fn)
}
func BlockOnce(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption {
return fsedsl.BlockOnce(fn)
}
func Request(fn func(HandlerCtx, []byte) ([]byte, error)) NodeOption {
return fsedsl.Request(fn)
}
func CreateFile(fn func(HandlerCtx, string) error) NodeOption {
return fsedsl.CreateFile(fn)
}
func Remove(fn func(HandlerCtx) error) NodeOption {
return fsedsl.RemoveNode(fn)
}
func Stat(fn func() os.FileInfo) NodeOption {
return fsedsl.StatOverride[HandlerCtx](fn)
}
func UID(uid string) NodeOption {
return fsedsl.UID[HandlerCtx](uid)
}
func GID(gid string) NodeOption {
return fsedsl.GID[HandlerCtx](gid)
}
func Alias(names ...string) NodeOption {
return fsedsl.Alias[HandlerCtx](names...)
}
// Bind declares how Each bindings populate HandlerCtx.
// This is advisory metadata — the actual binding is done by Binding.Applier.
func Bind(targets ...BindTarget) NodeOption {
return fsedsl.Bind[HandlerCtx](targets...)
}
// Doc sets a human-readable description for the node.
func Doc(desc string) NodeOption {
return fsedsl.Doc[HandlerCtx](desc)
}
// ── Tree construction ────────────────────────────────────────────
// BuildTree walks an FsNodeDecl spec and produces a *Tree.
func BuildTree(spec FsNodeDecl, rootCtx HandlerCtx) *Tree {
return fsedsl.BuildTree(spec, rootCtx)
}
// NewTree creates a Tree backed by one or more directories.
func NewTree(dirs []string, perm os.FileMode, opts ...TreeOption) *Tree {
return fsedsl.NewTree(dirs, perm, opts...)
}
// Tree options
var (
WithReadOnly = fsedsl.WithReadOnly
WithIndex = fsedsl.WithIndex
WithResolver = fsedsl.WithResolver
WithLister = fsedsl.WithLister
WithCreator = fsedsl.WithCreator
WithDeleter = fsedsl.WithDeleter
WithRenamer = fsedsl.WithRenamer
WithOpener = fsedsl.WithOpener
WithStat = fsedsl.WithStat
WithReaddir = fsedsl.WithReaddir
)
// ── Helpers ──────────────────────────────────────────────────────
// FileEntry returns a synthetic file DirEntry.
func FileEntry(name string, mode os.FileMode) os.DirEntry {
return fsedsl.FileEntry(name, mode)
}
// DirEntry returns a synthetic directory DirEntry.
func DirEntry(name string, mode os.FileMode) os.DirEntry {
return fsedsl.DirEntry(name, mode)
}
// GenerateHelp produces help text from a filesystem spec tree.
func GenerateHelp(root FsNodeDecl) string {
return fsedsl.GenerateHelp(root)
}

View File

@ -1,501 +0,0 @@
package fs
import (
"os"
"testing"
)
// TestEachBasic tests the basic Blueprint pattern:
// - Declares structure statically
// - Bindings function provides identity (name, aliases, UID/GID)
// - Handlers receive context with bound data
func TestEachBasic(t *testing.T) {
// Test data that instances will bind to ctx
type testData struct {
ID string
Name string
}
instances := []testData{
{ID: "aaa-111", Name: "first"},
{ID: "bbb-222", Name: "second"},
}
// Create a Blueprint with static structure and dynamic instances
spec := DirNode("/",
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
var out []Binding
for _, d := range instances {
data := d // capture
out = append(out, Binding{
Name: data.Name,
Aliases: []string{data.ID},
UID: data.ID,
GID: "testgroup",
})
}
return out, nil
},
FileNode("id", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("test-id\n"), nil
})),
FileNode("name", 0666,
Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("test-name\n"), nil
}),
Write(func(ctx HandlerCtx, data []byte) error {
return nil // no-op for test
}),
),
),
)
tree := BuildTree(spec, HandlerCtx{})
// Test: List root should show the two instances by name
entries, err := tree.List()
if err != nil {
t.Fatalf("List root: %v", err)
}
if len(entries) != 2 {
t.Errorf("List root: got %d entries, want 2", len(entries))
}
names := make(map[string]bool)
for _, e := range entries {
names[e.Name()] = true
}
if !names["first"] {
t.Error("List root: missing 'first'")
}
if !names["second"] {
t.Error("List root: missing 'second'")
}
// Test: Access by name
sub, err := tree.Readdir("first")
if err != nil {
t.Fatalf("Readdir(first): %v", err)
}
foundID := false
foundName := false
for _, e := range sub {
switch e.Name() {
case "id":
foundID = true
case "name":
foundName = true
}
}
if !foundID {
t.Error("Readdir(first): missing 'id' leaf")
}
if !foundName {
t.Error("Readdir(first): missing 'name' leaf")
}
// Test: Access by alias
subByAlias, err := tree.Readdir("aaa-111")
if err != nil {
t.Fatalf("Readdir(aaa-111): %v", err)
}
if len(subByAlias) != len(sub) {
t.Errorf("Readdir by alias: got %d entries, want %d", len(subByAlias), len(sub))
}
// Test: Read leaf file
f, err := tree.Open("first/id")
if err != nil {
t.Fatalf("Open(first/id): %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read(first/id): %v", err)
}
if string(data) != "test-id\n" {
t.Errorf("Read(first/id) = %q, want %q", data, "test-id\n")
}
// Test: Stat instance directory
fi, err := tree.Stat("first")
if err != nil {
t.Fatalf("Stat(first): %v", err)
}
if !fi.IsDir() {
t.Error("Stat(first): expected directory")
}
if fi.Name() != "first" {
t.Errorf("Stat(first).Name() = %q, want 'first'", fi.Name())
}
// Test: Stat via alias
fiAlias, err := tree.Stat("aaa-111")
if err != nil {
t.Fatalf("Stat(aaa-111): %v", err)
}
if fiAlias.Name() != "aaa-111" {
t.Errorf("Stat(aaa-111).Name() = %q, want 'aaa-111'", fiAlias.Name())
}
}
// TestEachNestedStructure tests nested Blueprint nodes
func TestEachNestedStructure(t *testing.T) {
// Outer instances (like sessions)
outerBindings := []struct {
ID string
Name string
}{
{ID: "sess-1", Name: "session1"},
}
// Inner instances (like agents within a session)
innerBindings := []struct {
ID string
Name string
}{
{ID: "agent-a", Name: "agentA"},
{ID: "agent-b", Name: "agentB"},
}
spec := DirNode("/",
Each("{outer}", func(ctx HandlerCtx) ([]Binding, error) {
var out []Binding
for _, d := range outerBindings {
data := d
out = append(out, Binding{
Name: data.Name,
Aliases: []string{data.ID},
})
}
return out, nil
},
FileNode("outer-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("outer\n"), nil
})),
DirNode("inner-dir",
Each("{inner}", func(ctx HandlerCtx) ([]Binding, error) {
var out []Binding
for _, d := range innerBindings {
data := d
out = append(out, Binding{
Name: data.Name,
Aliases: []string{data.ID},
})
}
return out, nil
},
FileNode("inner-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("inner\n"), nil
})),
),
),
),
)
tree := BuildTree(spec, HandlerCtx{})
// Test: Navigate to nested blueprint
entries, err := tree.Readdir("session1/inner-dir")
if err != nil {
t.Fatalf("Readdir(session1/inner-dir): %v", err)
}
if len(entries) != 2 {
t.Errorf("Readdir(session1/inner-dir): got %d entries, want 2", len(entries))
}
// Test: Read deeply nested file
f, err := tree.Open("session1/inner-dir/agentA/inner-file")
if err != nil {
t.Fatalf("Open nested: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read nested: %v", err)
}
if string(data) != "inner\n" {
t.Errorf("Read nested = %q, want %q", data, "inner\n")
}
// Test: Access nested by alias
f2, err := tree.Open("sess-1/inner-dir/agent-b/inner-file")
if err != nil {
t.Fatalf("Open nested by alias: %v", err)
}
data2, err := f2.Read()
if err != nil {
t.Fatalf("Read nested by alias: %v", err)
}
if string(data2) != "inner\n" {
t.Errorf("Read nested by alias = %q, want %q", data2, "inner\n")
}
}
// TestEachContextPropagation tests that context is properly propagated
func TestEachContextPropagation(t *testing.T) {
// Use a channel to capture the context value seen by the handler
seen := make(chan string, 1)
spec := DirNode("/",
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
return []Binding{
{
Name: "test-instance",
UID: "test-uid",
GID: "test-gid",
},
}, nil
},
FileNode("check", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
seen <- "handler-called"
return []byte("ok\n"), nil
})),
),
)
tree := BuildTree(spec, HandlerCtx{})
f, err := tree.Open("test-instance/check")
if err != nil {
t.Fatalf("Open: %v", err)
}
_, err = f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
select {
case v := <-seen:
if v != "handler-called" {
t.Errorf("unexpected seen value: %s", v)
}
default:
t.Error("handler was not called")
}
}
// TestEachUIDGIDInheritance tests UID/GID resolution
func TestEachUIDGIDInheritance(t *testing.T) {
spec := DirNode("/", UID("root-uid"), GID("root-gid"),
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
return []Binding{
{Name: "with-uid", UID: "custom-uid", GID: "custom-gid"},
{Name: "without-uid"}, // should inherit
}, nil
},
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("data\n"), nil
})),
),
)
tree := BuildTree(spec, HandlerCtx{})
// Test: instance with custom UID/GID
fi1, err := tree.Stat("with-uid")
if err != nil {
t.Fatalf("Stat(with-uid): %v", err)
}
sfi1, ok := fi1.(*SyntheticFileInfo)
if !ok {
t.Fatalf("expected SyntheticFileInfo, got %T", fi1)
}
if sfi1.UID_ != "custom-uid" {
t.Errorf("with-uid UID = %q, want 'custom-uid'", sfi1.UID_)
}
if sfi1.GID_ != "custom-gid" {
t.Errorf("with-uid GID = %q, want 'custom-gid'", sfi1.GID_)
}
// Test: instance without UID/GID should inherit from parent
fi2, err := tree.Stat("without-uid")
if err != nil {
t.Fatalf("Stat(without-uid): %v", err)
}
sfi2, ok := fi2.(*SyntheticFileInfo)
if !ok {
t.Fatalf("expected SyntheticFileInfo, got %T", fi2)
}
// Should inherit from root
if sfi2.UID_ != "root-uid" {
t.Errorf("without-uid UID = %q, want 'root-uid'", sfi2.UID_)
}
if sfi2.GID_ != "root-gid" {
t.Errorf("without-uid GID = %q, want 'root-gid'", sfi2.GID_)
}
}
// TestEachRename tests the rename callback on instances
func TestEachRename(t *testing.T) {
renamed := ""
spec := DirNode("/",
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
return []Binding{
{
Name: "original",
Rename: func(newName string) error {
renamed = newName
return nil
},
},
}, nil
},
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("data\n"), nil
})),
),
)
tree := BuildTree(spec, HandlerCtx{})
// Perform rename
err := tree.Rename("original", "new-name")
if err != nil {
t.Fatalf("Rename: %v", err)
}
if renamed != "new-name" {
t.Errorf("rename callback got %q, want 'new-name'", renamed)
}
}
// TestEachEmptyBindings tests behavior when no instances exist
func TestEachEmptyBindings(t *testing.T) {
spec := DirNode("/",
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
return nil, nil // empty
},
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("data\n"), nil
})),
),
)
tree := BuildTree(spec, HandlerCtx{})
// List should return empty
entries, err := tree.List()
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 0 {
t.Errorf("List: got %d entries, want 0", len(entries))
}
// Access non-existent should fail
_, err = tree.Stat("nonexistent")
if err == nil {
t.Error("Stat(nonexistent) should fail when no instances")
}
}
// TestEachMixedStaticAndDynamic tests Blueprint alongside static children
func TestEachMixedStaticAndDynamic(t *testing.T) {
spec := DirNode("/",
FileNode("static-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("static\n"), nil
})),
DirNode("static-dir",
FileNode("nested", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("nested-static\n"), nil
})),
),
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
return []Binding{
{Name: "dynamic1"},
{Name: "dynamic2"},
}, nil
},
FileNode("dynamic-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("dynamic\n"), nil
})),
),
)
tree := BuildTree(spec, HandlerCtx{})
entries, err := tree.List()
if err != nil {
t.Fatalf("List: %v", err)
}
// Should have: static-file, static-dir, dynamic1, dynamic2
names := make(map[string]bool)
for _, e := range entries {
names[e.Name()] = true
}
expected := []string{"static-file", "static-dir", "dynamic1", "dynamic2"}
for _, name := range expected {
if !names[name] {
t.Errorf("List: missing %q", name)
}
}
// Read from static
f1, err := tree.Open("static-file")
if err != nil {
t.Fatalf("Open(static-file): %v", err)
}
d1, _ := f1.Read()
if string(d1) != "static\n" {
t.Errorf("static-file = %q", d1)
}
// Read from dynamic
f2, err := tree.Open("dynamic1/dynamic-file")
if err != nil {
t.Fatalf("Open(dynamic1/dynamic-file): %v", err)
}
d2, _ := f2.Read()
if string(d2) != "dynamic\n" {
t.Errorf("dynamic-file = %q", d2)
}
}
// TestEachStatMode tests that blueprint instances get correct file mode
func TestEachStatMode(t *testing.T) {
spec := DirNode("/",
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
return []Binding{{Name: "instance"}}, nil
},
FileNode("readable", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("r\n"), nil
})),
FileNode("writable", 0666,
Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("rw\n"), nil
}),
Write(func(ctx HandlerCtx, data []byte) error {
return nil
}),
),
FileNode("executable", 0555, Read(func(ctx HandlerCtx) ([]byte, error) {
return []byte("#!/bin/sh\n"), nil
})),
),
)
tree := BuildTree(spec, HandlerCtx{})
tests := []struct {
path string
mode os.FileMode
}{
{"instance", os.ModeDir | 0755}, // directory
{"instance/readable", 0444},
{"instance/writable", 0666},
{"instance/executable", 0555},
}
for _, tc := range tests {
fi, err := tree.Stat(tc.path)
if err != nil {
t.Errorf("Stat(%q): %v", tc.path, err)
continue
}
if fi.Mode() != tc.mode {
t.Errorf("Stat(%q).Mode() = %o, want %o", tc.path, fi.Mode(), tc.mode)
}
}
}

View File

@ -12,6 +12,7 @@ import (
"ollie/agent"
"ollie/backend"
"ollie/elevate"
"ollie/fsedsl"
"ollie/toolsrv"
)
@ -31,7 +32,7 @@ func readHelp(_ HandlerCtx) ([]byte, error) {
func init() {
helpFn = func() string {
helpOnce.Do(func() {
helpText = GenerateHelp(treeSpec)
helpText = fsedsl.GenerateHelp(treeSpec)
})
return helpText
}

View File

@ -6,6 +6,7 @@ import (
"os/user"
"ollie/elevate"
"ollie/fsedsl"
olog "ollie/log"
"ollie/skills"
coresession "ollie/session"
@ -56,7 +57,7 @@ func NewRoot(cfg Config) *Tree {
// Build the tree from the spec.
// Validation happens inside BuildTree.
tree := BuildTree(treeSpec, ctx)
tree := fsedsl.BuildTree(treeSpec, ctx)
ctx.Root = tree // now handlers can access root tree via ctx.Root
// Initialize root state.

View File

@ -4,6 +4,26 @@ package fs
// Every file and directory is declared here with Doc() descriptions.
// treeSpecUID/GID are set at NewRoot() time from the current system user.
import "ollie/fsedsl"
// Local aliases for fsedsl constructors, specialized to HandlerCtx.
var (
DirNode = fsedsl.DirNode[HandlerCtx]
FileNode = fsedsl.FileNode[HandlerCtx]
Each = fsedsl.Each[HandlerCtx]
Doc = fsedsl.Doc[HandlerCtx]
Read = fsedsl.Read[HandlerCtx]
Write = fsedsl.Write[HandlerCtx]
Stream = fsedsl.Stream[HandlerCtx]
BlockOnce = fsedsl.BlockOnce[HandlerCtx]
Request = fsedsl.Request[HandlerCtx]
Remove = fsedsl.RemoveNode[HandlerCtx]
Stat = fsedsl.StatOverride[HandlerCtx]
UID = fsedsl.UID[HandlerCtx]
GID = fsedsl.GID[HandlerCtx]
Bind = fsedsl.Bind[HandlerCtx]
)
var treeSpecUID, treeSpecGID string
var treeSpec = DirNode("/",

View File

@ -2,6 +2,8 @@ package fs
import (
"testing"
"ollie/fsedsl"
)
// --- helpers ---
@ -136,18 +138,18 @@ func checkStoreContract(t *testing.T, s FileTree, name string) {
// ===== Tree =====
func TestTreeContract(t *testing.T) {
checkStoreContract(t, NewTree([]string{t.TempDir()}, 0644), "test-file")
checkStoreContract(t, fsedsl.NewTree([]string{t.TempDir()}, 0644), "test-file")
}
func TestTreeCreateMkdirError(t *testing.T) {
fd := NewTree([]string{"/nonexistent/path"}, 0644)
fd := fsedsl.NewTree([]string{"/nonexistent/path"}, 0644)
if err := fd.Create("f"); err == nil {
t.Error("Create should fail when dir doesn't exist")
}
}
func TestTreePutMkdirError(t *testing.T) {
fd := NewTree([]string{"/nonexistent/path"}, 0644)
fd := fsedsl.NewTree([]string{"/nonexistent/path"}, 0644)
e, err := fd.Open("f")
if err != nil {
t.Fatalf("Open: %v", err)

View File

@ -107,12 +107,12 @@ func main() {
## Integration Guide
For larger projects, create a package that wraps fsedsl with your context type:
For larger projects, create an adapter package that specializes fsedsl for your context type.
### Step 1: Define your context type
### Step 1: Define your context type and type aliases
```go
// myproject/fs/types.go
// myproject/fs/adapter.go
package fs
import "git.lneely.de/lkn/fsedsl"
@ -124,7 +124,7 @@ type Ctx struct {
Session *Session
}
// Type aliases for cleaner code
// Type aliases for external use
type FsNodeDecl = fsedsl.FsNodeDecl[Ctx]
type NodeOption = fsedsl.NodeOption[Ctx]
type Binding = fsedsl.Binding[Ctx]
@ -132,51 +132,36 @@ type Tree = fsedsl.Tree
type File = fsedsl.File
```
### Step 2: Create wrapper functions (optional)
### Step 2: Create local function aliases
If you prefer not to repeat `[Ctx]` everywhere:
```go
// myproject/fs/edsl.go
package fs
import (
"os"
"git.lneely.de/lkn/fsedsl"
)
func DirNode(name string, args ...any) FsNodeDecl {
return fsedsl.DirNode[Ctx](name, args...)
}
func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
return fsedsl.FileNode[Ctx](name, mode, opts...)
}
func Each(name string, bindings func(Ctx) ([]Binding, error), args ...any) FsNodeDecl {
return fsedsl.Each[Ctx](name, bindings, args...)
}
func Read(fn func(Ctx) ([]byte, error)) NodeOption { return fsedsl.Read[Ctx](fn) }
func Write(fn func(Ctx, []byte) error) NodeOption { return fsedsl.Write[Ctx](fn) }
func Doc(desc string) NodeOption { return fsedsl.Doc[Ctx](desc) }
// ... other options as needed
func BuildTree(spec FsNodeDecl, rootCtx Ctx) *Tree {
return fsedsl.BuildTree(spec, rootCtx)
}
func GenerateHelp(root FsNodeDecl) string {
return fsedsl.GenerateHelp(root)
}
```
### Step 3: Define your filesystem spec
In the file where you define your spec, create package-local variable aliases
that bake in your context type. This keeps the spec clean without requiring
`[Ctx]` on every call:
```go
// myproject/fs/spec.go
package fs
import "git.lneely.de/lkn/fsedsl"
// Local aliases for fsedsl constructors, specialized to Ctx.
var (
DirNode = fsedsl.DirNode[Ctx]
FileNode = fsedsl.FileNode[Ctx]
Each = fsedsl.Each[Ctx]
Doc = fsedsl.Doc[Ctx]
Read = fsedsl.Read[Ctx]
Write = fsedsl.Write[Ctx]
Stream = fsedsl.Stream[Ctx]
BlockOnce = fsedsl.BlockOnce[Ctx]
Request = fsedsl.Request[Ctx]
Remove = fsedsl.RemoveNode[Ctx]
Stat = fsedsl.StatOverride[Ctx]
UID = fsedsl.UID[Ctx]
GID = fsedsl.GID[Ctx]
Bind = fsedsl.Bind[Ctx]
)
var Spec = DirNode("/",
FileNode("version", 0444,
Doc("API version"),
@ -219,17 +204,20 @@ func userBindings(ctx Ctx) ([]Binding, error) {
}
```
### Step 4: Build and use the tree
### Step 3: Build and use the tree
```go
// myproject/main.go
package main
import "myproject/fs"
import (
"myproject/fs"
"git.lneely.de/lkn/fsedsl"
)
func main() {
ctx := fs.Ctx{DB: connectDB()}
tree := fs.BuildTree(fs.Spec, ctx)
tree := fsedsl.BuildTree(fs.Spec, ctx)
// Use tree.List(), tree.Open(), tree.Stat(), etc.
}