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
|
# 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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue