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:
parent
b1802ca28c
commit
6c232c7bb7
55
fs/edsl.go
55
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue