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.Version from a native revision or mamori.VersionHash(bytes). It must change whenever the value changes; that is what mamori uses for change detection.
  • Use mamori.SelectKey(payload, ref.Key) for #key selection.
  • Set Sensitive: true on secret-bearing values; never log the payload.
  • Honor ctx in 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 == "" returns data unchanged.
  • A key beginning 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 key is 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.

KindUse for
ErrNotFoundKey, secret, path, or version genuinely absent
ErrPermissionDeniedAuthenticated but not authorized: IAM deny, Vault policy, RBAC
ErrUnauthenticatedMissing, malformed, or expired credentials; failed token renewal
ErrUnavailableNetwork failure, DNS, timeout, 5xx, circuit open
ErrRateLimitedThrottling, quota exhaustion, 429
ErrInvalidThe 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.