diff --git a/fs/adapter.go b/fs/adapter.go index e50f149..a3a81ce 100644 --- a/fs/adapter.go +++ b/fs/adapter.go @@ -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) -} diff --git a/fs/adapter_test.go b/fs/adapter_test.go deleted file mode 100644 index 4099743..0000000 --- a/fs/adapter_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/fs/handlers.go b/fs/handlers.go index 84877ae..a6f254d 100644 --- a/fs/handlers.go +++ b/fs/handlers.go @@ -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 } diff --git a/fs/newroot.go b/fs/newroot.go index 9d1be2f..0daaa89 100644 --- a/fs/newroot.go +++ b/fs/newroot.go @@ -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. diff --git a/fs/spec.go b/fs/spec.go index cd41f8f..6ff0970 100644 --- a/fs/spec.go +++ b/fs/spec.go @@ -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("/", diff --git a/fs/tree_test.go b/fs/tree_test.go index 827b389..2945f7c 100644 --- a/fs/tree_test.go +++ b/fs/tree_test.go @@ -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) diff --git a/fsedsl/README.md b/fsedsl/README.md index a92ca14..f743b39 100644 --- a/fsedsl/README.md +++ b/fsedsl/README.md @@ -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. }