Add explicit Adapter pattern to fsedsl

- Add Adapter[Ctx] type that provides all EDSL methods specialized for a context type
- NewAdapter[Ctx]() creates an adapter instance
- Update ollie/fs/edsl.go to use the adapter explicitly
- Update README with adapter-based examples

The adapter makes it explicit that we're building a type-specialized
interface to the generic EDSL library.
This commit is contained in:
Levi Neely 2026-08-04 12:06:44 +02:00
parent b1802ca28c
commit 6c232c7bb7
3 changed files with 197 additions and 43 deletions

View File

@ -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)
}

View File

@ -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

119
fsedsl/adapter.go Normal file
View File

@ -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)
}