feat(aw): AW REST client with idempotent EnsureBucket
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
// Package aw is a thin ActivityWatch REST client: it creates buckets and reads
|
||||||
|
// and writes events. It holds no Keel concepts, so it stays a leaf package.
|
||||||
|
package aw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event is one ActivityWatch event. Duration is seconds; for Keel's discrete
|
||||||
|
// derived events it is 0.
|
||||||
|
type Event struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
Duration float64
|
||||||
|
Data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client talks to an aw-server over its /api/0 REST surface.
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
hc *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New builds a client for the given base URL (e.g. http://localhost:5600).
|
||||||
|
func New(baseURL string) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: strings.TrimRight(baseURL, "/"),
|
||||||
|
hc: &http.Client{Timeout: 5 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureBucket creates the bucket if absent. It is idempotent: an already-exists
|
||||||
|
// response (304) is success.
|
||||||
|
func (c *Client) EnsureBucket(ctx context.Context, id, eventType, client string) error {
|
||||||
|
body, _ := json.Marshal(map[string]string{
|
||||||
|
"client": client,
|
||||||
|
"type": eventType,
|
||||||
|
"hostname": hostname(),
|
||||||
|
})
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||||
|
c.baseURL+"/api/0/buckets/"+id, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := c.hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusNotModified:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("aw: create bucket %s: status %d", id, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostname() string {
|
||||||
|
if h, err := os.Hostname(); err == nil && h != "" {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
return "keel"
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package aw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnsureBucketPostsCreate(t *testing.T) {
|
||||||
|
var gotPath string
|
||||||
|
var gotBody map[string]string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
t.Errorf("method = %q, want POST", r.Method)
|
||||||
|
}
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
_ = json.Unmarshal(b, &gotBody)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := New(srv.URL)
|
||||||
|
if err := c.EnsureBucket(context.Background(), "keel.events", "keel.event", "keel"); err != nil {
|
||||||
|
t.Fatalf("EnsureBucket: %v", err)
|
||||||
|
}
|
||||||
|
if gotPath != "/api/0/buckets/keel.events" {
|
||||||
|
t.Fatalf("path = %q", gotPath)
|
||||||
|
}
|
||||||
|
if gotBody["type"] != "keel.event" || gotBody["client"] != "keel" {
|
||||||
|
t.Fatalf("body = %+v", gotBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureBucketTreatsExistsAsOK(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotModified) // AW returns 304 when the bucket exists
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
if err := New(srv.URL).EnsureBucket(context.Background(), "keel.events", "keel.event", "keel"); err != nil {
|
||||||
|
t.Fatalf("existing bucket should be OK, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureBucketErrorsOnServerError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
if err := New(srv.URL).EnsureBucket(context.Background(), "keel.events", "keel.event", "keel"); err == nil {
|
||||||
|
t.Fatal("expected error on 500")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user