Enable the `use-any` `revive` rule which requires replacing the empty
interface `interface{}` with `any`.
126 lines
2.8 KiB
Go
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
|
|
}
|