|
- package qcclient
-
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "log/slog"
- "net/http"
- "strings"
- "time"
- )
-
- type Client interface {
- Health(ctx context.Context) error
- ListAITemplates(ctx context.Context) ([]Template, error)
- GetTemplate(ctx context.Context, templateID int64) (*Template, error)
- GetTemplateSchema(ctx context.Context, templateID int64) (json.RawMessage, error)
- GenerateContent(ctx context.Context, req GenerateContentRequest) (GenerateContentData, json.RawMessage, error)
- CreateSite(ctx context.Context, req CreateSiteRequest) (*CreateSiteResponseData, json.RawMessage, error)
- GetJob(ctx context.Context, jobID int64) (*JobStatusData, json.RawMessage, error)
- GetEditorURL(ctx context.Context, siteID int64) (*SiteEditorLoginData, json.RawMessage, error)
- }
-
- type HTTPClient struct {
- baseURL string
- token string
- client *http.Client
- logger *slog.Logger
- }
-
- func New(baseURL, token string, timeout time.Duration, logger *slog.Logger) *HTTPClient {
- if timeout <= 0 {
- timeout = 10 * time.Second
- }
- return &HTTPClient{
- baseURL: strings.TrimRight(baseURL, "/"),
- token: token,
- client: &http.Client{Timeout: timeout},
- logger: logger,
- }
- }
-
- func (c *HTTPClient) Health(ctx context.Context) error {
- _, _, err := c.doJSON(ctx, http.MethodGet, "/health", nil)
- return err
- }
-
- func (c *HTTPClient) ListAITemplates(ctx context.Context) ([]Template, error) {
- body, _, err := c.doJSON(ctx, http.MethodGet, "/templates?type=ai", nil)
- if err != nil {
- return nil, err
- }
- var rsp APIResponse[[]Template]
- if err := json.Unmarshal(body, &rsp); err != nil {
- return nil, fmt.Errorf("decode templates response: %w", err)
- }
- return rsp.Data, nil
- }
-
- func (c *HTTPClient) GetTemplate(ctx context.Context, templateID int64) (*Template, error) {
- body, _, err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("/templates/%d", templateID), nil)
- if err != nil {
- return nil, err
- }
- var rsp APIResponse[Template]
- if err := json.Unmarshal(body, &rsp); err != nil {
- return nil, fmt.Errorf("decode template response: %w", err)
- }
- return &rsp.Data, nil
- }
-
- func (c *HTTPClient) GetTemplateSchema(ctx context.Context, templateID int64) (json.RawMessage, error) {
- body, _, err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("/templates/%d/schema", templateID), nil)
- if err != nil {
- return nil, err
- }
- return json.RawMessage(body), nil
- }
-
- func (c *HTTPClient) GenerateContent(ctx context.Context, req GenerateContentRequest) (GenerateContentData, json.RawMessage, error) {
- body, raw, err := c.doJSON(ctx, http.MethodPost, "/generate-content", req)
- if err != nil {
- return nil, nil, err
- }
- var rsp APIResponse[GenerateContentData]
- if err := json.Unmarshal(body, &rsp); err != nil {
- return nil, raw, fmt.Errorf("decode generate-content response: %w", err)
- }
- return rsp.Data, raw, nil
- }
-
- func (c *HTTPClient) CreateSite(ctx context.Context, req CreateSiteRequest) (*CreateSiteResponseData, json.RawMessage, error) {
- body, raw, err := c.doJSON(ctx, http.MethodPost, "/sites", req)
- if err != nil {
- return nil, nil, err
- }
- var rsp APIResponse[CreateSiteResponseData]
- if err := json.Unmarshal(body, &rsp); err != nil {
- return nil, raw, fmt.Errorf("decode create site response: %w", err)
- }
- return &rsp.Data, raw, nil
- }
-
- func (c *HTTPClient) GetJob(ctx context.Context, jobID int64) (*JobStatusData, json.RawMessage, error) {
- body, raw, err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("/jobs/%d", jobID), nil)
- if err != nil {
- return nil, nil, err
- }
- var rsp APIResponse[JobStatusData]
- if err := json.Unmarshal(body, &rsp); err != nil {
- return nil, raw, fmt.Errorf("decode job response: %w", err)
- }
- return &rsp.Data, raw, nil
- }
-
- func (c *HTTPClient) GetEditorURL(ctx context.Context, siteID int64) (*SiteEditorLoginData, json.RawMessage, error) {
- body, raw, err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("/sites/%d/editor-url", siteID), nil)
- if err != nil {
- return nil, nil, err
- }
- var rsp APIResponse[SiteEditorLoginData]
- if err := json.Unmarshal(body, &rsp); err != nil {
- return nil, raw, fmt.Errorf("decode editor-url response: %w", err)
- }
- return &rsp.Data, raw, nil
- }
-
- func (c *HTTPClient) doJSON(ctx context.Context, method, path string, payload any) ([]byte, json.RawMessage, error) {
- var body io.Reader
- if payload != nil {
- b, err := json.Marshal(payload)
- if err != nil {
- return nil, nil, fmt.Errorf("marshal request: %w", err)
- }
- body = bytes.NewReader(b)
- }
-
- req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
- if err != nil {
- return nil, nil, fmt.Errorf("build request: %w", err)
- }
- req.Header.Set("Authorization", "Bearer "+c.token)
- req.Header.Set("Accept", "application/json")
- if payload != nil {
- req.Header.Set("Content-Type", "application/json")
- }
-
- start := time.Now()
- res, err := c.client.Do(req)
- if err != nil {
- return nil, nil, fmt.Errorf("do request: %w", err)
- }
- defer res.Body.Close()
-
- respBody, err := io.ReadAll(res.Body)
- if err != nil {
- return nil, nil, fmt.Errorf("read response: %w", err)
- }
-
- c.logger.Info("qc request",
- "method", method,
- "path", path,
- "status", res.StatusCode,
- "duration_ms", time.Since(start).Milliseconds(),
- "auth", "Bearer [REDACTED]",
- )
-
- if res.StatusCode >= 400 {
- return nil, json.RawMessage(respBody), c.mapHTTPError(res.StatusCode, respBody)
- }
- return respBody, json.RawMessage(respBody), nil
- }
-
- func (c *HTTPClient) mapHTTPError(statusCode int, raw []byte) error {
- var apiErr APIError
- _ = json.Unmarshal(raw, &apiErr)
-
- err := &HTTPError{
- StatusCode: statusCode,
- APIError: &apiErr,
- RawBody: raw,
- }
- switch statusCode {
- case http.StatusBadRequest:
- return fmt.Errorf("%w: %w", ErrBadRequest, err)
- case http.StatusUnauthorized:
- return fmt.Errorf("%w: %w", ErrUnauthorized, err)
- case http.StatusNotFound:
- return fmt.Errorf("%w: %w", ErrNotFound, err)
- default:
- return err
- }
- }
|