From 427cc77fa3633aca7c4c3377fa26b0e70921a7b5 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Mon, 6 May 2024 00:18:22 +0200 Subject: [golang] bootstrapped a client in yet another language --- golang/pkg/api/agents.go | 14 +++++++ golang/pkg/api/api.go | 87 ++++++++++++++++++++++++++++++++++++++++ golang/pkg/api/client.go | 75 ++++++++++++++++++++++++++++++++++ golang/pkg/api/errors.go | 23 +++++++++++ golang/pkg/api/priority_queue.go | 44 ++++++++++++++++++++ golang/pkg/api/register.go | 20 +++++++++ 6 files changed, 263 insertions(+) create mode 100644 golang/pkg/api/agents.go create mode 100644 golang/pkg/api/api.go create mode 100644 golang/pkg/api/client.go create mode 100644 golang/pkg/api/errors.go create mode 100644 golang/pkg/api/priority_queue.go create mode 100644 golang/pkg/api/register.go (limited to 'golang/pkg/api') 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, + }) +} -- cgit v1.2.3