diff --git a/fs/edsl.go b/fs/edsl.go index 225211c..54de363 100644 --- a/fs/edsl.go +++ b/fs/edsl.go @@ -1,8 +1,7 @@ package fs -// This file provides type aliases and wrappers that instantiate the generic -// fsedsl types with ollie's HandlerCtx. This allows the fs package to use -// the generic EDSL library while maintaining backward compatibility. +// This file builds an fsedsl Adapter specialized for ollie's HandlerCtx. +// The adapter provides type-safe EDSL functions for declaring the 9P namespace. import ( "context" @@ -44,6 +43,12 @@ type HandlerCtx struct { Data any } +// ── Adapter instance ───────────────────────────────────────────── + +// edsl is the fsedsl Adapter specialized for HandlerCtx. +// All EDSL functions below delegate to this adapter. +var edsl = fsedsl.NewAdapter[HandlerCtx]() + // ── Type aliases from fsedsl ───────────────────────────────────── // FsNodeDecl describes one node in the 9P namespace. @@ -94,85 +99,85 @@ const ( BindData = fsedsl.BindData ) -// ── EDSL constructors (wrappers) ───────────────────────────────── +// ── EDSL constructors (via adapter) ────────────────────────────── // DirNode declares a static directory node. func DirNode(name string, args ...any) FsNodeDecl { - return fsedsl.DirNode[HandlerCtx](name, args...) + return edsl.DirNode(name, args...) } // Each declares a directory that is instantiated for each binding in a collection. func Each(name string, bindings func(HandlerCtx) ([]Binding, error), args ...any) FsNodeDecl { - return fsedsl.Each(name, bindings, args...) + return edsl.Each(name, bindings, args...) } // FileNode declares a file node (non-directory). func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl { - return fsedsl.FileNode[HandlerCtx](name, mode, opts...) + return edsl.FileNode(name, mode, opts...) } -// ── Options (wrappers) ─────────────────────────────────────────── +// ── Options (via adapter) ──────────────────────────────────────── func Read(fn func(HandlerCtx) ([]byte, error)) NodeOption { - return fsedsl.Read(fn) + return edsl.Read(fn) } func Write(fn func(HandlerCtx, []byte) error) NodeOption { - return fsedsl.Write(fn) + return edsl.Write(fn) } func Stream(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption { - return fsedsl.Stream(fn) + return edsl.Stream(fn) } func BlockOnce(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption { - return fsedsl.BlockOnce(fn) + return edsl.BlockOnce(fn) } func Request(fn func(HandlerCtx, []byte) ([]byte, error)) NodeOption { - return fsedsl.Request(fn) + return edsl.Request(fn) } func CreateFile(fn func(HandlerCtx, string) error) NodeOption { - return fsedsl.CreateFile(fn) + return edsl.CreateFile(fn) } func Remove(fn func(HandlerCtx) error) NodeOption { - return fsedsl.RemoveNode(fn) + return edsl.Remove(fn) } func Stat(fn func() os.FileInfo) NodeOption { - return fsedsl.StatOverride[HandlerCtx](fn) + return edsl.Stat(fn) } func UID(uid string) NodeOption { - return fsedsl.UID[HandlerCtx](uid) + return edsl.UID(uid) } func GID(gid string) NodeOption { - return fsedsl.GID[HandlerCtx](gid) + return edsl.GID(gid) } func Alias(names ...string) NodeOption { - return fsedsl.Alias[HandlerCtx](names...) + return edsl.Alias(names...) } // Bind declares how Each bindings populate HandlerCtx. // This is advisory metadata — the actual binding is done by Binding.Applier. func Bind(targets ...BindTarget) NodeOption { - return fsedsl.Bind[HandlerCtx](targets...) + return edsl.Bind(targets...) } // Doc sets a human-readable description for the node. func Doc(desc string) NodeOption { - return fsedsl.Doc[HandlerCtx](desc) + return edsl.Doc(desc) } -// ── Tree construction (wrappers) ───────────────────────────────── +// ── Tree construction (via adapter) ────────────────────────────── // BuildTree walks an FsNodeDecl spec and produces a *Tree. func BuildTree(spec FsNodeDecl, rootCtx HandlerCtx) *Tree { - return fsedsl.BuildTree(spec, rootCtx) + return edsl.BuildTree(spec, rootCtx) } // NewTree creates a Tree backed by one or more directories. @@ -194,7 +199,7 @@ var ( WithReaddir = fsedsl.WithReaddir ) -// ── Helpers (wrappers) ─────────────────────────────────────────── +// ── Helpers ────────────────────────────────────────────────────── // FileEntry returns a synthetic file DirEntry. func FileEntry(name string, mode os.FileMode) os.DirEntry { @@ -208,5 +213,5 @@ func DirEntry(name string, mode os.FileMode) os.DirEntry { // GenerateHelp produces help text from a filesystem spec tree. func GenerateHelp(root FsNodeDecl) string { - return fsedsl.GenerateHelp(root) + return edsl.GenerateHelp(root) } diff --git a/fsedsl/README.md b/fsedsl/README.md index 60de9aa..d7b4eec 100644 --- a/fsedsl/README.md +++ b/fsedsl/README.md @@ -6,6 +6,7 @@ A Go library providing an embedded domain-specific language (EDSL) for declaring - **Declarative filesystem specification**: Define your namespace structure with `DirNode`, `FileNode`, and `Each` constructors - **Generic context propagation**: Type-safe context passing through the tree via `Binding.Applier` +- **Adapter pattern**: Create a type-specialized adapter for ergonomic use - **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 @@ -16,7 +17,11 @@ A Go library providing an embedded domain-specific language (EDSL) for declaring go get git.lneely.de/lkn/fsedsl ``` -## Quick Example +## Usage + +### Using the Adapter (recommended) + +The `Adapter` type provides all EDSL functions specialized for your context type: ```go package main @@ -41,14 +46,18 @@ var users = []*User{ } func main() { - spec := fsedsl.DirNode[Ctx]("/", - fsedsl.FileNode[Ctx]("version", 0444, - fsedsl.Doc[Ctx]("API version"), - fsedsl.Read(func(ctx Ctx) ([]byte, error) { + // Create an adapter specialized for our context type + edsl := fsedsl.NewAdapter[Ctx]() + + // Build the spec using adapter methods + spec := edsl.DirNode("/", + edsl.FileNode("version", 0444, + edsl.Doc("API version"), + edsl.Read(func(ctx Ctx) ([]byte, error) { return []byte("1.0\n"), nil }), ), - fsedsl.Each("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) { + edsl.Each("{user}", func(ctx Ctx) ([]fsedsl.Binding[Ctx], error) { var out []fsedsl.Binding[Ctx] for _, u := range users { user := u // capture @@ -63,41 +72,62 @@ func main() { } return out, nil }, - fsedsl.FileNode[Ctx]("id", 0444, - fsedsl.Read(func(ctx Ctx) ([]byte, error) { + edsl.FileNode("id", 0444, + edsl.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) { + edsl.FileNode("name", 0666, + edsl.Read(func(ctx Ctx) ([]byte, error) { return []byte(ctx.User.Name + "\n"), nil }), ), ), ) - tree := fsedsl.BuildTree(spec, Ctx{}) + // Build the tree + tree := edsl.BuildTree(spec, Ctx{}) - // List root + // Use the tree 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) } ``` +### Using standalone functions + +You can also use the generic functions directly: + +```go +spec := fsedsl.DirNode[Ctx]("/", + fsedsl.FileNode[Ctx]("version", 0444, + fsedsl.Read(func(ctx Ctx) ([]byte, error) { + return []byte("1.0\n"), nil + }), + ), +) +tree := fsedsl.BuildTree(spec, Ctx{}) +``` + ## API Overview +### Adapter + +```go +edsl := fsedsl.NewAdapter[MyCtx]() +``` + ### Constructors -- `DirNode[Ctx](name, children/options...)` — static directory -- `FileNode[Ctx](name, mode, options...)` — file with handlers -- `Each[Ctx](pattern, bindingsFn, children/options...)` — dynamic directory template +- `DirNode(name, children/options...)` — static directory +- `FileNode(name, mode, options...)` — file with handlers +- `Each(pattern, bindingsFn, children/options...)` — dynamic directory template ### Options @@ -113,13 +143,13 @@ func main() { ### Building ```go -tree := fsedsl.BuildTree(spec, rootCtx) +tree := edsl.BuildTree(spec, rootCtx) ``` ### Help Generation ```go -help := fsedsl.GenerateHelp(spec) +help := edsl.GenerateHelp(spec) ``` ## License diff --git a/fsedsl/adapter.go b/fsedsl/adapter.go new file mode 100644 index 0000000..be92597 --- /dev/null +++ b/fsedsl/adapter.go @@ -0,0 +1,119 @@ +package fsedsl + +import ( + "context" + "os" +) + +// Adapter provides the EDSL functions specialized for a specific context type. +// Create one with NewAdapter[YourCtx]() and use its methods to build filesystem specs. +type Adapter[Ctx any] struct{} + +// NewAdapter creates an Adapter for the given context type. +// +// Example: +// +// type MyCtx struct { User *User } +// edsl := fsedsl.NewAdapter[MyCtx]() +// spec := edsl.DirNode("/", +// edsl.FileNode("version", 0444, edsl.Doc("API version")), +// ) +func NewAdapter[Ctx any]() *Adapter[Ctx] { + return &Adapter[Ctx]{} +} + +// ── Constructors ───────────────────────────────────────────────── + +// DirNode declares a static directory node. +func (a *Adapter[Ctx]) DirNode(name string, args ...any) FsNodeDecl[Ctx] { + return DirNode[Ctx](name, args...) +} + +// FileNode declares a file node (non-directory). +func (a *Adapter[Ctx]) FileNode(name string, mode os.FileMode, opts ...NodeOption[Ctx]) FsNodeDecl[Ctx] { + return FileNode[Ctx](name, mode, opts...) +} + +// Each declares a directory that is instantiated for each binding in a collection. +func (a *Adapter[Ctx]) Each(name string, bindings func(Ctx) ([]Binding[Ctx], error), args ...any) FsNodeDecl[Ctx] { + return Each[Ctx](name, bindings, args...) +} + +// ── Options ────────────────────────────────────────────────────── + +// Read sets the read handler for a file node. +func (a *Adapter[Ctx]) Read(fn func(Ctx) ([]byte, error)) NodeOption[Ctx] { + return Read[Ctx](fn) +} + +// Write sets the write handler for a file node. +func (a *Adapter[Ctx]) Write(fn func(Ctx, []byte) error) NodeOption[Ctx] { + return Write[Ctx](fn) +} + +// Stream sets a streaming read handler that blocks indefinitely. +func (a *Adapter[Ctx]) Stream(fn func(Ctx, context.Context, string) ([]byte, string, error)) NodeOption[Ctx] { + return Stream[Ctx](fn) +} + +// BlockOnce sets a blocking read handler that returns once. +func (a *Adapter[Ctx]) BlockOnce(fn func(Ctx, context.Context, string) ([]byte, string, error)) NodeOption[Ctx] { + return BlockOnce[Ctx](fn) +} + +// Request sets a request-response handler (rdwr pattern). +func (a *Adapter[Ctx]) Request(fn func(Ctx, []byte) ([]byte, error)) NodeOption[Ctx] { + return Request[Ctx](fn) +} + +// CreateFile sets the file creation handler for a directory node. +func (a *Adapter[Ctx]) CreateFile(fn func(Ctx, string) error) NodeOption[Ctx] { + return CreateFile[Ctx](fn) +} + +// Remove sets the removal handler for a node. +func (a *Adapter[Ctx]) Remove(fn func(Ctx) error) NodeOption[Ctx] { + return RemoveNode[Ctx](fn) +} + +// Stat sets a custom stat handler that bypasses default metadata. +func (a *Adapter[Ctx]) Stat(fn func() os.FileInfo) NodeOption[Ctx] { + return StatOverride[Ctx](fn) +} + +// UID sets the owner for a node. +func (a *Adapter[Ctx]) UID(uid string) NodeOption[Ctx] { + return UID[Ctx](uid) +} + +// GID sets the group for a node. +func (a *Adapter[Ctx]) GID(gid string) NodeOption[Ctx] { + return GID[Ctx](gid) +} + +// Alias adds alternate names for resolving this node. +func (a *Adapter[Ctx]) Alias(names ...string) NodeOption[Ctx] { + return Alias[Ctx](names...) +} + +// Bind declares how Each bindings populate the context (advisory). +func (a *Adapter[Ctx]) Bind(targets ...BindTarget) NodeOption[Ctx] { + return Bind[Ctx](targets...) +} + +// Doc sets a human-readable description for the node. +func (a *Adapter[Ctx]) Doc(desc string) NodeOption[Ctx] { + return Doc[Ctx](desc) +} + +// ── Tree construction ──────────────────────────────────────────── + +// BuildTree walks an FsNodeDecl spec and produces a *Tree. +func (a *Adapter[Ctx]) BuildTree(spec FsNodeDecl[Ctx], rootCtx Ctx) *Tree { + return BuildTree[Ctx](spec, rootCtx) +} + +// GenerateHelp produces documentation from a filesystem spec tree. +func (a *Adapter[Ctx]) GenerateHelp(root FsNodeDecl[Ctx]) string { + return GenerateHelp[Ctx](root) +}