179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const maxResponseBytes = 64 << 20
|
|
|
|
type Client struct {
|
|
cfg Config
|
|
http *http.Client
|
|
token string
|
|
apiKey string
|
|
timeout time.Duration
|
|
}
|
|
|
|
func newClient(cfg Config, token, apiKey string, timeout time.Duration) *Client {
|
|
if timeout <= 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
return &Client{
|
|
cfg: cfg,
|
|
http: &http.Client{Timeout: timeout},
|
|
token: token,
|
|
apiKey: apiKey,
|
|
timeout: timeout,
|
|
}
|
|
}
|
|
|
|
func (c *Client) cmsURL(endpoint string) string {
|
|
return joinURL(effectiveBaseURL(c.cfg), "/api", endpoint)
|
|
}
|
|
|
|
func (c *Client) finderURL(endpoint string) string {
|
|
return joinURL(effectiveFinderURL(c.cfg), "/api", endpoint)
|
|
}
|
|
|
|
// finderAdminURL is deliberately based on BaseURL, never FinderURL. The
|
|
// backoffice owns the writeradmin surface and is the only supported write path.
|
|
func (c *Client) finderAdminURL(endpoint string) string {
|
|
return joinURL(effectiveBaseURL(c.cfg), "/catalog/admin", endpoint)
|
|
}
|
|
|
|
func joinURL(base string, parts ...string) string {
|
|
u, err := url.Parse(strings.TrimRight(base, "/"))
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
return strings.TrimRight(base, "/") + "/" + strings.TrimLeft(path.Join(parts...), "/")
|
|
}
|
|
joined := strings.TrimRight(u.Path, "/")
|
|
for _, part := range parts {
|
|
joined = path.Join(joined, "/"+strings.Trim(part, "/"))
|
|
}
|
|
u.Path = joined
|
|
return u.String()
|
|
}
|
|
|
|
func escapePathSegment(value string) string {
|
|
return url.PathEscape(value)
|
|
}
|
|
|
|
func (c *Client) request(ctx context.Context, method, endpoint string, body []byte, contentType string) ([]byte, int, error) {
|
|
var reader io.Reader
|
|
if body != nil {
|
|
reader = bytes.NewReader(body)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, endpoint, reader)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
req.Header.Set("Accept", "application/json, application/jsonl+json, text/plain")
|
|
if body != nil {
|
|
if contentType == "" {
|
|
contentType = "application/json"
|
|
}
|
|
req.Header.Set("Content-Type", contentType)
|
|
}
|
|
if c.apiKey != "" {
|
|
req.Header.Set("X-API-Key", c.apiKey)
|
|
} else if c.token != "" {
|
|
tokenType := c.cfg.TokenType
|
|
if tokenType == "" {
|
|
tokenType = "Bearer"
|
|
}
|
|
req.Header.Set("Authorization", tokenType+" "+c.token)
|
|
}
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes+1))
|
|
if err != nil {
|
|
return nil, resp.StatusCode, err
|
|
}
|
|
if len(responseBody) > maxResponseBytes {
|
|
return nil, resp.StatusCode, fmt.Errorf("response exceeds %d MiB", maxResponseBytes/(1<<20))
|
|
}
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return responseBody, resp.StatusCode, &HTTPError{StatusCode: resp.StatusCode, Body: responseBody}
|
|
}
|
|
return responseBody, resp.StatusCode, nil
|
|
}
|
|
|
|
type HTTPError struct {
|
|
StatusCode int
|
|
Body []byte
|
|
}
|
|
|
|
func (e *HTTPError) Error() string {
|
|
body := strings.TrimSpace(string(e.Body))
|
|
if body == "" {
|
|
return fmt.Sprintf("HTTP %d", e.StatusCode)
|
|
}
|
|
var payload struct {
|
|
Error string `json:"error"`
|
|
ErrorDescription string `json:"error_description"`
|
|
Message string `json:"message"`
|
|
}
|
|
if json.Unmarshal(e.Body, &payload) == nil {
|
|
for _, msg := range []string{payload.ErrorDescription, payload.Message, payload.Error} {
|
|
if msg != "" {
|
|
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, msg)
|
|
}
|
|
}
|
|
}
|
|
if len(body) > 500 {
|
|
body = body[:500] + "…"
|
|
}
|
|
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, body)
|
|
}
|
|
|
|
func (c *Client) get(ctx context.Context, endpoint string) ([]byte, error) {
|
|
body, _, err := c.request(ctx, http.MethodGet, endpoint, nil, "")
|
|
return body, err
|
|
}
|
|
|
|
func (c *Client) postJSON(ctx context.Context, endpoint string, value any) ([]byte, error) {
|
|
body, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.postBody(ctx, endpoint, body, "application/json")
|
|
}
|
|
|
|
func (c *Client) postBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
|
|
response, _, err := c.request(ctx, http.MethodPost, endpoint, body, contentType)
|
|
return response, err
|
|
}
|
|
|
|
func (c *Client) putBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
|
|
response, _, err := c.request(ctx, http.MethodPut, endpoint, body, contentType)
|
|
return response, err
|
|
}
|
|
|
|
func (c *Client) patchBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
|
|
response, _, err := c.request(ctx, http.MethodPatch, endpoint, body, contentType)
|
|
return response, err
|
|
}
|
|
|
|
func (c *Client) delete(ctx context.Context, endpoint string) ([]byte, error) {
|
|
response, _, err := c.request(ctx, http.MethodDelete, endpoint, nil, "")
|
|
return response, err
|
|
}
|
|
|
|
func (c *Client) deleteBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
|
|
response, _, err := c.request(ctx, http.MethodDelete, endpoint, body, contentType)
|
|
return response, err
|
|
}
|