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:
parent
44f5238538
commit
ae6b833d1e
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
265
fsedsl/README.md
265
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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Reference in New Issue