feat(garmin): expose ErrNotFound for a wrapper-reported 404

roundTrip now wraps the returned error with the new ErrNotFound
sentinel whenever the wrapper's response set not_found (Task 1),
letting any Client method's caller distinguish a definitive 404 from
a transient failure via errors.Is, regardless of which garminconnect
method was called.
This commit is contained in:
2026-07-27 18:53:59 +02:00
parent 1d878e0f46
commit c6e2c5c00e
2 changed files with 52 additions and 7 deletions

View File

@@ -8,6 +8,7 @@ import (
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
@@ -77,11 +78,20 @@ type wireRequest struct {
// wireResponse is one line read from the wrapper subprocess's stdout.
type wireResponse struct {
ID int `json:"id"`
Result json.RawMessage `json:"result,omitempty"`
Error string `json:"error,omitempty"`
ID int `json:"id"`
Result json.RawMessage `json:"result,omitempty"`
Error string `json:"error,omitempty"`
NotFound bool `json:"not_found,omitempty"`
}
// ErrNotFound wraps any error a Client method returns when the wrapper
// reported a definitive HTTP 404 (garminconnect's own
// GarminConnectNotFoundError) -- e.g. GetWorkoutByID for a workout deleted
// on Garmin's side after being linked to an activity. Callers use
// errors.Is(err, ErrNotFound) to distinguish this from a transient failure
// worth retrying.
var ErrNotFound = errors.New("garmin: resource not found")
// callParams is the Params payload for a generic "call" request: dispatches
// to any garminconnect.Garmin method by name.
type callParams struct {
@@ -254,7 +264,11 @@ func (c *subprocessClient) roundTrip(ctx context.Context, cmdName string, params
return nil, err
}
if resp.Error != "" {
err = fmt.Errorf("%s: %s", cmdName, resp.Error)
if resp.NotFound {
err = fmt.Errorf("%s: %s: %w", cmdName, resp.Error, ErrNotFound)
} else {
err = fmt.Errorf("%s: %s", cmdName, resp.Error)
}
return nil, err
}
result = resp.Result