diff --git a/virtfs/README.md b/virtfs/README.md index 8a8cba8..1b3d152 100644 --- a/virtfs/README.md +++ b/virtfs/README.md @@ -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