1package hyper
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "net/url"
10 "strings"
11 "time"
12
13 "github.com/google/uuid"
14 fantasy "charm.land/fantasy"
15)
16
17// Client is a minimal client for the Hyper API.
18type Client struct {
19 BaseURL *url.URL
20 APIKey string
21 HTTPClient *http.Client
22}
23
24// New creates a new Hyper client.
25func New(base string, apiKey string) (*Client, error) {
26 u, err := url.Parse(strings.TrimRight(base, "/"))
27 if err != nil {
28 return nil, fmt.Errorf("parse base url: %w", err)
29 }
30 return &Client{
31 BaseURL: u,
32 APIKey: apiKey,
33 HTTPClient: &http.Client{Timeout: 30 * time.Second},
34 }, nil
35}
36
37// Project mirrors the JSON returned by Hyper.
38type Project struct {
39 ID uuid.UUID `json:"id"`
40 Name string `json:"name"`
41 Description string `json:"description"`
42 OrganizationID uuid.UUID `json:"organization_id"`
43 UserID uuid.UUID `json:"user_id"`
44 Archived bool `json:"archived"`
45 Identifiers []string `json:"identifiers"`
46 CreatedAt time.Time `json:"created_at"`
47 UpdatedAt time.Time `json:"updated_at"`
48}
49
50// CreateProject creates a project.
51func (c *Client) CreateProject(ctx context.Context, name, description string, organizationID uuid.UUID, identifiers []string) (Project, error) {
52 var p Project
53 body := map[string]any{
54 "name": name,
55 "description": description,
56 "organization_id": organizationID,
57 }
58 if len(identifiers) > 0 {
59 body["identifiers"] = identifiers
60 }
61 bts, _ := json.Marshal(body)
62 endpoint := c.BaseURL.JoinPath("api/v1", "projects").String()
63 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(bts))
64 if err != nil {
65 return p, err
66 }
67 c.addAuth(req)
68 req.Header.Set("Content-Type", "application/json")
69 resp, err := c.http().Do(req)
70 if err != nil {
71 return p, err
72 }
73 defer resp.Body.Close()
74 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
75 return p, fmt.Errorf("create project: http %d", resp.StatusCode)
76 }
77 if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
78 return p, err
79 }
80 return p, nil
81}
82
83// ListProjects lists projects for the authenticated user.
84// If identifiers is not empty, projects that match ANY identifier are returned.
85func (c *Client) ListProjects(ctx context.Context, identifiers []string) ([]Project, error) {
86 endpoint := c.BaseURL.JoinPath("api/v1", "projects")
87 q := endpoint.Query()
88 if len(identifiers) > 0 {
89 q.Set("identifiers", strings.Join(identifiers, ","))
90 endpoint.RawQuery = q.Encode()
91 }
92 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
93 if err != nil {
94 return nil, err
95 }
96 c.addAuth(req)
97 resp, err := c.http().Do(req)
98 if err != nil {
99 return nil, err
100 }
101 defer resp.Body.Close()
102 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
103 return nil, fmt.Errorf("list projects: http %d", resp.StatusCode)
104 }
105 var ps []Project
106 if err := json.NewDecoder(resp.Body).Decode(&ps); err != nil {
107 return nil, err
108 }
109 return ps, nil
110}
111
112// Memorize sends messages to be memorized for a given project and echoes them back.
113func (c *Client) Memorize(ctx context.Context, projectID uuid.UUID, msgs []fantasy.Message) ([]fantasy.Message, error) {
114 bts, _ := json.Marshal(msgs)
115 endpoint := c.BaseURL.JoinPath("api/v1", "projects", projectID.String(), "memorize").String()
116 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(bts))
117 if err != nil {
118 return nil, err
119 }
120 c.addAuth(req)
121 req.Header.Set("Content-Type", "application/json")
122 resp, err := c.http().Do(req)
123 if err != nil {
124 return nil, err
125 }
126 defer resp.Body.Close()
127 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
128 return nil, fmt.Errorf("memorize: http %d", resp.StatusCode)
129 }
130 var echoed []fantasy.Message
131 if err := json.NewDecoder(resp.Body).Decode(&echoed); err != nil {
132 return nil, err
133 }
134 return echoed, nil
135}
136
137func (c *Client) http() *http.Client {
138 if c.HTTPClient != nil {
139 return c.HTTPClient
140 }
141 return http.DefaultClient
142}
143
144func (c *Client) addAuth(req *http.Request) {
145 if c.APIKey != "" {
146 req.Header.Set("Authorization", "Bearer "+c.APIKey)
147 }
148}