Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
158 lines
3.9 KiB
Go
158 lines
3.9 KiB
Go
package forge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// APIError represents an error response from the Forgejo API.
|
|
type APIError struct {
|
|
StatusCode int
|
|
Message string
|
|
URL string
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("forge: %s %d: %s", e.URL, e.StatusCode, e.Message)
|
|
}
|
|
|
|
// IsNotFound returns true if the error is a 404 response.
|
|
func IsNotFound(err error) bool {
|
|
var apiErr *APIError
|
|
return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound
|
|
}
|
|
|
|
// IsForbidden returns true if the error is a 403 response.
|
|
func IsForbidden(err error) bool {
|
|
var apiErr *APIError
|
|
return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusForbidden
|
|
}
|
|
|
|
// IsConflict returns true if the error is a 409 response.
|
|
func IsConflict(err error) bool {
|
|
var apiErr *APIError
|
|
return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict
|
|
}
|
|
|
|
// Option configures the Client.
|
|
type Option func(*Client)
|
|
|
|
// WithHTTPClient sets a custom http.Client.
|
|
func WithHTTPClient(hc *http.Client) Option {
|
|
return func(c *Client) { c.httpClient = hc }
|
|
}
|
|
|
|
// WithUserAgent sets the User-Agent header.
|
|
func WithUserAgent(ua string) Option {
|
|
return func(c *Client) { c.userAgent = ua }
|
|
}
|
|
|
|
// Client is a low-level HTTP client for the Forgejo API.
|
|
type Client struct {
|
|
baseURL string
|
|
token string
|
|
httpClient *http.Client
|
|
userAgent string
|
|
}
|
|
|
|
// NewClient creates a new Forgejo API client.
|
|
func NewClient(url, token string, opts ...Option) *Client {
|
|
c := &Client{
|
|
baseURL: strings.TrimRight(url, "/"),
|
|
token: token,
|
|
httpClient: http.DefaultClient,
|
|
userAgent: "go-forge/0.1",
|
|
}
|
|
for _, opt := range opts {
|
|
opt(c)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Get performs a GET request.
|
|
func (c *Client) Get(ctx context.Context, path string, out any) error {
|
|
return c.do(ctx, http.MethodGet, path, nil, out)
|
|
}
|
|
|
|
// Post performs a POST request.
|
|
func (c *Client) Post(ctx context.Context, path string, body, out any) error {
|
|
return c.do(ctx, http.MethodPost, path, body, out)
|
|
}
|
|
|
|
// Patch performs a PATCH request.
|
|
func (c *Client) Patch(ctx context.Context, path string, body, out any) error {
|
|
return c.do(ctx, http.MethodPatch, path, body, out)
|
|
}
|
|
|
|
// Put performs a PUT request.
|
|
func (c *Client) Put(ctx context.Context, path string, body, out any) error {
|
|
return c.do(ctx, http.MethodPut, path, body, out)
|
|
}
|
|
|
|
// Delete performs a DELETE request.
|
|
func (c *Client) Delete(ctx context.Context, path string) error {
|
|
return c.do(ctx, http.MethodDelete, path, nil, nil)
|
|
}
|
|
|
|
func (c *Client) do(ctx context.Context, method, path string, body, out any) error {
|
|
url := c.baseURL + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("forge: marshal body: %w", err)
|
|
}
|
|
bodyReader = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return fmt.Errorf("forge: create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", "token "+c.token)
|
|
req.Header.Set("Accept", "application/json")
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("forge: request %s %s: %w", method, path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return c.parseError(resp, path)
|
|
}
|
|
|
|
if out != nil && resp.StatusCode != http.StatusNoContent {
|
|
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
|
return fmt.Errorf("forge: decode response: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) parseError(resp *http.Response, path string) error {
|
|
var errBody struct {
|
|
Message string `json:"message"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&errBody)
|
|
return &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Message: errBody.Message,
|
|
URL: path,
|
|
}
|
|
}
|