1
0
Fork 0

[golang] bootstrapped a client in yet another language

This commit is contained in:
Julien Dessaux 2024-05-06 00:18:22 +02:00
parent 5aac233c08
commit 427cc77fa3
Signed by: adyxax
GPG key ID: F92E51B86E07177E
12 changed files with 447 additions and 0 deletions

14
golang/pkg/api/agents.go Normal file
View file

@ -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)
}

87
golang/pkg/api/api.go Normal file
View file

@ -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
}

75
golang/pkg/api/client.go Normal file
View file

@ -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,
}
}
}
}
}

23
golang/pkg/api/errors.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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,
})
}