- Create fsedsl/ as embedded library with generic types: - FsNodeDecl[Ctx], Binding[Ctx], NodeOption[Ctx] - BuildTree[Ctx], GenerateHelp[Ctx] - Tree, File, FileConfig, SyntheticFileInfo - Update ollie/fs to use fsedsl via type aliases: - type FsNodeDecl = fsedsl.FsNodeDecl[HandlerCtx] - Wrapper functions for constructors and options - Migrate binding functions to Applier pattern: - Binding.Applier replaces direct field assignment - Cleanly separates EDSL mechanics from domain types - Add replace directive in go.mod for local module The library can be extracted to a separate repo later by changing the module path and removing the replace directive. |
||
|---|---|---|
| .. | ||
| LICENSE.md | ||
| README.md | ||
| builder.go | ||
| decl.go | ||
| decl_test.go | ||
| file.go | ||
| go.mod | ||
| help.go | ||
| tree.go | ||
README.md
fsedsl
A Go library providing an embedded domain-specific language (EDSL) for declaring synthetic filesystems. Designed for building 9P namespaces where the entire structure is declared statically but populated dynamically at runtime.
Features
- Declarative filesystem specification: Define your namespace structure with
DirNode,FileNode, andEachconstructors - Generic context propagation: Type-safe context passing through the tree via
Binding.Applier - Dynamic bindings:
Eachnodes iterate over runtime-determined collections - Auto-generated help:
GenerateHelpproduces documentation fromDoc()annotations - Standard filesystem semantics: Implements read, write, stat, list, create, delete, rename
Installation
go get git.lneely.de/lkn/fsedsl
Quick Example
package main
import (
"fmt"
"git.lneely.de/lkn/fsedsl"
)
type Ctx struct {
User *User
}
type User struct {
ID string
Name string
}
var users = []*User{
{ID: "1", Name: "alice"},
{ID: "2", Name: "bob"},
}
func main() {
spec := fsedsl.DirNode[Ctx]("/",
fsedsl.FileNode[Ctx]("version", 0444,
fsedsl.Doc[Ctx]("API version"),
fsedsl.Read(func(ctx Ctx) ([]byte, error) {
return []byte("1.0\n"), nil
}),
),
fsedsl.Each("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) {
var out []fsedsl.Binding[Ctx]
for _, u := range users {
user := u // capture
out = append(out, fsedsl.Binding[Ctx]{
Name: user.Name,
Aliases: []string{user.ID},
Applier: func(c Ctx) Ctx {
c.User = user
return c
},
})
}
return out, nil
},
fsedsl.FileNode[Ctx]("id", 0444,
fsedsl.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.ID + "\n"), nil
}),
),
fsedsl.FileNode[Ctx]("name", 0666,
fsedsl.Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.User.Name + "\n"), nil
}),
),
),
)
tree := fsedsl.BuildTree(spec, Ctx{})
// List root
entries, _ := tree.List()
for _, e := range entries {
fmt.Println(e.Name())
}
// Read a file
f, _ := tree.Open("alice/name")
data, _ := f.Read()
fmt.Printf("alice/name: %s", data)
}
API Overview
Constructors
DirNode[Ctx](name, children/options...)— static directoryFileNode[Ctx](name, mode, options...)— file with handlersEach[Ctx](pattern, bindingsFn, children/options...)— dynamic directory template
Options
Read(fn)— read handlerWrite(fn)— write handlerStream(fn)— streaming read (blocks indefinitely)BlockOnce(fn)— blocking read (returns once)Request(fn)— request/response pattern (rdwr)Doc(desc)— human-readable descriptionUID(uid),GID(gid)— ownershipAlias(names...)— alternate lookup names
Building
tree := fsedsl.BuildTree(spec, rootCtx)
Help Generation
help := fsedsl.GenerateHelp(spec)
License
GPLv3