Same pattern as BlockOnce: framework handles the blocking loop. readFn takes a base string, returns (data, nextBase, error). signal fires when new data may be available. Chat stream handlers now use Stream(a.ChatRead, a.ChatSignal). StreamRaw retained for custom handlers. streamChat() in support.go is now dead code (replaced by ChatRead). |
||
|---|---|---|
| .. | ||
| LICENSE.md | ||
| README.md | ||
| builder.go | ||
| decl.go | ||
| decl_test.go | ||
| file.go | ||
| go.mod | ||
| help.go | ||
| tree.go | ||
| tree_test.go | ||
README.md
virtfs
A Go library for declaring synthetic filesystems. Define your namespace structure statically, populate it dynamically at runtime with closures.
Produces a *Tree usable with any filesystem protocol (9P, FUSE, etc.) or as an in-memory filesystem for testing.
Design
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.
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
package main
import (
"fmt"
"ollie/virtfs"
)
type User struct {
ID string
Name string
}
var users = []*User{
{ID: "1", Name: "alice"},
{ID: "2", Name: "bob"},
}
func main() {
spec := virtfs.DirNode("/",
virtfs.FileNode("version", 0444,
virtfs.Doc("API version"),
virtfs.Read(func() ([]byte, error) {
return []byte("1.0\n"), nil
}),
),
virtfs.Each("{user}", func() ([]virtfs.Binding, error) {
var out []virtfs.Binding
for _, u := range users {
user := u // capture
out = append(out, virtfs.Binding{
Name: user.Name,
Aliases: []string{user.ID},
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
}),
)
tree := virtfs.BuildTree(spec)
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)
}
API
Constructors
| Function | Description |
|---|---|
DirNode(name, children/options...) |
Static directory |
FileNode(name, mode, options...) |
File with handlers |
Each(name, bindingsFn, options...) |
Dynamic directory — children come from bindings |
Options (NodeOption)
| 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.
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
tree := virtfs.BuildTree(spec)
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
help := virtfs.GenerateHelp(spec)
// Produces formatted docs from Doc() annotations
License
GPLv3