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:
parent
ee7426d628
commit
3a875da9f6
227
virtfs/README.md
227
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue