Write a provider
A provider is a small, self-contained Go module that resolves one URL scheme. This page is the complete contract: implement the interface, follow the rules, and pass the conformance kit.
Module layout
Each provider is its own module so a backend SDK never leaks into the core or into other providers. Create it under providers/<name>:
providers/<name>/
go.mod module github.com/xavidop/mamori/providers/<name>
<name>.go the provider
<name>_test.go unit tests + providertest.Run against a fake
README.md scheme, ref grammar, auth, what is verified
The go.mod requires the core module and, during local development in the monorepo, points at it with a replace directive:
module github.com/xavidop/mamori/providers/<name>
go 1.26
require github.com/xavidop/mamori v0.1.0
replace github.com/xavidop/mamori => ../..
Consumers install it on its own, so your SDK dependency is opt-in:
go get github.com/xavidop/mamori/providers/<name>
The interface
package myprovider
import (
"context"
"github.com/xavidop/mamori"
)
type Provider struct{ /* client, config */ }
func New(opts ...Option) *Provider { /* ... */ return &Provider{} }
func (p *Provider) Scheme() string { return "myscheme" }
func (p *Provider) Resolve(ctx context.Context, ref mamori.Ref) (mamori.Value, error) {
raw, err := p.fetch(ctx, ref.Path) // your backend call
if isNotFound(err) {
return mamori.Value{}, mamori.ErrNotFound // MUST satisfy errors.Is
}
if err != nil {
return mamori.Value{}, err
}
if ref.Key != "" { // #key selects from a JSON payload, identically everywhere
raw, err = mamori.SelectKey(raw, ref.Key)
if err != nil {
return mamori.Value{}, err
}
}
return mamori.Value{
Bytes: raw,
Version: backendRevision, // or mamori.VersionHash(raw)
Sensitive: true, // true for secret managers
}, nil
}
func init() { mamori.Register(New()) } // database/sql pattern; panics on duplicate scheme
Rules
These keep every provider interchangeable:
- Return an error satisfying
errors.Is(err, mamori.ErrNotFound)for missing values (never nil error + empty bytes). - Set
Value.Versionfrom a native revision, ormamori.VersionHash(bytes). It must change when the value changes. - Use
mamori.SelectKey(payload, ref.Key)for#keyselection so it behaves the same across providers. - Never log the payload.
- Implement
Watchonly if the backend has native change notification; otherwise mamori polls for you. ImplementResolveBatchif the backend can fetch many refs in one call. - Honor
ctxin every network call.
Native watch
// Optional. Implement only for backends that can push (informers, blocking queries, fsnotify).
func (p *Provider) Watch(ctx context.Context, ref mamori.Ref) (<-chan mamori.Update, error) {
ch := make(chan mamori.Update, 1)
go func() {
defer close(ch) // MUST close on ctx cancel; no goroutine leaks
// ...subscribe, emit mamori.Update{Value: v} on change...
}()
return ch, nil
}
The conformance kit
github.com/xavidop/mamori/providertest runs one function that exercises resolution, not-found typing, Version monotonicity, concurrency, context cancellation, native watch, goroutine hygiene (goleak), and a no-payload-logging assertion. A provider that passes behaves identically to every other one.
func TestConformance(t *testing.T) {
backend := newInMemoryFake()
providertest.Run(t, providertest.Config{
New: func() mamori.Provider { return myprovider.New(myprovider.WithClient(backend)) },
Ref: func(key string) string { return "myscheme://" + key },
Seed: func(ctx context.Context, key, val string) error { return backend.set(key, val) },
Mutate: func(ctx context.Context, key, val string) error { return backend.set(key, val) },
})
}
Inject a client interface so the kit (and your unit tests) run against an in-memory fake, with live-backend tests behind a //go:build integration tag. A provider that passes the kit earns a badge in the registry.
Build and test
Each module is built and tested independently, with the workspace disabled (this is exactly what CI does per module):
cd providers/<name>
GOWORK=off go mod tidy
GOWORK=off go build ./...
GOWORK=off go vet ./...
GOWORK=off go test ./...
Or, from the repo root, make test / make lint run every module for you.
Acceptance checklist
-
Scheme()returns your scheme;Resolvereturnsmamori.ErrNotFound(viaerrors.Is) for missing values. -
Value.Versionis set and changes when the value changes; secret-bearing values setSensitive: true. -
#keyusesmamori.SelectKey; the payload is never logged. -
Watchis implemented only for native-push backends, closes onctxcancel, and leaks no goroutines. - A client interface is injected so
providertest.Runpasses against an in-memory fake; live tests are behind//go:build integration. -
go build,go vet, andgo testare clean withGOWORK=off; the README documents scheme, ref grammar, and auth.