code.oscarkilo.com/okg/internal/rest/rest.go

..
rest.go
// Package rest is the shared base for okg's per-service HTTP
// clients (//okg/klee, //okg/who, etc.). It owns the
// host/api-key/transport plumbing and JSON marshaling; each
// domain-specific client embeds *Client and adds typed methods.
package rest

import "encoding/json"
import "fmt"
import "io"
import "net/http"
import "strings"

// Client carries the host, API key, and underlying http.Client
// that the JSON helpers below dispatch through.
type Client struct {
  Host   string
  APIKey string
  HTTP   *http.Client
}

func NewClient(host, apiKey string) *Client {
  return &Client{
    Host:   host,
    APIKey: apiKey,
    HTTP:   &http.Client{},
  }
}

// Do builds a request to c.Host+path with the standard headers
// (Accept, Authorization, Content-Type when a body is present)
// and runs it through c.HTTP.
func (c *Client) Do(
  method, path string, body io.Reader,
) (*http.Response, error) {
  req, err := http.NewRequest(method, c.Host+path, body)
  if err != nil {
    return nil, err
  }
  req.Header.Set("Accept", "application/json")
  if c.APIKey != "" {
    req.Header.Set(
      "Authorization", "Bearer "+c.APIKey)
  }
  if body != nil {
    req.Header.Set(
      "Content-Type", "application/json")
  }
  return c.HTTP.Do(req)
}

// GetJSON issues a GET, returns an HTTP-status error on 4xx/5xx,
// and decodes a successful body into dst (or skips decode if
// dst is nil).
func (c *Client) GetJSON(
  path string, dst interface{},
) error {
  return c.exchange("GET", path, nil, dst)
}

// PostJSON issues a POST with payload (omitted if nil) and
// decodes the response into dst (or skips decode if dst is nil).
func (c *Client) PostJSON(
  path string, payload, dst interface{},
) error {
  return c.exchange("POST", path, payload, dst)
}

// PatchJSON issues a PATCH; payload is required.
func (c *Client) PatchJSON(
  path string, payload, dst interface{},
) error {
  return c.exchange("PATCH", path, payload, dst)
}

func (c *Client) exchange(
  method, path string, payload, dst interface{},
) error {
  var body io.Reader
  if payload != nil {
    data, err := json.Marshal(payload)
    if err != nil {
      return err
    }
    body = strings.NewReader(string(data))
  }
  resp, err := c.Do(method, path, body)
  if err != nil {
    return err
  }
  defer resp.Body.Close()
  if resp.StatusCode >= 400 {
    b, _ := io.ReadAll(resp.Body)
    return fmt.Errorf(
      "HTTP %d: %s", resp.StatusCode, b)
  }
  if dst != nil {
    return json.NewDecoder(resp.Body).Decode(dst)
  }
  return nil
}