fs: simplify adapter to type aliases only
- Remove wrapper functions from adapter.go (212 → 91 lines) - Delete redundant adapter_test.go (fsedsl has 94.8% coverage) - Add local function aliases in spec.go for clean EDSL usage - Update handlers.go, newroot.go, tree_test.go to use fsedsl directly - Update fsedsl README with cleaner integration pattern
This commit is contained in:
parent
19e255c869
commit
aaacae40a8
127
fs/adapter.go
127
fs/adapter.go
|
|
@ -1,13 +1,10 @@
|
|||
package fs
|
||||
|
||||
// This file provides type aliases and wrapper functions that specialize
|
||||
// fsedsl for ollie's HandlerCtx. This provides a clean API for the rest
|
||||
// of the fs package without requiring explicit type parameters everywhere.
|
||||
// This file provides type aliases that specialize fsedsl for ollie's HandlerCtx.
|
||||
// The fs package uses fsedsl directly (e.g., fsedsl.DirNode[HandlerCtx]).
|
||||
// These aliases allow other packages to use fs.FsNodeDecl, fs.Tree, etc.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"ollie/agent"
|
||||
"ollie/elevate"
|
||||
"ollie/fsedsl"
|
||||
|
|
@ -87,126 +84,8 @@ const (
|
|||
)
|
||||
|
||||
// BindTarget declares what data an Each block binds to handlers.
|
||||
// This is advisory — the actual binding is done by Binding.Applier.
|
||||
type BindTarget = fsedsl.BindTarget
|
||||
|
||||
const (
|
||||
BindData = fsedsl.BindData
|
||||
)
|
||||
|
||||
// ── EDSL constructors ────────────────────────────────────────────
|
||||
|
||||
// DirNode declares a static directory node.
|
||||
func DirNode(name string, args ...any) FsNodeDecl {
|
||||
return fsedsl.DirNode[HandlerCtx](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...)
|
||||
}
|
||||
|
||||
// FileNode declares a file node (non-directory).
|
||||
func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
|
||||
return fsedsl.FileNode(name, mode, opts...)
|
||||
}
|
||||
|
||||
// ── Options ──────────────────────────────────────────────────────
|
||||
|
||||
func Read(fn func(HandlerCtx) ([]byte, error)) NodeOption {
|
||||
return fsedsl.Read(fn)
|
||||
}
|
||||
|
||||
func Write(fn func(HandlerCtx, []byte) error) NodeOption {
|
||||
return fsedsl.Write(fn)
|
||||
}
|
||||
|
||||
func Stream(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption {
|
||||
return fsedsl.Stream(fn)
|
||||
}
|
||||
|
||||
func BlockOnce(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption {
|
||||
return fsedsl.BlockOnce(fn)
|
||||
}
|
||||
|
||||
func Request(fn func(HandlerCtx, []byte) ([]byte, error)) NodeOption {
|
||||
return fsedsl.Request(fn)
|
||||
}
|
||||
|
||||
func CreateFile(fn func(HandlerCtx, string) error) NodeOption {
|
||||
return fsedsl.CreateFile(fn)
|
||||
}
|
||||
|
||||
func Remove(fn func(HandlerCtx) error) NodeOption {
|
||||
return fsedsl.RemoveNode(fn)
|
||||
}
|
||||
|
||||
func Stat(fn func() os.FileInfo) NodeOption {
|
||||
return fsedsl.StatOverride[HandlerCtx](fn)
|
||||
}
|
||||
|
||||
func UID(uid string) NodeOption {
|
||||
return fsedsl.UID[HandlerCtx](uid)
|
||||
}
|
||||
|
||||
func GID(gid string) NodeOption {
|
||||
return fsedsl.GID[HandlerCtx](gid)
|
||||
}
|
||||
|
||||
func Alias(names ...string) NodeOption {
|
||||
return fsedsl.Alias[HandlerCtx](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...)
|
||||
}
|
||||
|
||||
// Doc sets a human-readable description for the node.
|
||||
func Doc(desc string) NodeOption {
|
||||
return fsedsl.Doc[HandlerCtx](desc)
|
||||
}
|
||||
|
||||
// ── Tree construction ────────────────────────────────────────────
|
||||
|
||||
// BuildTree walks an FsNodeDecl spec and produces a *Tree.
|
||||
func BuildTree(spec FsNodeDecl, rootCtx HandlerCtx) *Tree {
|
||||
return fsedsl.BuildTree(spec, rootCtx)
|
||||
}
|
||||
|
||||
// NewTree creates a Tree backed by one or more directories.
|
||||
func NewTree(dirs []string, perm os.FileMode, opts ...TreeOption) *Tree {
|
||||
return fsedsl.NewTree(dirs, perm, opts...)
|
||||
}
|
||||
|
||||
// Tree options
|
||||
var (
|
||||
WithReadOnly = fsedsl.WithReadOnly
|
||||
WithIndex = fsedsl.WithIndex
|
||||
WithResolver = fsedsl.WithResolver
|
||||
WithLister = fsedsl.WithLister
|
||||
WithCreator = fsedsl.WithCreator
|
||||
WithDeleter = fsedsl.WithDeleter
|
||||
WithRenamer = fsedsl.WithRenamer
|
||||
WithOpener = fsedsl.WithOpener
|
||||
WithStat = fsedsl.WithStat
|
||||
WithReaddir = fsedsl.WithReaddir
|
||||
)
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// FileEntry returns a synthetic file DirEntry.
|
||||
func FileEntry(name string, mode os.FileMode) os.DirEntry {
|
||||
return fsedsl.FileEntry(name, mode)
|
||||
}
|
||||
|
||||
// DirEntry returns a synthetic directory DirEntry.
|
||||
func DirEntry(name string, mode os.FileMode) os.DirEntry {
|
||||
return fsedsl.DirEntry(name, mode)
|
||||
}
|
||||
|
||||
// GenerateHelp produces help text from a filesystem spec tree.
|
||||
func GenerateHelp(root FsNodeDecl) string {
|
||||
return fsedsl.GenerateHelp(root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,501 +0,0 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEachBasic tests the basic Blueprint pattern:
|
||||
// - Declares structure statically
|
||||
// - Bindings function provides identity (name, aliases, UID/GID)
|
||||
// - Handlers receive context with bound data
|
||||
func TestEachBasic(t *testing.T) {
|
||||
// Test data that instances will bind to ctx
|
||||
type testData struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
instances := []testData{
|
||||
{ID: "aaa-111", Name: "first"},
|
||||
{ID: "bbb-222", Name: "second"},
|
||||
}
|
||||
|
||||
// Create a Blueprint with static structure and dynamic instances
|
||||
spec := DirNode("/",
|
||||
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
var out []Binding
|
||||
for _, d := range instances {
|
||||
data := d // capture
|
||||
out = append(out, Binding{
|
||||
Name: data.Name,
|
||||
Aliases: []string{data.ID},
|
||||
UID: data.ID,
|
||||
GID: "testgroup",
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
FileNode("id", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("test-id\n"), nil
|
||||
})),
|
||||
FileNode("name", 0666,
|
||||
Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("test-name\n"), nil
|
||||
}),
|
||||
Write(func(ctx HandlerCtx, data []byte) error {
|
||||
return nil // no-op for test
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
// Test: List root should show the two instances by name
|
||||
entries, err := tree.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List root: %v", err)
|
||||
}
|
||||
if len(entries) != 2 {
|
||||
t.Errorf("List root: got %d entries, want 2", len(entries))
|
||||
}
|
||||
|
||||
names := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
names[e.Name()] = true
|
||||
}
|
||||
if !names["first"] {
|
||||
t.Error("List root: missing 'first'")
|
||||
}
|
||||
if !names["second"] {
|
||||
t.Error("List root: missing 'second'")
|
||||
}
|
||||
|
||||
// Test: Access by name
|
||||
sub, err := tree.Readdir("first")
|
||||
if err != nil {
|
||||
t.Fatalf("Readdir(first): %v", err)
|
||||
}
|
||||
foundID := false
|
||||
foundName := false
|
||||
for _, e := range sub {
|
||||
switch e.Name() {
|
||||
case "id":
|
||||
foundID = true
|
||||
case "name":
|
||||
foundName = true
|
||||
}
|
||||
}
|
||||
if !foundID {
|
||||
t.Error("Readdir(first): missing 'id' leaf")
|
||||
}
|
||||
if !foundName {
|
||||
t.Error("Readdir(first): missing 'name' leaf")
|
||||
}
|
||||
|
||||
// Test: Access by alias
|
||||
subByAlias, err := tree.Readdir("aaa-111")
|
||||
if err != nil {
|
||||
t.Fatalf("Readdir(aaa-111): %v", err)
|
||||
}
|
||||
if len(subByAlias) != len(sub) {
|
||||
t.Errorf("Readdir by alias: got %d entries, want %d", len(subByAlias), len(sub))
|
||||
}
|
||||
|
||||
// Test: Read leaf file
|
||||
f, err := tree.Open("first/id")
|
||||
if err != nil {
|
||||
t.Fatalf("Open(first/id): %v", err)
|
||||
}
|
||||
data, err := f.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Read(first/id): %v", err)
|
||||
}
|
||||
if string(data) != "test-id\n" {
|
||||
t.Errorf("Read(first/id) = %q, want %q", data, "test-id\n")
|
||||
}
|
||||
|
||||
// Test: Stat instance directory
|
||||
fi, err := tree.Stat("first")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(first): %v", err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
t.Error("Stat(first): expected directory")
|
||||
}
|
||||
if fi.Name() != "first" {
|
||||
t.Errorf("Stat(first).Name() = %q, want 'first'", fi.Name())
|
||||
}
|
||||
|
||||
// Test: Stat via alias
|
||||
fiAlias, err := tree.Stat("aaa-111")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(aaa-111): %v", err)
|
||||
}
|
||||
if fiAlias.Name() != "aaa-111" {
|
||||
t.Errorf("Stat(aaa-111).Name() = %q, want 'aaa-111'", fiAlias.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachNestedStructure tests nested Blueprint nodes
|
||||
func TestEachNestedStructure(t *testing.T) {
|
||||
// Outer instances (like sessions)
|
||||
outerBindings := []struct {
|
||||
ID string
|
||||
Name string
|
||||
}{
|
||||
{ID: "sess-1", Name: "session1"},
|
||||
}
|
||||
|
||||
// Inner instances (like agents within a session)
|
||||
innerBindings := []struct {
|
||||
ID string
|
||||
Name string
|
||||
}{
|
||||
{ID: "agent-a", Name: "agentA"},
|
||||
{ID: "agent-b", Name: "agentB"},
|
||||
}
|
||||
|
||||
spec := DirNode("/",
|
||||
Each("{outer}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
var out []Binding
|
||||
for _, d := range outerBindings {
|
||||
data := d
|
||||
out = append(out, Binding{
|
||||
Name: data.Name,
|
||||
Aliases: []string{data.ID},
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
FileNode("outer-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("outer\n"), nil
|
||||
})),
|
||||
DirNode("inner-dir",
|
||||
Each("{inner}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
var out []Binding
|
||||
for _, d := range innerBindings {
|
||||
data := d
|
||||
out = append(out, Binding{
|
||||
Name: data.Name,
|
||||
Aliases: []string{data.ID},
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
FileNode("inner-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("inner\n"), nil
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
// Test: Navigate to nested blueprint
|
||||
entries, err := tree.Readdir("session1/inner-dir")
|
||||
if err != nil {
|
||||
t.Fatalf("Readdir(session1/inner-dir): %v", err)
|
||||
}
|
||||
if len(entries) != 2 {
|
||||
t.Errorf("Readdir(session1/inner-dir): got %d entries, want 2", len(entries))
|
||||
}
|
||||
|
||||
// Test: Read deeply nested file
|
||||
f, err := tree.Open("session1/inner-dir/agentA/inner-file")
|
||||
if err != nil {
|
||||
t.Fatalf("Open nested: %v", err)
|
||||
}
|
||||
data, err := f.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Read nested: %v", err)
|
||||
}
|
||||
if string(data) != "inner\n" {
|
||||
t.Errorf("Read nested = %q, want %q", data, "inner\n")
|
||||
}
|
||||
|
||||
// Test: Access nested by alias
|
||||
f2, err := tree.Open("sess-1/inner-dir/agent-b/inner-file")
|
||||
if err != nil {
|
||||
t.Fatalf("Open nested by alias: %v", err)
|
||||
}
|
||||
data2, err := f2.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Read nested by alias: %v", err)
|
||||
}
|
||||
if string(data2) != "inner\n" {
|
||||
t.Errorf("Read nested by alias = %q, want %q", data2, "inner\n")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachContextPropagation tests that context is properly propagated
|
||||
func TestEachContextPropagation(t *testing.T) {
|
||||
// Use a channel to capture the context value seen by the handler
|
||||
seen := make(chan string, 1)
|
||||
|
||||
spec := DirNode("/",
|
||||
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
return []Binding{
|
||||
{
|
||||
Name: "test-instance",
|
||||
UID: "test-uid",
|
||||
GID: "test-gid",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
FileNode("check", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
seen <- "handler-called"
|
||||
return []byte("ok\n"), nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
f, err := tree.Open("test-instance/check")
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
_, err = f.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Read: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case v := <-seen:
|
||||
if v != "handler-called" {
|
||||
t.Errorf("unexpected seen value: %s", v)
|
||||
}
|
||||
default:
|
||||
t.Error("handler was not called")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachUIDGIDInheritance tests UID/GID resolution
|
||||
func TestEachUIDGIDInheritance(t *testing.T) {
|
||||
spec := DirNode("/", UID("root-uid"), GID("root-gid"),
|
||||
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
return []Binding{
|
||||
{Name: "with-uid", UID: "custom-uid", GID: "custom-gid"},
|
||||
{Name: "without-uid"}, // should inherit
|
||||
}, nil
|
||||
},
|
||||
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("data\n"), nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
// Test: instance with custom UID/GID
|
||||
fi1, err := tree.Stat("with-uid")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(with-uid): %v", err)
|
||||
}
|
||||
sfi1, ok := fi1.(*SyntheticFileInfo)
|
||||
if !ok {
|
||||
t.Fatalf("expected SyntheticFileInfo, got %T", fi1)
|
||||
}
|
||||
if sfi1.UID_ != "custom-uid" {
|
||||
t.Errorf("with-uid UID = %q, want 'custom-uid'", sfi1.UID_)
|
||||
}
|
||||
if sfi1.GID_ != "custom-gid" {
|
||||
t.Errorf("with-uid GID = %q, want 'custom-gid'", sfi1.GID_)
|
||||
}
|
||||
|
||||
// Test: instance without UID/GID should inherit from parent
|
||||
fi2, err := tree.Stat("without-uid")
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(without-uid): %v", err)
|
||||
}
|
||||
sfi2, ok := fi2.(*SyntheticFileInfo)
|
||||
if !ok {
|
||||
t.Fatalf("expected SyntheticFileInfo, got %T", fi2)
|
||||
}
|
||||
// Should inherit from root
|
||||
if sfi2.UID_ != "root-uid" {
|
||||
t.Errorf("without-uid UID = %q, want 'root-uid'", sfi2.UID_)
|
||||
}
|
||||
if sfi2.GID_ != "root-gid" {
|
||||
t.Errorf("without-uid GID = %q, want 'root-gid'", sfi2.GID_)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachRename tests the rename callback on instances
|
||||
func TestEachRename(t *testing.T) {
|
||||
renamed := ""
|
||||
|
||||
spec := DirNode("/",
|
||||
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
return []Binding{
|
||||
{
|
||||
Name: "original",
|
||||
Rename: func(newName string) error {
|
||||
renamed = newName
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("data\n"), nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
// Perform rename
|
||||
err := tree.Rename("original", "new-name")
|
||||
if err != nil {
|
||||
t.Fatalf("Rename: %v", err)
|
||||
}
|
||||
|
||||
if renamed != "new-name" {
|
||||
t.Errorf("rename callback got %q, want 'new-name'", renamed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachEmptyBindings tests behavior when no instances exist
|
||||
func TestEachEmptyBindings(t *testing.T) {
|
||||
spec := DirNode("/",
|
||||
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
return nil, nil // empty
|
||||
},
|
||||
FileNode("file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("data\n"), nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
// List should return empty
|
||||
entries, err := tree.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("List: got %d entries, want 0", len(entries))
|
||||
}
|
||||
|
||||
// Access non-existent should fail
|
||||
_, err = tree.Stat("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("Stat(nonexistent) should fail when no instances")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachMixedStaticAndDynamic tests Blueprint alongside static children
|
||||
func TestEachMixedStaticAndDynamic(t *testing.T) {
|
||||
spec := DirNode("/",
|
||||
FileNode("static-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("static\n"), nil
|
||||
})),
|
||||
DirNode("static-dir",
|
||||
FileNode("nested", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("nested-static\n"), nil
|
||||
})),
|
||||
),
|
||||
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
return []Binding{
|
||||
{Name: "dynamic1"},
|
||||
{Name: "dynamic2"},
|
||||
}, nil
|
||||
},
|
||||
FileNode("dynamic-file", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("dynamic\n"), nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
entries, err := tree.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
|
||||
// Should have: static-file, static-dir, dynamic1, dynamic2
|
||||
names := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
names[e.Name()] = true
|
||||
}
|
||||
|
||||
expected := []string{"static-file", "static-dir", "dynamic1", "dynamic2"}
|
||||
for _, name := range expected {
|
||||
if !names[name] {
|
||||
t.Errorf("List: missing %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Read from static
|
||||
f1, err := tree.Open("static-file")
|
||||
if err != nil {
|
||||
t.Fatalf("Open(static-file): %v", err)
|
||||
}
|
||||
d1, _ := f1.Read()
|
||||
if string(d1) != "static\n" {
|
||||
t.Errorf("static-file = %q", d1)
|
||||
}
|
||||
|
||||
// Read from dynamic
|
||||
f2, err := tree.Open("dynamic1/dynamic-file")
|
||||
if err != nil {
|
||||
t.Fatalf("Open(dynamic1/dynamic-file): %v", err)
|
||||
}
|
||||
d2, _ := f2.Read()
|
||||
if string(d2) != "dynamic\n" {
|
||||
t.Errorf("dynamic-file = %q", d2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachStatMode tests that blueprint instances get correct file mode
|
||||
func TestEachStatMode(t *testing.T) {
|
||||
spec := DirNode("/",
|
||||
Each("{id}", func(ctx HandlerCtx) ([]Binding, error) {
|
||||
return []Binding{{Name: "instance"}}, nil
|
||||
},
|
||||
FileNode("readable", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("r\n"), nil
|
||||
})),
|
||||
FileNode("writable", 0666,
|
||||
Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("rw\n"), nil
|
||||
}),
|
||||
Write(func(ctx HandlerCtx, data []byte) error {
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
FileNode("executable", 0555, Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte("#!/bin/sh\n"), nil
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
tree := BuildTree(spec, HandlerCtx{})
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
mode os.FileMode
|
||||
}{
|
||||
{"instance", os.ModeDir | 0755}, // directory
|
||||
{"instance/readable", 0444},
|
||||
{"instance/writable", 0666},
|
||||
{"instance/executable", 0555},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
fi, err := tree.Stat(tc.path)
|
||||
if err != nil {
|
||||
t.Errorf("Stat(%q): %v", tc.path, err)
|
||||
continue
|
||||
}
|
||||
if fi.Mode() != tc.mode {
|
||||
t.Errorf("Stat(%q).Mode() = %o, want %o", tc.path, fi.Mode(), tc.mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"ollie/agent"
|
||||
"ollie/backend"
|
||||
"ollie/elevate"
|
||||
"ollie/fsedsl"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ func readHelp(_ HandlerCtx) ([]byte, error) {
|
|||
func init() {
|
||||
helpFn = func() string {
|
||||
helpOnce.Do(func() {
|
||||
helpText = GenerateHelp(treeSpec)
|
||||
helpText = fsedsl.GenerateHelp(treeSpec)
|
||||
})
|
||||
return helpText
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os/user"
|
||||
|
||||
"ollie/elevate"
|
||||
"ollie/fsedsl"
|
||||
olog "ollie/log"
|
||||
"ollie/skills"
|
||||
coresession "ollie/session"
|
||||
|
|
@ -56,7 +57,7 @@ func NewRoot(cfg Config) *Tree {
|
|||
|
||||
// Build the tree from the spec.
|
||||
// Validation happens inside BuildTree.
|
||||
tree := BuildTree(treeSpec, ctx)
|
||||
tree := fsedsl.BuildTree(treeSpec, ctx)
|
||||
ctx.Root = tree // now handlers can access root tree via ctx.Root
|
||||
|
||||
// Initialize root state.
|
||||
|
|
|
|||
20
fs/spec.go
20
fs/spec.go
|
|
@ -4,6 +4,26 @@ package fs
|
|||
// Every file and directory is declared here with Doc() descriptions.
|
||||
// treeSpecUID/GID are set at NewRoot() time from the current system user.
|
||||
|
||||
import "ollie/fsedsl"
|
||||
|
||||
// Local aliases for fsedsl constructors, specialized to HandlerCtx.
|
||||
var (
|
||||
DirNode = fsedsl.DirNode[HandlerCtx]
|
||||
FileNode = fsedsl.FileNode[HandlerCtx]
|
||||
Each = fsedsl.Each[HandlerCtx]
|
||||
Doc = fsedsl.Doc[HandlerCtx]
|
||||
Read = fsedsl.Read[HandlerCtx]
|
||||
Write = fsedsl.Write[HandlerCtx]
|
||||
Stream = fsedsl.Stream[HandlerCtx]
|
||||
BlockOnce = fsedsl.BlockOnce[HandlerCtx]
|
||||
Request = fsedsl.Request[HandlerCtx]
|
||||
Remove = fsedsl.RemoveNode[HandlerCtx]
|
||||
Stat = fsedsl.StatOverride[HandlerCtx]
|
||||
UID = fsedsl.UID[HandlerCtx]
|
||||
GID = fsedsl.GID[HandlerCtx]
|
||||
Bind = fsedsl.Bind[HandlerCtx]
|
||||
)
|
||||
|
||||
var treeSpecUID, treeSpecGID string
|
||||
|
||||
var treeSpec = DirNode("/",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package fs
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"ollie/fsedsl"
|
||||
)
|
||||
|
||||
// --- helpers ---
|
||||
|
|
@ -136,18 +138,18 @@ func checkStoreContract(t *testing.T, s FileTree, name string) {
|
|||
// ===== Tree =====
|
||||
|
||||
func TestTreeContract(t *testing.T) {
|
||||
checkStoreContract(t, NewTree([]string{t.TempDir()}, 0644), "test-file")
|
||||
checkStoreContract(t, fsedsl.NewTree([]string{t.TempDir()}, 0644), "test-file")
|
||||
}
|
||||
|
||||
func TestTreeCreateMkdirError(t *testing.T) {
|
||||
fd := NewTree([]string{"/nonexistent/path"}, 0644)
|
||||
fd := fsedsl.NewTree([]string{"/nonexistent/path"}, 0644)
|
||||
if err := fd.Create("f"); err == nil {
|
||||
t.Error("Create should fail when dir doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreePutMkdirError(t *testing.T) {
|
||||
fd := NewTree([]string{"/nonexistent/path"}, 0644)
|
||||
fd := fsedsl.NewTree([]string{"/nonexistent/path"}, 0644)
|
||||
e, err := fd.Open("f")
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
|
|
|
|||
|
|
@ -107,12 +107,12 @@ func main() {
|
|||
|
||||
## Integration Guide
|
||||
|
||||
For larger projects, create a package that wraps fsedsl with your context type:
|
||||
For larger projects, create an adapter package that specializes fsedsl for your context type.
|
||||
|
||||
### Step 1: Define your context type
|
||||
### Step 1: Define your context type and type aliases
|
||||
|
||||
```go
|
||||
// myproject/fs/types.go
|
||||
// myproject/fs/adapter.go
|
||||
package fs
|
||||
|
||||
import "git.lneely.de/lkn/fsedsl"
|
||||
|
|
@ -124,7 +124,7 @@ type Ctx struct {
|
|||
Session *Session
|
||||
}
|
||||
|
||||
// Type aliases for cleaner code
|
||||
// Type aliases for external use
|
||||
type FsNodeDecl = fsedsl.FsNodeDecl[Ctx]
|
||||
type NodeOption = fsedsl.NodeOption[Ctx]
|
||||
type Binding = fsedsl.Binding[Ctx]
|
||||
|
|
@ -132,51 +132,36 @@ type Tree = fsedsl.Tree
|
|||
type File = fsedsl.File
|
||||
```
|
||||
|
||||
### Step 2: Create wrapper functions (optional)
|
||||
### Step 2: Create local function aliases
|
||||
|
||||
If you prefer not to repeat `[Ctx]` everywhere:
|
||||
|
||||
```go
|
||||
// myproject/fs/edsl.go
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"git.lneely.de/lkn/fsedsl"
|
||||
)
|
||||
|
||||
func DirNode(name string, args ...any) FsNodeDecl {
|
||||
return fsedsl.DirNode[Ctx](name, args...)
|
||||
}
|
||||
|
||||
func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
|
||||
return fsedsl.FileNode[Ctx](name, mode, opts...)
|
||||
}
|
||||
|
||||
func Each(name string, bindings func(Ctx) ([]Binding, error), args ...any) FsNodeDecl {
|
||||
return fsedsl.Each[Ctx](name, bindings, args...)
|
||||
}
|
||||
|
||||
func Read(fn func(Ctx) ([]byte, error)) NodeOption { return fsedsl.Read[Ctx](fn) }
|
||||
func Write(fn func(Ctx, []byte) error) NodeOption { return fsedsl.Write[Ctx](fn) }
|
||||
func Doc(desc string) NodeOption { return fsedsl.Doc[Ctx](desc) }
|
||||
// ... other options as needed
|
||||
|
||||
func BuildTree(spec FsNodeDecl, rootCtx Ctx) *Tree {
|
||||
return fsedsl.BuildTree(spec, rootCtx)
|
||||
}
|
||||
|
||||
func GenerateHelp(root FsNodeDecl) string {
|
||||
return fsedsl.GenerateHelp(root)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Define your filesystem spec
|
||||
In the file where you define your spec, create package-local variable aliases
|
||||
that bake in your context type. This keeps the spec clean without requiring
|
||||
`[Ctx]` on every call:
|
||||
|
||||
```go
|
||||
// myproject/fs/spec.go
|
||||
package fs
|
||||
|
||||
import "git.lneely.de/lkn/fsedsl"
|
||||
|
||||
// Local aliases for fsedsl constructors, specialized to Ctx.
|
||||
var (
|
||||
DirNode = fsedsl.DirNode[Ctx]
|
||||
FileNode = fsedsl.FileNode[Ctx]
|
||||
Each = fsedsl.Each[Ctx]
|
||||
Doc = fsedsl.Doc[Ctx]
|
||||
Read = fsedsl.Read[Ctx]
|
||||
Write = fsedsl.Write[Ctx]
|
||||
Stream = fsedsl.Stream[Ctx]
|
||||
BlockOnce = fsedsl.BlockOnce[Ctx]
|
||||
Request = fsedsl.Request[Ctx]
|
||||
Remove = fsedsl.RemoveNode[Ctx]
|
||||
Stat = fsedsl.StatOverride[Ctx]
|
||||
UID = fsedsl.UID[Ctx]
|
||||
GID = fsedsl.GID[Ctx]
|
||||
Bind = fsedsl.Bind[Ctx]
|
||||
)
|
||||
|
||||
var Spec = DirNode("/",
|
||||
FileNode("version", 0444,
|
||||
Doc("API version"),
|
||||
|
|
@ -219,17 +204,20 @@ func userBindings(ctx Ctx) ([]Binding, error) {
|
|||
}
|
||||
```
|
||||
|
||||
### Step 4: Build and use the tree
|
||||
### Step 3: Build and use the tree
|
||||
|
||||
```go
|
||||
// myproject/main.go
|
||||
package main
|
||||
|
||||
import "myproject/fs"
|
||||
import (
|
||||
"myproject/fs"
|
||||
"git.lneely.de/lkn/fsedsl"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := fs.Ctx{DB: connectDB()}
|
||||
tree := fs.BuildTree(fs.Spec, ctx)
|
||||
tree := fsedsl.BuildTree(fs.Spec, ctx)
|
||||
|
||||
// Use tree.List(), tree.Open(), tree.Stat(), etc.
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue