Resolve and errors
Resolve returns the current mamori.Value for a Ref and classifies every failure. Get this right and your provider is interchangeable with every other one.
Implement Resolve
A Ref gives you Path (the backend location), Key (the #key fragment, empty when absent), and Opts (query options):
func (p *Provider) Resolve(ctx context.Context, ref mamori.Ref) (mamori.Value, error) {
raw, err := p.client.get(ctx, ref.Path)
if isNotFound(err) {
return mamori.Value{}, mamori.ErrNotFound
}
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: mamori.VersionHash(raw), // or a native backend revision
Sensitive: true, // true for secret managers
}, nil
}
Rules that keep providers interchangeable:
- Missing values: return an error satisfying
errors.Is(err, mamori.ErrNotFound), never a nil error with empty bytes. - Set
Value.Versionfrom a native revision ormamori.VersionHash(bytes). It must change whenever the value changes; that is what mamori uses for change detection. - Use
mamori.SelectKey(payload, ref.Key)for#keyselection. - Set
Sensitive: trueon secret-bearing values; never log the payload. - Honor
ctxin every network call.
SelectKey’s contract
SelectKey(data []byte, key string) ([]byte, error) handles both forms ref.Key can take, so your provider never has to branch on them itself:
key == ""returnsdataunchanged.- A
keybeginning with/is an RFC 6901 JSON Pointer, addressing a value at any depth through objects and array elements ("/credentials/password","/replicas/5/host"). - Any other
keyis a literal top-level key, exactly as before.
Calling SelectKey is what earns your provider pointer support for free: a provider that hand-rolls its own map[string]json.RawMessage lookup instead passes every other conformance case and fails only JSONPointerSelection (see Conformance). An absent key or an out-of-range array index wraps ErrNotFound, so default:/optional still apply exactly as they do for a missing top-level key; a structural mismatch - a pointer descending into a scalar, a malformed array token, a bad escape, or a non-JSON payload - wraps ErrInvalid instead, because it is a malformed request against this payload rather than genuine absence. See Ref grammar for the full grammar and error table.
This only applies when your provider’s fragment is actually a JSON selector. Some providers use ref.Key for a backend-native lookup that has no JSON document to point into at all (a Kubernetes Secret’s data map, a facet like #variant/#payload on a feature-flag evaluation); those providers correctly skip SelectKey and are not expected to support pointers.
Map backend errors to kinds
ErrNotFound is the only error that changes mamori’s behavior (it triggers default: and optional handling). Classify every other failure too, so telemetry (mamori.error.kind) and callers using mamori.ErrorKind can tell an operator what went wrong.
Wrap the SDK error with the matching sentinel using two %w verbs, sentinel first:
var ae smithy.APIError
if errors.As(err, &ae) {
switch ae.ErrorCode() {
case "ResourceNotFoundException":
return mamori.Value{}, fmt.Errorf("%w: %w", mamori.ErrNotFound, err)
case "AccessDeniedException":
return mamori.Value{}, fmt.Errorf("%w: %w", mamori.ErrPermissionDenied, err)
case "ThrottlingException":
return mamori.Value{}, fmt.Errorf("%w: %w", mamori.ErrRateLimited, err)
}
}
return mamori.Value{}, err // unmapped: reports as unknown, which is fine
Two %w verbs keep errors.Is(err, mamori.ErrPermissionDenied) matching the sentinel while errors.As can still reach the original SDK error type.
Never use %v for the sentinel. It flattens the sentinel into a string and breaks the chain:
// WRONG: %v flattens the sentinel into a string and destroys the chain.
return mamori.Value{}, fmt.Errorf("secretsmanager: %v", mamori.ErrPermissionDenied)
This still compiles and reads fine, but errors.Is no longer matches and every failure reports as unknown. The ErrorClassification conformance case exists to catch exactly this.
Which kind to use
Map each failure to the sentinel that names its cause. Leaving an error unmapped (reported as unknown) is fine; guessing is not.
| Kind | Use for |
|---|---|
ErrNotFound | Key, secret, path, or version genuinely absent |
ErrPermissionDenied | Authenticated but not authorized: IAM deny, Vault policy, RBAC |
ErrUnauthenticated | Missing, malformed, or expired credentials; failed token renewal |
ErrUnavailable | Network failure, DNS, timeout, 5xx, circuit open |
ErrRateLimited | Throttling, quota exhaustion, 429 |
ErrInvalid | The ref is malformed for this provider, or the payload cannot be parsed |
| (unmapped) | Anything else. Reports as unknown, an honest answer. |
| (automatic) | context.DeadlineExceeded is classified as unavailable by ErrorKind; you need not map it. A plain context.Canceled reports unknown, since the caller withdrew the request. |
See Error kinds for how consumers read Kind.