package httpclient import ( "bytes" "context" "encoding/json" "io" "net/http" "code.nochebuena.dev/go/xerrors" ) // DoJSON executes req and decodes the JSON response body into T. // Returns a xerrors-typed error for HTTP 4xx/5xx responses. func DoJSON[T any](ctx context.Context, client Client, req *http.Request) (*T, error) { req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, MapStatusToError(resp.StatusCode, "external API error") } body, err := io.ReadAll(resp.Body) if err != nil { return nil, xerrors.New(xerrors.ErrInternal, "failed to read response body").WithError(err) } var data T if err := json.Unmarshal(body, &data); err != nil { return nil, xerrors.New(xerrors.ErrInternal, "failed to decode JSON response").WithError(err) } return &data, nil } // DoJSONRequest serialises body as JSON, POST/PUT/PATCHes it to rawURL, and // decodes the response into Resp. For requests without a body, use DoJSON instead. func DoJSONRequest[Req, Resp any](ctx context.Context, client Client, method, rawURL string, body Req) (*Resp, error) { data, err := json.Marshal(body) if err != nil { return nil, xerrors.New(xerrors.ErrInternal, "failed to encode request body").WithError(err) } req, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewReader(data)) if err != nil { return nil, xerrors.New(xerrors.ErrInternal, "failed to create request").WithError(err) } req.Header.Set("Content-Type", "application/json") return DoJSON[Resp](ctx, client, req) } // MapStatusToError maps an HTTP status code to the matching xerrors type. func MapStatusToError(code int, msg string) error { switch code { case http.StatusNotFound: return xerrors.New(xerrors.ErrNotFound, msg) case http.StatusBadRequest: return xerrors.New(xerrors.ErrInvalidInput, msg) case http.StatusUnauthorized: return xerrors.New(xerrors.ErrUnauthorized, msg) case http.StatusForbidden: return xerrors.New(xerrors.ErrPermissionDenied, msg) case http.StatusConflict: return xerrors.New(xerrors.ErrAlreadyExists, msg) case http.StatusTooManyRequests: return xerrors.New(xerrors.ErrUnavailable, msg) default: return xerrors.New(xerrors.ErrInternal, msg) } }