225 lines
7.7 KiB
Go
225 lines
7.7 KiB
Go
// Package fsedsl 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.
|
|
//
|
|
// The core type is FsNodeDecl[Ctx], a recursive structure that describes
|
|
// directories, files, and template nodes (Each). Handlers receive a context
|
|
// of type Ctx, allowing domain-specific data to flow through the tree.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// type MyCtx struct {
|
|
// User *User
|
|
// Data any
|
|
// }
|
|
//
|
|
// spec := DirNode[MyCtx]("/",
|
|
// FileNode[MyCtx]("version", 0444,
|
|
// Doc("API version"),
|
|
// Read(func(ctx MyCtx) ([]byte, error) {
|
|
// return []byte("1.0\n"), nil
|
|
// }),
|
|
// ),
|
|
// Each[MyCtx]("{user}", userBindings,
|
|
// FileNode[MyCtx]("name", 0444,
|
|
// Read(func(ctx MyCtx) ([]byte, error) {
|
|
// return []byte(ctx.User.Name + "\n"), nil
|
|
// }),
|
|
// ),
|
|
// ),
|
|
// )
|
|
//
|
|
// tree := BuildTree(spec, MyCtx{})
|
|
package fsedsl
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
)
|
|
|
|
// FsNodeDecl describes one node in a filesystem namespace.
|
|
// The entire filesystem is a single recursive tree of these declarations.
|
|
// Ctx is the context type passed to all handler functions.
|
|
type FsNodeDecl[Ctx any] struct {
|
|
// Identity.
|
|
Name string
|
|
Aliases []string // alternate names that resolve to this node (e.g. immutable ID)
|
|
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 (non-nil bypasses Name/UID/GID/Mode/size defaulting).
|
|
Stat func() os.FileInfo
|
|
|
|
// Twalk — children (implies directory; at most one set).
|
|
Children []FsNodeDecl[Ctx] // static, known at compile time
|
|
Bindings func(Ctx) ([]Binding[Ctx], error) // Each bindings
|
|
BindTargets []BindTarget // declares what Bindings provides
|
|
|
|
// Tread (non-blocking) + Twrite.
|
|
Read func(ctx Ctx) ([]byte, error)
|
|
Write func(ctx Ctx, data []byte) error
|
|
|
|
// Tread — blocking variants (at most one set).
|
|
BlockOnce func(ctx Ctx, cctx context.Context, base string) ([]byte, string, error)
|
|
Stream func(ctx Ctx, cctx context.Context, base string) ([]byte, string, error)
|
|
|
|
// Request — rdwr: write triggers result, read returns it once.
|
|
Request func(ctx Ctx, data []byte) ([]byte, error)
|
|
|
|
// Tcreate + Tremove + Trename (directory ops).
|
|
Create func(ctx Ctx, name string) error
|
|
Remove func(ctx Ctx) error
|
|
Rename func(newName string) error
|
|
}
|
|
|
|
// NodeOption configures a FsNodeDecl at construction time.
|
|
type NodeOption[Ctx any] func(*FsNodeDecl[Ctx])
|
|
|
|
// Binding provides identity and data for one iteration of Each.
|
|
// The structure is cloned for each binding, and handlers receive
|
|
// the bound data via the Ctx fields specified by BindTargets.
|
|
type Binding[Ctx any] struct {
|
|
Name string // directory/file name (e.g. session name, agent name)
|
|
Aliases []string // alternate lookup names (e.g. immutable ID)
|
|
UID string // owner (empty = inherit)
|
|
GID string // group (empty = inherit)
|
|
|
|
// Applier updates the context with bound data.
|
|
// Called when traversing into this binding's node.
|
|
Applier func(ctx Ctx) Ctx
|
|
|
|
// Lifecycle callbacks.
|
|
Remove func()
|
|
Rename func(string) error
|
|
}
|
|
|
|
// BindTarget declares what data an Each block binds to handlers.
|
|
// This is advisory — the actual binding is done by Binding.Applier.
|
|
type BindTarget int
|
|
|
|
const (
|
|
BindData BindTarget = iota // generic data binding
|
|
)
|
|
|
|
// ── EDSL constructors ────────────────────────────────────────────
|
|
|
|
// DirNode declares a static directory node.
|
|
// Arguments can be FsNodeDecl[Ctx] children or NodeOption[Ctx] modifiers.
|
|
func DirNode[Ctx any](name string, args ...any) FsNodeDecl[Ctx] {
|
|
d := FsNodeDecl[Ctx]{Name: name}
|
|
for _, a := range args {
|
|
switch v := a.(type) {
|
|
case FsNodeDecl[Ctx]:
|
|
d.Children = append(d.Children, v)
|
|
case NodeOption[Ctx]:
|
|
v(&d)
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Each declares a directory that is instantiated for each binding in a collection.
|
|
// The bindings function is called at list/traverse time to determine instances.
|
|
//
|
|
// Example:
|
|
//
|
|
// Each[MyCtx]("{id}", func(ctx MyCtx) ([]Binding[MyCtx], error) {
|
|
// return []Binding[MyCtx]{
|
|
// {Name: "foo", Applier: func(c MyCtx) MyCtx { c.Data = "foo"; return c }},
|
|
// }, nil
|
|
// },
|
|
// FileNode[MyCtx]("name", 0444, Read(readName)),
|
|
// )
|
|
func Each[Ctx any](name string, bindings func(Ctx) ([]Binding[Ctx], error), args ...any) FsNodeDecl[Ctx] {
|
|
d := FsNodeDecl[Ctx]{Name: name, Bindings: bindings}
|
|
for _, a := range args {
|
|
switch v := a.(type) {
|
|
case FsNodeDecl[Ctx]:
|
|
d.Children = append(d.Children, v)
|
|
case NodeOption[Ctx]:
|
|
v(&d)
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// FileNode declares a file node (non-directory).
|
|
func FileNode[Ctx any](name string, mode os.FileMode, opts ...NodeOption[Ctx]) FsNodeDecl[Ctx] {
|
|
d := FsNodeDecl[Ctx]{Name: name, Mode: mode}
|
|
for _, o := range opts {
|
|
o(&d)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// ── Options ──────────────────────────────────────────────────────
|
|
|
|
// Read sets the read handler for a file node.
|
|
func Read[Ctx any](fn func(Ctx) ([]byte, error)) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Read = fn }
|
|
}
|
|
|
|
// Write sets the write handler for a file node.
|
|
func Write[Ctx any](fn func(Ctx, []byte) error) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Write = fn }
|
|
}
|
|
|
|
// Stream sets a streaming read handler that blocks indefinitely.
|
|
func Stream[Ctx any](fn func(Ctx, context.Context, string) ([]byte, string, error)) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Stream = fn }
|
|
}
|
|
|
|
// BlockOnce sets a blocking read handler that returns once.
|
|
func BlockOnce[Ctx any](fn func(Ctx, context.Context, string) ([]byte, string, error)) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.BlockOnce = fn }
|
|
}
|
|
|
|
// Request sets a request-response handler (rdwr pattern).
|
|
func Request[Ctx any](fn func(Ctx, []byte) ([]byte, error)) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Request = fn }
|
|
}
|
|
|
|
// CreateFile sets the file creation handler for a directory node.
|
|
func CreateFile[Ctx any](fn func(Ctx, string) error) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Create = fn }
|
|
}
|
|
|
|
// RemoveNode sets the removal handler for a node.
|
|
func RemoveNode[Ctx any](fn func(Ctx) error) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Remove = fn }
|
|
}
|
|
|
|
// StatOverride sets a custom stat handler that bypasses default metadata.
|
|
func StatOverride[Ctx any](fn func() os.FileInfo) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Stat = fn }
|
|
}
|
|
|
|
// UID sets the owner for a node.
|
|
func UID[Ctx any](uid string) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.UID = uid }
|
|
}
|
|
|
|
// GID sets the group for a node.
|
|
func GID[Ctx any](gid string) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.GID = gid }
|
|
}
|
|
|
|
// Alias adds alternate names for resolving this node.
|
|
func Alias[Ctx any](names ...string) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Aliases = append(d.Aliases, names...) }
|
|
}
|
|
|
|
// Bind declares how Each bindings populate the context.
|
|
// This is advisory metadata; the actual binding is done by Binding.Applier.
|
|
func Bind[Ctx any](targets ...BindTarget) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.BindTargets = append(d.BindTargets, targets...) }
|
|
}
|
|
|
|
// Doc sets a human-readable description for the node.
|
|
// Used by GenerateHelp to produce documentation.
|
|
func Doc[Ctx any](desc string) NodeOption[Ctx] {
|
|
return func(d *FsNodeDecl[Ctx]) { d.Desc = desc }
|
|
}
|