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
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
- **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
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.
## Installation
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
```
Dynamic subtrees use `Each`: a bindings function returns `[]Binding`, where each binding carries pre-bound `Children` (fully-wired subtrees with their own closures).
## Quick Start
@ -36,13 +17,9 @@ package main
import (
"fmt"
"git.lneely.de/lkn/virtfs"
"ollie/virtfs"
)
type Ctx struct {
User *User
}
type User struct {
ID string
Name string
@ -54,46 +31,40 @@ var users = []*User{
}
func main() {
// Declare the filesystem spec
spec := virtfs.DirNode[Ctx]("/",
virtfs.FileNode[Ctx]("version", 0444,
virtfs.Doc[Ctx]("API version"),
virtfs.Read(func(ctx Ctx) ([]byte, error) {
spec := virtfs.DirNode("/",
virtfs.FileNode("version", 0444,
virtfs.Doc("API version"),
virtfs.Read(func() ([]byte, error) {
return []byte("1.0\n"), nil
}),
),
virtfs.Each[Ctx]("{user}", func(ctx Ctx) ([]virtfs.Binding[Ctx], error) {
var out []virtfs.Binding[Ctx]
virtfs.Each("{user}", func() ([]virtfs.Binding, error) {
var out []virtfs.Binding
for _, u := range users {
user := u // capture
out = append(out, virtfs.Binding[Ctx]{
out = append(out, virtfs.Binding{
Name: user.Name,
Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx {
c.User = user
return c
Children: []virtfs.FsNodeDecl{
virtfs.FileNode("id", 0444,
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
},
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, Ctx{})
tree := virtfs.BuildTree(spec)
// Use the tree
entries, _ := tree.List()
for _, e := range entries {
fmt.Println(e.Name())
@ -105,142 +76,66 @@ func main() {
}
```
## Usage
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
## API
### Constructors
| Function | Description |
|----------|-------------|
| `DirNode[Ctx](name, children/options...)` | Static directory |
| `FileNode[Ctx](name, mode, options...)` | File with handlers |
| `Each[Ctx](pattern, bindingsFn, children/options...)` | Dynamic directory template |
| `DirNode(name, children/options...)` | Static directory |
| `FileNode(name, mode, options...)` | File with handlers |
| `Each(name, bindingsFn, options...)` | Dynamic directory — children come from bindings |
### Options
### Options (NodeOption)
| Option | Description |
|--------|-------------|
| `Read(fn)` | Read handler: `func(Ctx) ([]byte, error)` |
| `Write(fn)` | Write handler: `func(Ctx, []byte) error` |
| `Stream(fn)` | Streaming read (blocks indefinitely) |
| `BlockOnce(fn)` | Blocking read (returns once) |
| `Request(fn)` | Request/response pattern (rdwr) |
| `Doc(desc)` | Human-readable description |
| `UID(uid)` | Set owner |
| `GID(gid)` | Set group |
| `Alias(names...)` | Alternate lookup names |
| `StatOverride(fn)` | Custom stat handler |
| `CreateFile(fn)` | File creation handler (directories) |
| `RemoveNode(fn)` | Removal handler |
| Option | Signature | Description |
|--------|-----------|-------------|
| `Read(fn)` | `func() ([]byte, error)` | Non-blocking read |
| `Write(fn)` | `func([]byte) error` | Write handler |
| `Stream(fn)` | `func(context.Context, string) ([]byte, string, error)` | Blocking streaming read |
| `BlockOnce(fn)` | `func(context.Context, string) ([]byte, string, error)` | Blocking read, returns once |
| `Request(fn)` | `func([]byte) ([]byte, error)` | Write-then-read (rdwr) |
| `Doc(desc)` | `string` | Human-readable description |
| `UID(uid)` | `string` | Set owner |
| `GID(gid)` | `string` | Set group |
| `StatOverride(fn)` | `func() os.FileInfo` | Custom stat |
| `RemoveNode(fn)` | `func() error` | Removal handler |
### Binding
Returned by the `Each` bindings function. Each binding represents one dynamic child.
```go
virtfs.Binding[Ctx]{
Name: "filename", // Primary name
Aliases: []string{"alt-name"}, // Alternate lookup names
UID: "owner", // Owner (optional)
GID: "group", // Group (optional)
Applier: func(c Ctx) Ctx { // Context mutation
c.MyField = myValue
return c
},
Remove: func() { ... }, // Removal callback (optional)
Rename: func(newName string) error { ... }, // Rename callback (optional)
virtfs.Binding{
Name: "alice",
Aliases: []string{"user-id-123"},
Children: []virtfs.FsNodeDecl{ ... }, // pre-bound subtree
Remove: func() { ... }, // optional
Rename: func(newName string) error { ... }, // optional
}
```
Children are fully-wired — their handlers are closures that already captured the specific instance (no applier/context needed).
### Tree Operations
```go
tree := virtfs.BuildTree(spec, rootCtx)
tree := virtfs.BuildTree(spec)
tree.List() // List entries
tree.Stat(name) // Get file info
tree.Open(name) // Open file for read/write
tree.Create(name) // Create file
tree.Delete(name) // Delete file
tree.Rename(old, new) // Rename file
tree.Readdir(path) // List subdirectory
tree.List() // list root entries
tree.Stat(name) // stat a path
tree.Open(name) // open file for read/write
tree.Create(name) // create file
tree.Delete(name) // remove file
tree.Rename(old, new) // rename file
tree.Readdir(path) // list subdirectory
```
### Help Generation
```go
help := virtfs.GenerateHelp(spec)
// Returns formatted documentation from Doc() annotations
// Produces formatted docs from Doc() annotations
```
## License