docs(ax): align guidance with current medium surface
Some checks failed
CI / test (push) Failing after 2s
CI / auto-fix (push) Failing after 0s
CI / auto-merge (push) Failing after 0s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-31 06:17:48 +00:00
parent 3054217038
commit 97535f650a
4 changed files with 25 additions and 24 deletions

View file

@ -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`).

View file

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

View file

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

View file

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