ollie/virtfs/decl.go

215 lines
6.7 KiB
Go

// Package virtfs provides an embedded domain-specific language for declaring
// synthetic filesystems. It is designed for building 9P namespaces where the
// entire filesystem structure is declared statically but populated dynamically.
//
// Handlers are plain closures that capture their state at binding time.
// There is no context type parameter — each handler already knows its target.
//
// Example usage:
//
// users := getUserList()
// spec := DirNode("/",
// FileNode("version", 0444, Read(func() ([]byte, error) {
// return []byte("1.0\n"), nil
// })),
// Each("{user}", func() ([]FsNodeDecl, error) {
// var out []FsNodeDecl
// for _, u := range users {
// user := u
// out = append(out, FsNodeDecl{
// Name: user.Name,
// Children: []FsNodeDecl{
// FileNode("name", 0444, Read(func() ([]byte, error) {
// return []byte(user.Name + "\n"), nil
// })),
// },
// })
// }
// return out, nil
// }),
// )
//
// tree := BuildTree(spec)
package virtfs
import (
"context"
"os"
)
// FsNodeDecl describes one node in a filesystem namespace.
// Handlers are closures — they capture their target at construction time.
type FsNodeDecl struct {
// Identity.
Name string
Aliases []string // alternate names that resolve to this node
UID string // "" = inherit from parent
GID string // "" = inherit from parent
Mode os.FileMode // 0 = inherit parent default
Desc string // human-readable description
// Tstat — metadata override.
Stat func() os.FileInfo
// Twalk — children (implies directory; at most one set).
Children []FsNodeDecl // static, known at compile time
Bindings func() ([]FsNodeDecl, error) // Each: dynamic children
// Tread (non-blocking) + Twrite.
Read func() ([]byte, error)
Write func(data []byte) error
// Tread — blocking variants (at most one set).
BlockOnce func(ctx context.Context, base string) ([]byte, string, error)
Stream func(ctx context.Context, base string) ([]byte, string, error)
// Rdwr — atomic write-then-read: write triggers computation, read returns result once.
Rdwr func(ctx context.Context, data []byte) ([]byte, error)
// Tremove + Trename.
Remove func() error
Rename func(newName string) error
}
// NodeOption configures a FsNodeDecl at construction time.
type NodeOption func(*FsNodeDecl)
// ── EDSL constructors ────────────────────────────────────────────
// DirNode declares a static directory node.
func DirNode(name string, args ...any) FsNodeDecl {
d := FsNodeDecl{Name: name}
for _, a := range args {
switch v := a.(type) {
case FsNodeDecl:
d.Children = append(d.Children, v)
case []FsNodeDecl:
d.Children = append(d.Children, v...)
case NodeOption:
v(&d)
}
}
return d
}
// Each declares a directory whose children are produced dynamically.
func Each(name string, list func() ([]FsNodeDecl, error), opts ...NodeOption) FsNodeDecl {
d := FsNodeDecl{Name: name, Bindings: list}
for _, o := range opts {
o(&d)
}
return d
}
// FileNode declares a file node (non-directory).
func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
d := FsNodeDecl{Name: name, Mode: mode}
for _, o := range opts {
o(&d)
}
return d
}
// ── Options ──────────────────────────────────────────────────────
// Read sets the read handler for a file node.
func Read(fn func() ([]byte, error)) NodeOption {
return func(d *FsNodeDecl) { d.Read = fn }
}
// Write sets the write handler for a file node.
func Write(fn func([]byte) error) NodeOption {
return func(d *FsNodeDecl) { d.Write = fn }
}
// Stream sets a streaming read handler that blocks indefinitely.
// readFn returns (data, nextBase, error) given the current base.
// signal returns a channel that is closed when new data may be available.
// The framework blocks on signal until readFn returns non-empty data.
func Stream(readFn func(base string) ([]byte, string, error), signal func() <-chan struct{}) NodeOption {
return func(d *FsNodeDecl) {
d.Stream = func(ctx context.Context, base string) ([]byte, string, error) {
for {
data, nextBase, err := readFn(base)
if err != nil {
return nil, base, err
}
if len(data) > 0 {
return data, nextBase, nil
}
base = nextBase
select {
case <-signal():
case <-ctx.Done():
return nil, base, nil
}
}
}
}
}
// StreamRaw sets a raw streaming read handler with custom blocking logic.
func StreamRaw(fn func(context.Context, string) ([]byte, string, error)) NodeOption {
return func(d *FsNodeDecl) { d.Stream = fn }
}
// BlockOnce sets a blocking read handler that returns once per open.
// readFn returns (data, hash, error) — the current value.
// signal returns a channel that is closed when the value may have changed.
// The framework blocks until readFn returns a hash different from base.
func BlockOnce(readFn func() ([]byte, string, error), signal func() <-chan struct{}) NodeOption {
return func(d *FsNodeDecl) {
d.BlockOnce = func(ctx context.Context, base string) ([]byte, string, error) {
for {
data, hash, err := readFn()
if err != nil {
return nil, base, err
}
if base == "" {
base = hash
}
if hash != base {
return data, hash, nil
}
select {
case <-signal():
case <-ctx.Done():
return nil, "", nil // timeout: empty response, client re-opens
}
}
}
}
}
// BlockOnceRaw sets a blocking read handler with custom blocking logic.
// Use for queue-style consumers where there is no "current value" to compare.
func BlockOnceRaw(fn func(context.Context, string) ([]byte, string, error)) NodeOption {
return func(d *FsNodeDecl) { d.BlockOnce = fn }
}
// Rdwr sets an atomic write-then-read handler.
// Write triggers computation; the subsequent read returns the result once.
func Rdwr(fn func(context.Context, []byte) ([]byte, error)) NodeOption {
return func(d *FsNodeDecl) { d.Rdwr = fn }
}
// StatOverride sets a custom stat handler.
func StatOverride(fn func() os.FileInfo) NodeOption {
return func(d *FsNodeDecl) { d.Stat = fn }
}
// UID sets the owner for a node.
func UID(uid string) NodeOption {
return func(d *FsNodeDecl) { d.UID = uid }
}
// GID sets the group for a node.
func GID(gid string) NodeOption {
return func(d *FsNodeDecl) { d.GID = gid }
}
// Doc sets a human-readable description for the node.
func Doc(desc string) NodeOption {
return func(d *FsNodeDecl) { d.Desc = desc }
}