fsedsl: remove Adapter, use generic functions directly

The Adapter was pure boilerplate - every method just delegated to the
package-level generic function. Removing it simplifies the API:

- Delete fsedsl/adapter.go
- Update fs/adapter.go to call fsedsl functions directly
- Update README.md to document direct usage
- Remove adapter tests (now redundant)

Coverage remains at 94.8%.
This commit is contained in:
Levi Neely 2026-08-04 12:33:03 +02:00
parent 44f5238538
commit ae6b833d1e
4 changed files with 132 additions and 558 deletions

View File

@ -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)
}

View File

@ -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
```

View File

@ -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)
}

View File

@ -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
// ══════════════════════════════════════════════════════════════════