diff --git a/fs/adapter.go b/fs/adapter.go index 54de363..1206f7c 100644 --- a/fs/adapter.go +++ b/fs/adapter.go @@ -1,7 +1,8 @@ package fs -// This file builds an fsedsl Adapter specialized for ollie's HandlerCtx. -// The adapter provides type-safe EDSL functions for declaring the 9P namespace. +// 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. import ( "context" @@ -43,12 +44,6 @@ type HandlerCtx struct { Data any } -// ── Adapter instance ───────────────────────────────────────────── - -// edsl is the fsedsl Adapter specialized for HandlerCtx. -// All EDSL functions below delegate to this adapter. -var edsl = fsedsl.NewAdapter[HandlerCtx]() - // ── Type aliases from fsedsl ───────────────────────────────────── // FsNodeDecl describes one node in the 9P namespace. @@ -99,85 +94,85 @@ const ( BindData = fsedsl.BindData ) -// ── EDSL constructors (via adapter) ────────────────────────────── +// ── EDSL constructors ──────────────────────────────────────────── // DirNode declares a static directory node. func DirNode(name string, args ...any) FsNodeDecl { - return edsl.DirNode(name, args...) + 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 edsl.Each(name, bindings, args...) + return fsedsl.Each[HandlerCtx](name, bindings, args...) } // FileNode declares a file node (non-directory). func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl { - return edsl.FileNode(name, mode, opts...) + return fsedsl.FileNode[HandlerCtx](name, mode, opts...) } -// ── Options (via adapter) ──────────────────────────────────────── +// ── Options ────────────────────────────────────────────────────── func Read(fn func(HandlerCtx) ([]byte, error)) NodeOption { - return edsl.Read(fn) + return fsedsl.Read[HandlerCtx](fn) } func Write(fn func(HandlerCtx, []byte) error) NodeOption { - return edsl.Write(fn) + return fsedsl.Write[HandlerCtx](fn) } func Stream(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption { - return edsl.Stream(fn) + return fsedsl.Stream[HandlerCtx](fn) } func BlockOnce(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption { - return edsl.BlockOnce(fn) + return fsedsl.BlockOnce[HandlerCtx](fn) } func Request(fn func(HandlerCtx, []byte) ([]byte, error)) NodeOption { - return edsl.Request(fn) + return fsedsl.Request[HandlerCtx](fn) } func CreateFile(fn func(HandlerCtx, string) error) NodeOption { - return edsl.CreateFile(fn) + return fsedsl.CreateFile[HandlerCtx](fn) } func Remove(fn func(HandlerCtx) error) NodeOption { - return edsl.Remove(fn) + return fsedsl.RemoveNode[HandlerCtx](fn) } func Stat(fn func() os.FileInfo) NodeOption { - return edsl.Stat(fn) + return fsedsl.StatOverride[HandlerCtx](fn) } func UID(uid string) NodeOption { - return edsl.UID(uid) + return fsedsl.UID[HandlerCtx](uid) } func GID(gid string) NodeOption { - return edsl.GID(gid) + return fsedsl.GID[HandlerCtx](gid) } func Alias(names ...string) NodeOption { - return edsl.Alias(names...) + 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 edsl.Bind(targets...) + return fsedsl.Bind[HandlerCtx](targets...) } // Doc sets a human-readable description for the node. func Doc(desc string) NodeOption { - return edsl.Doc(desc) + return fsedsl.Doc[HandlerCtx](desc) } -// ── Tree construction (via adapter) ────────────────────────────── +// ── Tree construction ──────────────────────────────────────────── // BuildTree walks an FsNodeDecl spec and produces a *Tree. func BuildTree(spec FsNodeDecl, rootCtx HandlerCtx) *Tree { - return edsl.BuildTree(spec, rootCtx) + return fsedsl.BuildTree(spec, rootCtx) } // NewTree creates a Tree backed by one or more directories. @@ -213,5 +208,5 @@ func DirEntry(name string, mode os.FileMode) os.DirEntry { // GenerateHelp produces help text from a filesystem spec tree. func GenerateHelp(root FsNodeDecl) string { - return edsl.GenerateHelp(root) + return fsedsl.GenerateHelp(root) } diff --git a/fsedsl/README.md b/fsedsl/README.md index 047eae3..a92ca14 100644 --- a/fsedsl/README.md +++ b/fsedsl/README.md @@ -8,7 +8,6 @@ The library produces a generic `Tree` that can be used with any filesystem proto - **Declarative filesystem specification**: Define your namespace structure with `DirNode`, `FileNode`, and `Each` constructors - **Generic context propagation**: Type-safe context passing through the tree via `Binding.Applier` -- **Adapter pattern**: Create a type-specialized adapter for ergonomic use - **Dynamic bindings**: `Each` nodes iterate over runtime-determined collections - **Auto-generated help**: `GenerateHelp` produces documentation from `Doc()` annotations - **Standard filesystem semantics**: Implements read, write, stat, list, create, delete, rename @@ -30,16 +29,90 @@ Or reference it directly from the ollie repo: go mod edit -replace=git.lneely.de/lkn/fsedsl=git.lneely.de/lkn/ollie/fsedsl@main ``` +## Quick Start + +```go +package main + +import ( + "fmt" + "git.lneely.de/lkn/fsedsl" +) + +type Ctx struct { + User *User +} + +type User struct { + ID string + Name string +} + +var users = []*User{ + {ID: "1", Name: "alice"}, + {ID: "2", Name: "bob"}, +} + +func main() { + // Declare the filesystem spec + spec := fsedsl.DirNode[Ctx]("/", + fsedsl.FileNode[Ctx]("version", 0444, + fsedsl.Doc[Ctx]("API version"), + fsedsl.Read(func(ctx Ctx) ([]byte, error) { + return []byte("1.0\n"), nil + }), + ), + fsedsl.Each[Ctx]("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) { + var out []fsedsl.Binding[Ctx] + for _, u := range users { + user := u // capture + out = append(out, fsedsl.Binding[Ctx]{ + Name: user.Name, + Aliases: []string{user.ID}, + Applier: func(c Ctx) Ctx { + c.User = user + return c + }, + }) + } + return out, nil + }, + fsedsl.FileNode[Ctx]("id", 0444, + fsedsl.Read(func(ctx Ctx) ([]byte, error) { + return []byte(ctx.User.ID + "\n"), nil + }), + ), + fsedsl.FileNode[Ctx]("name", 0444, + fsedsl.Read(func(ctx Ctx) ([]byte, error) { + return []byte(ctx.User.Name + "\n"), nil + }), + ), + ), + ) + + // Build the tree + tree := fsedsl.BuildTree(spec, Ctx{}) + + // Use the tree + entries, _ := tree.List() + for _, e := range entries { + fmt.Println(e.Name()) + } + + f, _ := tree.Open("alice/name") + data, _ := f.Read() + fmt.Printf("alice/name: %s", data) +} +``` + ## Integration Guide -To integrate fsedsl into your project, you'll create an **adapter file** that specializes the generic library for your domain-specific context type. +For larger projects, create a package that wraps fsedsl with your context type: ### Step 1: Define your context type -Your context carries domain-specific data through the filesystem tree: - ```go -// myproject/fs/adapter.go +// myproject/fs/types.go package fs import "git.lneely.de/lkn/fsedsl" @@ -49,21 +122,9 @@ type Ctx struct { DB *Database User *User Session *Session - Data any // generic binding data } -``` -### Step 2: Create the adapter instance - -```go -// The adapter specializes fsedsl for our context type. -var edsl = fsedsl.NewAdapter[Ctx]() -``` - -### Step 3: Create type aliases for convenience - -```go -// Type aliases so callers don't need to write generic parameters +// Type aliases for cleaner code type FsNodeDecl = fsedsl.FsNodeDecl[Ctx] type NodeOption = fsedsl.NodeOption[Ctx] type Binding = fsedsl.Binding[Ctx] @@ -71,49 +132,52 @@ type Tree = fsedsl.Tree type File = fsedsl.File ``` -### Step 4: Export wrapper functions +### Step 2: Create wrapper functions (optional) -This provides a clean API without requiring callers to use the adapter directly: +If you prefer not to repeat `[Ctx]` everywhere: ```go -// Constructors +// myproject/fs/edsl.go +package fs + +import ( + "os" + "git.lneely.de/lkn/fsedsl" +) + func DirNode(name string, args ...any) FsNodeDecl { - return edsl.DirNode(name, args...) + return fsedsl.DirNode[Ctx](name, args...) } func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl { - return edsl.FileNode(name, mode, opts...) + return fsedsl.FileNode[Ctx](name, mode, opts...) } func Each(name string, bindings func(Ctx) ([]Binding, error), args ...any) FsNodeDecl { - return edsl.Each(name, bindings, args...) + return fsedsl.Each[Ctx](name, bindings, args...) } -// Options -func Read(fn func(Ctx) ([]byte, error)) NodeOption { return edsl.Read(fn) } -func Write(fn func(Ctx, []byte) error) NodeOption { return edsl.Write(fn) } -func Doc(desc string) NodeOption { return edsl.Doc(desc) } +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 -// Tree construction func BuildTree(spec FsNodeDecl, rootCtx Ctx) *Tree { - return edsl.BuildTree(spec, rootCtx) + return fsedsl.BuildTree(spec, rootCtx) } func GenerateHelp(root FsNodeDecl) string { - return edsl.GenerateHelp(root) + return fsedsl.GenerateHelp(root) } ``` -### Step 5: Define your filesystem spec - -Now you can declare your filesystem in a separate file: +### Step 3: Define your filesystem spec ```go // myproject/fs/spec.go package fs -var spec = DirNode("/", +var Spec = DirNode("/", FileNode("version", 0444, Doc("API version"), Read(func(ctx Ctx) ([]byte, error) { @@ -129,11 +193,6 @@ var spec = DirNode("/", return []byte(ctx.User.Name + "\n"), nil }), ), - FileNode("email", 0444, - Read(func(ctx Ctx) ([]byte, error) { - return []byte(ctx.User.Email + "\n"), nil - }), - ), ), ), ) @@ -160,7 +219,7 @@ func userBindings(ctx Ctx) ([]Binding, error) { } ``` -### Step 6: Build and use the tree +### Step 4: Build and use the tree ```go // myproject/main.go @@ -170,129 +229,21 @@ import "myproject/fs" func main() { ctx := fs.Ctx{DB: connectDB()} - tree := fs.BuildTree(fs.Spec(), ctx) + tree := fs.BuildTree(fs.Spec, ctx) // Use tree.List(), tree.Open(), tree.Stat(), etc. } ``` -## Complete Adapter Example - -See [ollie/fs/adapter.go](https://git.lneely.de/lkn/ollie/src/branch/main/fs/adapter.go) for a real-world example of integrating fsedsl. - -## Usage - -### Using the Adapter (recommended) - -The `Adapter` type provides all EDSL functions specialized for your context type: - -```go -package main - -import ( - "fmt" - "git.lneely.de/lkn/fsedsl" -) - -type Ctx struct { - User *User -} - -type User struct { - ID string - Name string -} - -var users = []*User{ - {ID: "1", Name: "alice"}, - {ID: "2", Name: "bob"}, -} - -func main() { - // Create an adapter specialized for our context type - edsl := fsedsl.NewAdapter[Ctx]() - - // Build the spec using adapter methods - spec := edsl.DirNode("/", - edsl.FileNode("version", 0444, - edsl.Doc("API version"), - edsl.Read(func(ctx Ctx) ([]byte, error) { - return []byte("1.0\n"), nil - }), - ), - edsl.Each("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) { - var out []fsedsl.Binding[Ctx] - for _, u := range users { - user := u // capture - out = append(out, fsedsl.Binding[Ctx]{ - Name: user.Name, - Aliases: []string{user.ID}, - Applier: func(c Ctx) Ctx { - c.User = user - return c - }, - }) - } - return out, nil - }, - edsl.FileNode("id", 0444, - edsl.Read(func(ctx Ctx) ([]byte, error) { - return []byte(ctx.User.ID + "\n"), nil - }), - ), - edsl.FileNode("name", 0666, - edsl.Read(func(ctx Ctx) ([]byte, error) { - return []byte(ctx.User.Name + "\n"), nil - }), - ), - ), - ) - - // Build the tree - tree := edsl.BuildTree(spec, Ctx{}) - - // Use the tree - entries, _ := tree.List() - for _, e := range entries { - fmt.Println(e.Name()) - } - - f, _ := tree.Open("alice/name") - data, _ := f.Read() - fmt.Printf("alice/name: %s", data) -} -``` - -### Using standalone functions - -You can also use the generic functions directly without an adapter: - -```go -spec := fsedsl.DirNode[Ctx]("/", - fsedsl.FileNode[Ctx]("version", 0444, - fsedsl.Read(func(ctx Ctx) ([]byte, error) { - return []byte("1.0\n"), nil - }), - ), -) -tree := fsedsl.BuildTree(spec, Ctx{}) -``` - ## API Reference -### Adapter - -```go -edsl := fsedsl.NewAdapter[MyCtx]() -``` - ### Constructors | Function | Description | |----------|-------------| -| `DirNode(name, children/options...)` | Static directory | -| `FileNode(name, mode, options...)` | File with handlers | -| `Each(pattern, bindingsFn, children/options...)` | Dynamic directory template | +| `DirNode[Ctx](name, children/options...)` | Static directory | +| `FileNode[Ctx](name, mode, options...)` | File with handlers | +| `Each[Ctx](pattern, bindingsFn, children/options...)` | Dynamic directory template | ### Options @@ -307,9 +258,9 @@ edsl := fsedsl.NewAdapter[MyCtx]() | `UID(uid)` | Set owner | | `GID(gid)` | Set group | | `Alias(names...)` | Alternate lookup names | -| `Stat(fn)` | Custom stat handler | +| `StatOverride(fn)` | Custom stat handler | | `CreateFile(fn)` | File creation handler (directories) | -| `Remove(fn)` | Removal handler | +| `RemoveNode(fn)` | Removal handler | ### Binding @@ -331,7 +282,7 @@ fsedsl.Binding[Ctx]{ ### Tree Operations ```go -tree := edsl.BuildTree(spec, rootCtx) +tree := fsedsl.BuildTree(spec, rootCtx) tree.List() // List entries tree.Stat(name) // Get file info @@ -345,7 +296,7 @@ tree.Readdir(path) // List subdirectory ### Help Generation ```go -help := edsl.GenerateHelp(spec) +help := fsedsl.GenerateHelp(spec) // Returns formatted documentation from Doc() annotations ``` diff --git a/fsedsl/adapter.go b/fsedsl/adapter.go deleted file mode 100644 index be92597..0000000 --- a/fsedsl/adapter.go +++ /dev/null @@ -1,119 +0,0 @@ -package fsedsl - -import ( - "context" - "os" -) - -// Adapter provides the EDSL functions specialized for a specific context type. -// Create one with NewAdapter[YourCtx]() and use its methods to build filesystem specs. -type Adapter[Ctx any] struct{} - -// NewAdapter creates an Adapter for the given context type. -// -// Example: -// -// type MyCtx struct { User *User } -// edsl := fsedsl.NewAdapter[MyCtx]() -// spec := edsl.DirNode("/", -// edsl.FileNode("version", 0444, edsl.Doc("API version")), -// ) -func NewAdapter[Ctx any]() *Adapter[Ctx] { - return &Adapter[Ctx]{} -} - -// ── Constructors ───────────────────────────────────────────────── - -// DirNode declares a static directory node. -func (a *Adapter[Ctx]) DirNode(name string, args ...any) FsNodeDecl[Ctx] { - return DirNode[Ctx](name, args...) -} - -// FileNode declares a file node (non-directory). -func (a *Adapter[Ctx]) FileNode(name string, mode os.FileMode, opts ...NodeOption[Ctx]) FsNodeDecl[Ctx] { - return FileNode[Ctx](name, mode, opts...) -} - -// Each declares a directory that is instantiated for each binding in a collection. -func (a *Adapter[Ctx]) Each(name string, bindings func(Ctx) ([]Binding[Ctx], error), args ...any) FsNodeDecl[Ctx] { - return Each[Ctx](name, bindings, args...) -} - -// ── Options ────────────────────────────────────────────────────── - -// Read sets the read handler for a file node. -func (a *Adapter[Ctx]) Read(fn func(Ctx) ([]byte, error)) NodeOption[Ctx] { - return Read[Ctx](fn) -} - -// Write sets the write handler for a file node. -func (a *Adapter[Ctx]) Write(fn func(Ctx, []byte) error) NodeOption[Ctx] { - return Write[Ctx](fn) -} - -// Stream sets a streaming read handler that blocks indefinitely. -func (a *Adapter[Ctx]) Stream(fn func(Ctx, context.Context, string) ([]byte, string, error)) NodeOption[Ctx] { - return Stream[Ctx](fn) -} - -// BlockOnce sets a blocking read handler that returns once. -func (a *Adapter[Ctx]) BlockOnce(fn func(Ctx, context.Context, string) ([]byte, string, error)) NodeOption[Ctx] { - return BlockOnce[Ctx](fn) -} - -// Request sets a request-response handler (rdwr pattern). -func (a *Adapter[Ctx]) Request(fn func(Ctx, []byte) ([]byte, error)) NodeOption[Ctx] { - return Request[Ctx](fn) -} - -// CreateFile sets the file creation handler for a directory node. -func (a *Adapter[Ctx]) CreateFile(fn func(Ctx, string) error) NodeOption[Ctx] { - return CreateFile[Ctx](fn) -} - -// Remove sets the removal handler for a node. -func (a *Adapter[Ctx]) Remove(fn func(Ctx) error) NodeOption[Ctx] { - return RemoveNode[Ctx](fn) -} - -// Stat sets a custom stat handler that bypasses default metadata. -func (a *Adapter[Ctx]) Stat(fn func() os.FileInfo) NodeOption[Ctx] { - return StatOverride[Ctx](fn) -} - -// UID sets the owner for a node. -func (a *Adapter[Ctx]) UID(uid string) NodeOption[Ctx] { - return UID[Ctx](uid) -} - -// GID sets the group for a node. -func (a *Adapter[Ctx]) GID(gid string) NodeOption[Ctx] { - return GID[Ctx](gid) -} - -// Alias adds alternate names for resolving this node. -func (a *Adapter[Ctx]) Alias(names ...string) NodeOption[Ctx] { - return Alias[Ctx](names...) -} - -// Bind declares how Each bindings populate the context (advisory). -func (a *Adapter[Ctx]) Bind(targets ...BindTarget) NodeOption[Ctx] { - return Bind[Ctx](targets...) -} - -// Doc sets a human-readable description for the node. -func (a *Adapter[Ctx]) Doc(desc string) NodeOption[Ctx] { - return Doc[Ctx](desc) -} - -// ── Tree construction ──────────────────────────────────────────── - -// BuildTree walks an FsNodeDecl spec and produces a *Tree. -func (a *Adapter[Ctx]) BuildTree(spec FsNodeDecl[Ctx], rootCtx Ctx) *Tree { - return BuildTree[Ctx](spec, rootCtx) -} - -// GenerateHelp produces documentation from a filesystem spec tree. -func (a *Adapter[Ctx]) GenerateHelp(root FsNodeDecl[Ctx]) string { - return GenerateHelp[Ctx](root) -} diff --git a/fsedsl/decl_test.go b/fsedsl/decl_test.go index 052c044..2426707 100644 --- a/fsedsl/decl_test.go +++ b/fsedsl/decl_test.go @@ -13,259 +13,6 @@ type TestCtx struct { UserID string } -// ══════════════════════════════════════════════════════════════════ -// Adapter tests -// ══════════════════════════════════════════════════════════════════ - -func TestAdapter(t *testing.T) { - edsl := NewAdapter[TestCtx]() - - // Test all adapter methods produce equivalent results to direct functions - spec := edsl.DirNode("/", - edsl.FileNode("version", 0444, - edsl.Doc("API version"), - edsl.Read(func(ctx TestCtx) ([]byte, error) { - return []byte("1.0\n"), nil - }), - ), - edsl.FileNode("config", 0666, - edsl.Read(func(ctx TestCtx) ([]byte, error) { - return []byte("cfg\n"), nil - }), - edsl.Write(func(ctx TestCtx, data []byte) error { - return nil - }), - edsl.UID("root"), - edsl.GID("wheel"), - edsl.Alias("configuration"), - ), - edsl.Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) { - return []Binding[TestCtx]{{Name: "item1"}}, nil - }, - edsl.FileNode("data", 0444, edsl.Read(func(ctx TestCtx) ([]byte, error) { - return []byte("data\n"), nil - })), - edsl.Bind(BindData), - ), - ) - - tree := edsl.BuildTree(spec, TestCtx{}) - if tree == nil { - t.Fatal("BuildTree returned nil") - } - - // Test List - entries, err := tree.List() - if err != nil { - t.Fatalf("List: %v", err) - } - if len(entries) != 3 { // version, config, item1 - t.Errorf("List: got %d entries, want 3", len(entries)) - } - - // Test GenerateHelp via adapter - help := edsl.GenerateHelp(spec) - if help == "" { - t.Error("GenerateHelp returned empty") - } - if !contains(help, "API version") { - t.Error("help missing 'API version'") - } -} - -func TestAdapterStream(t *testing.T) { - edsl := NewAdapter[TestCtx]() - - streamCalled := false - spec := edsl.DirNode("/", - edsl.FileNode("stream", 0444, - edsl.Stream(func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) { - streamCalled = true - return []byte("streamed"), "next", nil - }), - ), - ) - - tree := edsl.BuildTree(spec, TestCtx{}) - f, err := tree.Open("stream") - if err != nil { - t.Fatalf("Open: %v", err) - } - if !f.StreamMode() { - t.Error("expected StreamMode() = true") - } - data, next, err := f.BlockingRead(context.Background(), "") - if err != nil { - t.Fatalf("BlockingRead: %v", err) - } - if !streamCalled { - t.Error("stream handler not called") - } - if string(data) != "streamed" { - t.Errorf("data = %q, want 'streamed'", data) - } - if next != "next" { - t.Errorf("next = %q, want 'next'", next) - } -} - -func TestAdapterBlockOnce(t *testing.T) { - edsl := NewAdapter[TestCtx]() - - blockCalled := false - spec := edsl.DirNode("/", - edsl.FileNode("block", 0444, - edsl.BlockOnce(func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) { - blockCalled = true - return []byte("blocked"), "done", nil - }), - ), - ) - - tree := edsl.BuildTree(spec, TestCtx{}) - f, err := tree.Open("block") - if err != nil { - t.Fatalf("Open: %v", err) - } - if !f.BlockingReadMode() { - t.Error("expected BlockingReadMode() = true") - } - data, next, err := f.BlockingRead(context.Background(), "") - if err != nil { - t.Fatalf("BlockingRead: %v", err) - } - if !blockCalled { - t.Error("block handler not called") - } - if string(data) != "blocked" { - t.Errorf("data = %q, want 'blocked'", data) - } - if next != "done" { - t.Errorf("next = %q, want 'done'", next) - } -} - -func TestAdapterRequest(t *testing.T) { - edsl := NewAdapter[TestCtx]() - - spec := edsl.DirNode("/", - edsl.FileNode("rdwr", 0666, - edsl.Request(func(ctx TestCtx, data []byte) ([]byte, error) { - return []byte("response:" + string(data)), nil - }), - ), - ) - - tree := edsl.BuildTree(spec, TestCtx{}) - f, err := tree.Open("rdwr") - if err != nil { - t.Fatalf("Open: %v", err) - } - if !f.RequestRespMode() { - t.Error("expected RequestRespMode() = true") - } - - // Write triggers request handler - err = f.Write([]byte("hello")) - if err != nil { - t.Fatalf("Write: %v", err) - } - - // Read returns response - data, err := f.Read() - if err != nil { - t.Fatalf("Read: %v", err) - } - if string(data) != "response:hello" { - t.Errorf("data = %q, want 'response:hello'", data) - } - - // Second read returns nil (result consumed) - data2, err := f.Read() - if err != nil { - t.Fatalf("Read2: %v", err) - } - if data2 != nil { - t.Errorf("second read = %q, want nil", data2) - } -} - -func TestAdapterCreateFile(t *testing.T) { - edsl := NewAdapter[TestCtx]() - - var created string - spec := edsl.DirNode("/", - edsl.FileNode("dummy", 0444), - edsl.CreateFile(func(ctx TestCtx, name string) error { - created = name - return nil - }), - ) - - tree := edsl.BuildTree(spec, TestCtx{}) - err := tree.Create("newfile") - if err != nil { - t.Fatalf("Create: %v", err) - } - if created != "newfile" { - t.Errorf("created = %q, want 'newfile'", created) - } -} - -func TestAdapterRemove(t *testing.T) { - edsl := NewAdapter[TestCtx]() - - var removed bool - spec := edsl.DirNode("/", - edsl.Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) { - return []Binding[TestCtx]{{Name: "item", Remove: func() { removed = true }}}, nil - }, - edsl.FileNode("data", 0444), - edsl.Remove(func(ctx TestCtx) error { - removed = true - return nil - }), - ), - ) - - tree := edsl.BuildTree(spec, TestCtx{}) - err := tree.Delete("item") - if err != nil { - t.Fatalf("Delete: %v", err) - } - if !removed { - t.Error("remove handler not called") - } -} - -func TestAdapterStat(t *testing.T) { - edsl := NewAdapter[TestCtx]() - - spec := edsl.DirNode("/", - edsl.FileNode("custom", 0444, - edsl.Stat(func() os.FileInfo { - return &SyntheticFileInfo{Name_: "custom", Mode_: 0555, Size_: 42} - }), - ), - ) - - tree := edsl.BuildTree(spec, TestCtx{}) - f, err := tree.Open("custom") - if err != nil { - t.Fatalf("Open: %v", err) - } - fi, err := f.Stat() - if err != nil { - t.Fatalf("Stat: %v", err) - } - if fi.Mode() != 0555 { - t.Errorf("Mode = %o, want 0555", fi.Mode()) - } - if fi.Size() != 42 { - t.Errorf("Size = %d, want 42", fi.Size()) - } -} - // ══════════════════════════════════════════════════════════════════ // NodeOption tests // ══════════════════════════════════════════════════════════════════