summaryrefslogtreecommitdiff
path: root/golang/pkg/api
diff options
context:
space:
mode:
Diffstat (limited to 'golang/pkg/api')
-rw-r--r--golang/pkg/api/agents.go14
-rw-r--r--golang/pkg/api/api.go87
-rw-r--r--golang/pkg/api/client.go75
-rw-r--r--golang/pkg/api/errors.go23
-rw-r--r--golang/pkg/api/priority_queue.go44
-rw-r--r--golang/pkg/api/register.go20
6 files changed, 263 insertions, 0 deletions
diff --git a/golang/pkg/api/agents.go b/golang/pkg/api/agents.go
new file mode 100644
index 0000000..aa8107a
--- /dev/null
+++ b/golang/pkg/api/agents.go
@@ -0,0 +1,14 @@
+package api
+
+type AgentMessage struct {
+ AccountID string `json:"accountId"`
+ Credits int `json:"credits"`
+ Headquarters string `json:"headquarters"`
+ ShipCount int `json:"shipCount"`
+ StartingFaction string `json:"startingFaction"`
+ Symbol string `json:"symbol"`
+}
+
+func (c *Client) MyAgent() (APIMessage[AgentMessage, any], error) {
+ return Send[AgentMessage](c, "GET", "/my/agent", nil)
+}
diff --git a/golang/pkg/api/api.go b/golang/pkg/api/api.go
new file mode 100644
index 0000000..662bb87
--- /dev/null
+++ b/golang/pkg/api/api.go
@@ -0,0 +1,87 @@
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "log/slog"
+ "net/http"
+ "time"
+)
+
+type Error[T any] struct {
+ Code int `json:"code"`
+ Data T `json:"data"`
+ Message string `json:"message"`
+}
+
+type APIMessage[T any, E any] struct {
+ Data T `json:"data"`
+ Error Error[E] `json:"error"`
+ //meta
+}
+
+type Response struct {
+ Response []byte
+ Err error
+}
+
+func Send[T any](c *Client, method, path string, payload any) (message APIMessage[T, any], err error) {
+ resp := make(chan *Response)
+ c.channel <- &Request{
+ method: method,
+ path: path,
+ payload: payload,
+ priority: 10,
+ resp: resp,
+ }
+ res := <-resp
+ if res.Err != nil {
+ return message, res.Err
+ }
+ err = json.Unmarshal(res.Response, &message)
+ return message, err
+}
+
+func (c *Client) sendOne(method, path string, payload any) (body []byte, err error) {
+ slog.Debug("Request", "method", method, "path", path, "payload", payload)
+ var req *http.Request
+ if payload != nil {
+ body, err = json.Marshal(payload)
+ if err == nil {
+ req, err = http.NewRequest(method, c.baseURL+path, bytes.NewBuffer(body))
+ } else {
+ return nil, err
+ }
+ } else {
+ req, err = http.NewRequest(method, c.baseURL+path, nil)
+ }
+ if err != nil {
+ return nil, err
+ }
+ req.Header = *c.headers
+ req = req.WithContext(c.ctx)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ slog.Error("sendOne Do", "method", method, "path", path, "error", err)
+ return nil, err
+ }
+ defer func() {
+ if e := resp.Body.Close(); err == nil {
+ err = e
+ }
+ }()
+ if body, err = io.ReadAll(resp.Body); err != nil {
+ slog.Error("sendOne ReadAll", "method", method, "path", path, "error", err)
+ return nil, err
+ }
+ slog.Debug("Response", "body", string(body))
+ switch resp.StatusCode {
+ case 429:
+ e := decode429(body)
+ time.Sleep(time.Duration(e.Error.Data.RetryAfter * float64(time.Second)))
+ return c.sendOne(method, path, payload)
+ }
+ return body, nil
+}
diff --git a/golang/pkg/api/client.go b/golang/pkg/api/client.go
new file mode 100644
index 0000000..70f3e68
--- /dev/null
+++ b/golang/pkg/api/client.go
@@ -0,0 +1,75 @@
+package api
+
+import (
+ "container/heap"
+ "context"
+ "net/http"
+ "time"
+)
+
+type Client struct {
+ baseURL string
+ channel chan *Request
+ ctx context.Context
+ headers *http.Header
+ httpClient *http.Client
+ pq *PriorityQueue
+}
+
+func NewClient(ctx context.Context) *Client {
+ pq := make(PriorityQueue, 0)
+ heap.Init(&pq)
+ client := &Client{
+ baseURL: "https://api.spacetraders.io/v2",
+ channel: make(chan *Request),
+ ctx: ctx,
+ headers: &http.Header{
+ "Content-Type": {"application/json"},
+ },
+ httpClient: &http.Client{
+ Timeout: time.Minute,
+ },
+ pq: &pq,
+ }
+ go queueProcessor(client)
+ return client
+}
+
+func (c *Client) Close() {
+ close(c.channel)
+}
+
+func (c *Client) SetToken(token string) {
+ c.headers.Set("Authorization", "Bearer "+token)
+}
+
+func queueProcessor(client *Client) {
+ var ok bool
+ for {
+ // The queue is empty so we do this blocking call
+ req := <-client.channel
+ heap.Push(client.pq, req)
+ // we enqueue all values read from the channel and process the queue's
+ // contents until empty. We keep reading the channel as long as this
+ // emptying goes on
+ for {
+ select {
+ case req = <-client.channel:
+ heap.Push(client.pq, req)
+ default:
+ if client.pq.Len() == 0 {
+ break
+ }
+ // we process one
+ if req, ok = heap.Pop(client.pq).(*Request); !ok {
+ panic("queueProcessor got something other than a Request on its channel")
+ }
+ response, err := client.sendOne(req.method, req.path, req.payload)
+ req.resp <- &Response{
+ Response: response,
+ Err: err,
+ }
+ }
+ }
+ }
+}
diff --git a/golang/pkg/api/errors.go b/golang/pkg/api/errors.go
new file mode 100644
index 0000000..d39a205
--- /dev/null
+++ b/golang/pkg/api/errors.go
@@ -0,0 +1,23 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+)
+
+type RateLimitError struct {
+ LimitType string `json:"type"`
+ RetryAfter float64 `json:"retryAfter"`
+ LimitBurst int `json:"limitBurst"`
+ LimitPerSecond int `json:"limitPerSecond"`
+ Remaining int `json:"remaining"`
+ Reset time.Time `json:"reset"`
+}
+
+func decode429(msg []byte) (e APIMessage[any, RateLimitError]) {
+ if err := json.Unmarshal(msg, &e); err != nil {
+ panic(fmt.Sprintf("Failed to decode419: %+v", err))
+ }
+ return e
+}
diff --git a/golang/pkg/api/priority_queue.go b/golang/pkg/api/priority_queue.go
new file mode 100644
index 0000000..077c8f7
--- /dev/null
+++ b/golang/pkg/api/priority_queue.go
@@ -0,0 +1,44 @@
+package api
+
+type Request struct {
+ index int
+ priority int
+
+ method string
+ path string
+ payload any
+ resp chan *Response
+}
+
+type PriorityQueue []*Request
+
+func (pq PriorityQueue) Len() int {
+ return len(pq)
+}
+
+func (pq PriorityQueue) Less(i, j int) bool {
+ return pq[i].priority < pq[j].priority
+}
+
+func (pq PriorityQueue) Swap(i, j int) {
+ pq[i], pq[j] = pq[j], pq[i]
+ pq[i].index = i
+ pq[j].index = j
+}
+
+func (pq *PriorityQueue) Push(x any) {
+ n := len(*pq)
+ item := x.(*Request)
+ item.index = n
+ *pq = append(*pq, item)
+}
+
+func (pq *PriorityQueue) Pop() any {
+ old := *pq
+ n := len(old)
+ item := old[n-1]
+ old[n-1] = nil // avoid memory leak
+ item.index = -1 // for safety
+ *pq = old[0 : n-1]
+ return item
+}
diff --git a/golang/pkg/api/register.go b/golang/pkg/api/register.go
new file mode 100644
index 0000000..4f95e2f
--- /dev/null
+++ b/golang/pkg/api/register.go
@@ -0,0 +1,20 @@
+package api
+
+type RegisterMessage struct {
+ //agent
+ //contract
+ //faction
+ //ship
+ Token string `json:"token"`
+}
+
+func (c *Client) Register(faction, symbol string) (APIMessage[RegisterMessage, any], error) {
+ type RegisterRequest struct {
+ Faction string `json:"faction"`
+ Symbol string `json:"symbol"`
+ }
+ return Send[RegisterMessage](c, "POST", "/register", RegisterRequest{
+ Faction: faction,
+ Symbol: symbol,
+ })
+}