virtfs: rewrite README for closure-capture model

Drop all references to generic context type parameter, Applier,
and type/function aliasing patterns. Document the actual API:
plain closures, pre-bound Children in Bindings, no generics.
This commit is contained in:
Ollie Agent 2026-08-11 17:22:39 +02:00
parent ee7426d628
commit 3a875da9f6
1 changed files with 62 additions and 167 deletions

View File

@ -1,33 +1,14 @@
# virtfs # virtfs
A Go library providing an embedded domain-specific language (EDSL) for declaring synthetic filesystems. Define your namespace structure statically while populating it dynamically at runtime. A Go library for declaring synthetic filesystems. Define your namespace structure statically, populate it dynamically at runtime with closures.
The library produces a generic `Tree` that can be used with any filesystem protocol (9P, FUSE, etc.) or as an in-memory filesystem for testing. Produces a `*Tree` usable with any filesystem protocol (9P, FUSE, etc.) or as an in-memory filesystem for testing.
## Features ## Design
- **Declarative filesystem specification**: Define your namespace structure with `DirNode`, `FileNode`, and `Each` constructors Handlers are plain closures that capture their state at binding time. There is no context type parameter — each handler already knows its target via closure capture.
- **Generic context propagation**: Type-safe context passing through the tree via `Binding.Applier`
- **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
## Installation Dynamic subtrees use `Each`: a bindings function returns `[]Binding`, where each binding carries pre-bound `Children` (fully-wired subtrees with their own closures).
The library is currently embedded in the [ollie](https://git.lneely.de/lkn/ollie) repository at `virtfs/`.
To use it in your project, add a replace directive to your `go.mod`:
```bash
# Clone or copy the virtfs directory to your project, then:
go mod edit -replace=git.lneely.de/lkn/virtfs=./virtfs
```
Or reference it directly from the ollie repo:
```bash
go mod edit -replace=git.lneely.de/lkn/virtfs=git.lneely.de/lkn/ollie/virtfs@main
```
## Quick Start ## Quick Start
@ -36,13 +17,9 @@ package main
import ( import (
"fmt" "fmt"
"git.lneely.de/lkn/virtfs" "ollie/virtfs"
) )
type Ctx struct {
User *User
}
type User struct { type User struct {
ID string ID string
Name string Name string
@ -54,46 +31,40 @@ var users = []*User{
} }
func main() { func main() {
// Declare the filesystem spec spec := virtfs.DirNode("/",
spec := virtfs.DirNode[Ctx]("/", virtfs.FileNode("version", 0444,
virtfs.FileNode[Ctx]("version", 0444, virtfs.Doc("API version"),
virtfs.Doc[Ctx]("API version"), virtfs.Read(func() ([]byte, error) {
virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte("1.0\n"), nil return []byte("1.0\n"), nil
}), }),
), ),
virtfs.Each[Ctx]("{user}", func(ctx Ctx) ([]virtfs.Binding[Ctx], error) { virtfs.Each("{user}", func() ([]virtfs.Binding, error) {
var out []virtfs.Binding[Ctx] var out []virtfs.Binding
for _, u := range users { for _, u := range users {
user := u // capture user := u // capture
out = append(out, virtfs.Binding[Ctx]{ out = append(out, virtfs.Binding{
Name: user.Name, Name: user.Name,
Aliases: []string{user.ID}, Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx { Children: []virtfs.FsNodeDecl{
c.User = user virtfs.FileNode("id", 0444,
return c virtfs.Read(func() ([]byte, error) {
return []byte(user.ID + "\n"), nil
}),
),
virtfs.FileNode("name", 0444,
virtfs.Read(func() ([]byte, error) {
return []byte(user.Name + "\n"), nil
}),
),
}, },
}) })
} }
return out, nil return out, nil
},
virtfs.FileNode[Ctx]("id", 0444,
virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.ID + "\n"), nil
}), }),
),
virtfs.FileNode[Ctx]("name", 0444,
virtfs.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.Name + "\n"), nil
}),
),
),
) )
// Build the tree tree := virtfs.BuildTree(spec)
tree := virtfs.BuildTree(spec, Ctx{})
// Use the tree
entries, _ := tree.List() entries, _ := tree.List()
for _, e := range entries { for _, e := range entries {
fmt.Println(e.Name()) fmt.Println(e.Name())
@ -105,142 +76,66 @@ func main() {
} }
``` ```
## Usage ## API
Define your context type, then create type and function aliases that bake it in:
```go
package fs
import "git.lneely.de/lkn/virtfs"
// Ctx carries runtime dependencies for handler functions.
type Ctx struct {
DB *Database
User *User
}
// Type aliases for external consumers
type (
FsNodeDecl = virtfs.FsNodeDecl[Ctx]
Binding = virtfs.Binding[Ctx]
Tree = virtfs.Tree
)
// Function aliases for clean spec definitions
var (
DirNode = virtfs.DirNode[Ctx]
FileNode = virtfs.FileNode[Ctx]
Each = virtfs.Each[Ctx]
Doc = virtfs.Doc[Ctx]
Read = virtfs.Read[Ctx]
Write = virtfs.Write[Ctx]
// ... add others as needed
)
var Spec = DirNode("/",
FileNode("version", 0444,
Doc("API version"),
Read(func(ctx Ctx) ([]byte, error) {
return []byte("1.0.0\n"), nil
}),
),
Each("{user}", userBindings,
FileNode("name", 0444,
Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.Name + "\n"), nil
}),
),
),
)
func userBindings(ctx Ctx) ([]Binding, error) {
users, _ := ctx.DB.ListUsers()
var out []Binding
for _, u := range users {
user := u
out = append(out, Binding{
Name: user.Username,
Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx { c.User = user; return c },
})
}
return out, nil
}
```
Build and use:
```go
tree := virtfs.BuildTree(fs.Spec, fs.Ctx{DB: db})
entries, _ := tree.List()
f, _ := tree.Open("alice/name")
data, _ := f.Read()
```
## API Reference
### Constructors ### Constructors
| Function | Description | | Function | Description |
|----------|-------------| |----------|-------------|
| `DirNode[Ctx](name, children/options...)` | Static directory | | `DirNode(name, children/options...)` | Static directory |
| `FileNode[Ctx](name, mode, options...)` | File with handlers | | `FileNode(name, mode, options...)` | File with handlers |
| `Each[Ctx](pattern, bindingsFn, children/options...)` | Dynamic directory template | | `Each(name, bindingsFn, options...)` | Dynamic directory — children come from bindings |
### Options ### Options (NodeOption)
| Option | Description | | Option | Signature | Description |
|--------|-------------| |--------|-----------|-------------|
| `Read(fn)` | Read handler: `func(Ctx) ([]byte, error)` | | `Read(fn)` | `func() ([]byte, error)` | Non-blocking read |
| `Write(fn)` | Write handler: `func(Ctx, []byte) error` | | `Write(fn)` | `func([]byte) error` | Write handler |
| `Stream(fn)` | Streaming read (blocks indefinitely) | | `Stream(fn)` | `func(context.Context, string) ([]byte, string, error)` | Blocking streaming read |
| `BlockOnce(fn)` | Blocking read (returns once) | | `BlockOnce(fn)` | `func(context.Context, string) ([]byte, string, error)` | Blocking read, returns once |
| `Request(fn)` | Request/response pattern (rdwr) | | `Request(fn)` | `func([]byte) ([]byte, error)` | Write-then-read (rdwr) |
| `Doc(desc)` | Human-readable description | | `Doc(desc)` | `string` | Human-readable description |
| `UID(uid)` | Set owner | | `UID(uid)` | `string` | Set owner |
| `GID(gid)` | Set group | | `GID(gid)` | `string` | Set group |
| `Alias(names...)` | Alternate lookup names | | `StatOverride(fn)` | `func() os.FileInfo` | Custom stat |
| `StatOverride(fn)` | Custom stat handler | | `RemoveNode(fn)` | `func() error` | Removal handler |
| `CreateFile(fn)` | File creation handler (directories) |
| `RemoveNode(fn)` | Removal handler |
### Binding ### Binding
Returned by the `Each` bindings function. Each binding represents one dynamic child.
```go ```go
virtfs.Binding[Ctx]{ virtfs.Binding{
Name: "filename", // Primary name Name: "alice",
Aliases: []string{"alt-name"}, // Alternate lookup names Aliases: []string{"user-id-123"},
UID: "owner", // Owner (optional) Children: []virtfs.FsNodeDecl{ ... }, // pre-bound subtree
GID: "group", // Group (optional) Remove: func() { ... }, // optional
Applier: func(c Ctx) Ctx { // Context mutation Rename: func(newName string) error { ... }, // optional
c.MyField = myValue
return c
},
Remove: func() { ... }, // Removal callback (optional)
Rename: func(newName string) error { ... }, // Rename callback (optional)
} }
``` ```
Children are fully-wired — their handlers are closures that already captured the specific instance (no applier/context needed).
### Tree Operations ### Tree Operations
```go ```go
tree := virtfs.BuildTree(spec, rootCtx) tree := virtfs.BuildTree(spec)
tree.List() // List entries tree.List() // list root entries
tree.Stat(name) // Get file info tree.Stat(name) // stat a path
tree.Open(name) // Open file for read/write tree.Open(name) // open file for read/write
tree.Create(name) // Create file tree.Create(name) // create file
tree.Delete(name) // Delete file tree.Delete(name) // remove file
tree.Rename(old, new) // Rename file tree.Rename(old, new) // rename file
tree.Readdir(path) // List subdirectory tree.Readdir(path) // list subdirectory
``` ```
### Help Generation ### Help Generation
```go ```go
help := virtfs.GenerateHelp(spec) help := virtfs.GenerateHelp(spec)
// Returns formatted documentation from Doc() annotations // Produces formatted docs from Doc() annotations
``` ```
## License ## License