// 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) // Request — rdwr: write triggers result, read returns it once. Request 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. func Stream(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. func BlockOnce(fn func(context.Context, string) ([]byte, string, error)) NodeOption { return func(d *FsNodeDecl) { d.BlockOnce = fn } } // Request sets a context-aware request-response handler (rdwr pattern). func Request(fn func(context.Context, []byte) ([]byte, error)) NodeOption { return func(d *FsNodeDecl) { d.Request = fn } } // RemoveNode sets the removal handler for a node. func RemoveNode(fn func() error) NodeOption { return func(d *FsNodeDecl) { d.Remove = fn } } // RenameNode sets the rename handler for a node. func RenameNode(fn func(string) error) NodeOption { return func(d *FsNodeDecl) { d.Rename = fn } } // Alias adds alternate lookup names for this node. func Alias(names ...string) NodeOption { return func(d *FsNodeDecl) { d.Aliases = append(d.Aliases, names...) } } // 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 } }