From 97535f650ac96b2f7235a5f4cbf26ee883eef9bf Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 06:17:48 +0000 Subject: [PATCH] docs(ax): align guidance with current medium surface Co-Authored-By: Virgil --- CLAUDE.md | 6 +++--- docs/architecture.md | 8 ++++---- docs/development.md | 28 +++++++++++++++------------- docs/index.md | 7 +++---- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5b03b0b..abe2112 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ GOWORK=off go test -cover ./... ### Core Interface -`io.Medium` — 18 methods: Read, Write, EnsureDir, IsFile, FileGet, FileSet, Delete, DeleteAll, Rename, List, Stat, Open, Create, Append, ReadStream, WriteStream, Exists, IsDir. +`io.Medium` — 17 methods: Read, Write, WriteMode, EnsureDir, IsFile, Delete, DeleteAll, Rename, List, Stat, Open, Create, Append, ReadStream, WriteStream, Exists, IsDir. ```go // Sandboxed to a project directory @@ -60,7 +60,7 @@ io.Copy(s3Medium, "backup.tar", localMedium, "restore/backup.tar") | `datanode` | Borg DataNode | Thread-safe (RWMutex) in-memory, snapshot/restore via tar | | `store` | SQLite KV store | Group-namespaced key-value with Go template rendering | | `workspace` | Core service | Encrypted workspaces, SHA-256 IDs, PGP keypairs | -| `MockMedium` | In-memory map | Testing — no filesystem needed | +| `MemoryMedium` | In-memory map | Testing — no filesystem needed | `store.Medium` maps filesystem paths as `group/key` — first path segment is the group, remainder is the key. `List("")` returns groups as directories. @@ -132,4 +132,4 @@ Sentinel errors (`var ErrNotFound`, `var ErrInvalidKey`, etc.) use standard `err ## Testing -Use `io.MockMedium` or `io.NewSandboxed(t.TempDir())` in tests — never hit real S3/SQLite unless integration testing. S3 tests use an interface-based mock (`s3.Client`). +Use `io.NewMemoryMedium()` or `io.NewSandboxed(t.TempDir())` in tests — never hit real S3/SQLite unless integration testing. S3 tests use an interface-based mock (`s3.Client`). diff --git a/docs/architecture.md b/docs/architecture.md index 801121a..8db0246 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -25,7 +25,7 @@ The `Medium` interface is defined in `io.go`. It is the only type that consuming - **`io.Local`** — a package-level variable initialised in `init()` via `local.New("/")`. This gives unsandboxed access to the host filesystem, mirroring the behaviour of the standard `os` package. - **`io.NewSandboxed(root)`** — creates a `local.Medium` restricted to `root`. All path resolution is confined within that directory. - **`io.Copy(src, srcPath, dst, dstPath)`** — copies a file between any two mediums by reading from one and writing to the other. -- **`io.MockMedium`** — a fully functional in-memory implementation for unit tests. It tracks files, directories, and modification times in plain maps. +- **`io.NewMemoryMedium()`** — a fully functional in-memory implementation for unit tests. It tracks files, directories, and modification times in plain maps. ### FileInfo and DirEntry (root package) @@ -36,7 +36,7 @@ Simple struct implementations of `fs.FileInfo` and `fs.DirEntry` are exported fr ### local.Medium -**File:** `local/client.go` +**File:** `local/medium.go` The local backend wraps the standard `os` package with two layers of path protection: @@ -100,7 +100,7 @@ Key capabilities beyond `Medium`: ### datanode.Medium -**File:** `datanode/client.go` +**File:** `datanode/medium.go` A thread-safe `Medium` backed by Borg's `DataNode` (an in-memory `fs.FS` with tar serialisation). It adds: @@ -271,7 +271,7 @@ Application code +-- node.Node --> in-memory map + tar serialisation +-- datanode.Medium --> Borg DataNode + sync.RWMutex +-- store.Medium --> store.Store (SQLite KV) --> Medium adapter - +-- MockMedium --> map[string]string (for tests) + +-- MemoryMedium --> map[string]string (for tests) ``` Every backend normalises paths using the same `path.Clean("/" + p)` pattern, ensuring consistent behaviour regardless of which backend is in use. diff --git a/docs/development.md b/docs/development.md index 2e95ad7..6ece61b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -88,18 +88,20 @@ func TestDelete_Bad_DirNotEmpty(t *testing.T) { /* returns error for non-empty d ## Writing Tests Against Medium -Use `MockMedium` from the root package for unit tests that need a storage backend but should not touch disk: +Use `MemoryMedium` from the root package for unit tests that need a storage backend but should not touch disk: ```go func TestMyFeature(t *testing.T) { - m := io.NewMockMedium() - m.Files["config.yaml"] = "key: value" - m.Dirs["data"] = true + m := io.NewMemoryMedium() + _ = m.Write("config.yaml", "key: value") + _ = m.EnsureDir("data") // Your code under test receives m as an io.Medium result, err := myFunction(m) assert.NoError(t, err) - assert.Equal(t, "expected", m.Files["output.txt"]) + output, err := m.Read("output.txt") + require.NoError(t, err) + assert.Equal(t, "expected", output) } ``` @@ -134,7 +136,7 @@ func TestWithSQLite(t *testing.T) { To add a new `Medium` implementation: 1. Create a new package directory (e.g., `sftp/`). -2. Define a struct that implements all 18 methods of `io.Medium`. +2. Define a struct that implements all 17 methods of `io.Medium`. 3. Add a compile-time check at the top of your file: ```go @@ -142,7 +144,7 @@ var _ coreio.Medium = (*Medium)(nil) ``` 4. Normalise paths using `path.Clean("/" + p)` to prevent traversal escapes. This is the convention followed by every existing backend. -5. Handle `nil` and empty input consistently: check how `MockMedium` and `local.Medium` behave and match that behaviour. +5. Handle `nil` and empty input consistently: check how `MemoryMedium` and `local.Medium` behave and match that behaviour. 6. Write tests using the `_Good` / `_Bad` / `_Ugly` naming convention. 7. Add your package to the table in `docs/index.md`. @@ -171,13 +173,13 @@ To add a new data transformation: ``` go-io/ -├── io.go # Medium interface, helpers, MockMedium -├── client_test.go # Tests for MockMedium and helpers +├── io.go # Medium interface, helpers, MemoryMedium +├── medium_test.go # Tests for MemoryMedium and helpers ├── bench_test.go # Benchmarks ├── go.mod ├── local/ -│ ├── client.go # Local filesystem backend -│ └── client_test.go +│ ├── medium.go # Local filesystem backend +│ └── medium_test.go ├── s3/ │ ├── s3.go # S3 backend │ └── s3_test.go @@ -188,8 +190,8 @@ go-io/ │ ├── node.go # In-memory fs.FS + Medium │ └── node_test.go ├── datanode/ -│ ├── client.go # Borg DataNode Medium wrapper -│ └── client_test.go +│ ├── medium.go # Borg DataNode Medium wrapper +│ └── medium_test.go ├── store/ │ ├── store.go # KV store │ ├── medium.go # Medium adapter for KV store diff --git a/docs/index.md b/docs/index.md index 2ac0992..9ce4c25 100644 --- a/docs/index.md +++ b/docs/index.md @@ -41,7 +41,7 @@ _ = bucket.Write("photo.jpg", rawData) | Package | Import Path | Purpose | |---------|-------------|---------| -| `io` (root) | `forge.lthn.ai/core/go-io` | `Medium` interface, helper functions, `MockMedium` for tests | +| `io` (root) | `forge.lthn.ai/core/go-io` | `Medium` interface, helper functions, `MemoryMedium` for tests | | `local` | `forge.lthn.ai/core/go-io/local` | Local filesystem backend with path sandboxing and symlink-escape protection | | `s3` | `forge.lthn.ai/core/go-io/s3` | Amazon S3 / S3-compatible backend (Garage, MinIO, etc.) | | `sqlite` | `forge.lthn.ai/core/go-io/sqlite` | SQLite-backed virtual filesystem (pure Go driver, no CGO) | @@ -54,15 +54,14 @@ _ = bucket.Write("photo.jpg", rawData) ## The Medium Interface -Every storage backend implements the same 18-method interface: +Every storage backend implements the same 17-method interface: ```go type Medium interface { // Content operations Read(path string) (string, error) Write(path, content string) error - FileGet(path string) (string, error) // alias for Read - FileSet(path, content string) error // alias for Write + WriteMode(path, content string, mode fs.FileMode) error // Streaming (for large files) ReadStream(path string) (io.ReadCloser, error)