Files
avo/internal/github/client.go
Michael McLoughlin 11e0a2138b all: enable use-any revive rule (#417)
Enable the `use-any` `revive` rule which requires replacing the empty
interface `interface{}` with `any`.
2024-01-07 13:16:59 -08:00

126 lines
2.8 KiB
Go

// Package github provides a client for the Github REST API.
package github
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
)
// Client for the Github REST API.
type Client struct {
client *http.Client
base string
token string
}
// Option configures a Github client.
type Option func(*Client)
// WithHTTPClient configures the HTTP client that should be used for Github API
// requests.
func WithHTTPClient(h *http.Client) Option {
return func(c *Client) { c.client = h }
}
// WithToken configures a Client with an authentication token for Github API
// requests.
func WithToken(token string) Option {
return func(c *Client) { c.token = token }
}
// WithTokenFromEnvironment configures a Client using the GITHUB_TOKEN
// environment variable.
func WithTokenFromEnvironment() Option {
return WithToken(os.Getenv("GITHUB_TOKEN"))
}
// NewClient initializes a client using the given HTTP client.
func NewClient(opts ...Option) *Client {
c := &Client{
client: http.DefaultClient,
base: "https://api.github.com",
}
for _, opt := range opts {
opt(c)
}
return c
}
// Repository gets information about the given Github repository.
func (c *Client) Repository(ctx context.Context, owner, name string) (*Repository, error) {
// Build request.
u := c.base + "/repos/" + owner + "/" + name
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
// Execute.
repo := &Repository{}
if err := c.request(req, repo); err != nil {
return nil, err
}
return repo, nil
}
// Issue gets information about the given Github issue.
func (c *Client) Issue(ctx context.Context, owner, name string, number int) (*Issue, error) {
// Build request.
u := fmt.Sprintf("%s/repos/%s/%s/issues/%d", c.base, owner, name, number)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
// Execute.
issue := &Issue{}
if err := c.request(req, issue); err != nil {
return nil, err
}
return issue, nil
}
func (c *Client) request(req *http.Request, payload any) (err error) {
// Add common headers.
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
// Execute the request.
res, err := c.client.Do(req)
if err != nil {
return err
}
defer func() {
if errc := res.Body.Close(); errc != nil && err == nil {
err = errc
}
}()
// Check status.
if res.StatusCode != http.StatusOK {
return fmt.Errorf("http status %d: %s", res.StatusCode, http.StatusText(res.StatusCode))
}
// Parse response body.
d := json.NewDecoder(res.Body)
if err := d.Decode(payload); err != nil {
return err
}
// Should not have trailing data.
if d.More() {
return errors.New("unexpected extra data after JSON")
}
return nil
}