mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-01 17:38:15 +00:00
Refactor cursor code to DRY it out
This commit is contained in:
+3
-13
@@ -1,8 +1,6 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -42,22 +40,14 @@ type logCursor struct {
|
||||
|
||||
// encodeLogCursor packs a session position into an opaque, URL-safe token.
|
||||
func encodeLogCursor(startedAt time.Time, id int64) string {
|
||||
b, _ := json.Marshal(logCursor{StartedAt: startedAt, ID: id})
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
return web.EncodeCursor(logCursor{StartedAt: startedAt, ID: id})
|
||||
}
|
||||
|
||||
// decodeLogCursor reverses encodeLogCursor. A missing or malformed token decodes
|
||||
// to nil — the first page — so a tampered URL just resets paging.
|
||||
func decodeLogCursor(tok string) *store.CommandLogCursor {
|
||||
if tok == "" {
|
||||
return nil
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(tok)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var c logCursor
|
||||
if json.Unmarshal(b, &c) != nil {
|
||||
c, ok := web.DecodeCursor[logCursor](tok)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &store.CommandLogCursor{StartedAt: c.StartedAt, ID: c.ID}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package marketing
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -178,24 +176,13 @@ type repCursor struct {
|
||||
}
|
||||
|
||||
func encodeRepCursor(name string, id int64) string {
|
||||
b, _ := json.Marshal(repCursor{Name: name, ID: id})
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
return web.EncodeCursor(repCursor{Name: name, ID: id})
|
||||
}
|
||||
|
||||
// decodeRepCursor reverses encodeRepCursor; a missing or malformed cursor decodes
|
||||
// to the first page ("", 0).
|
||||
func decodeRepCursor(tok string) (name string, id int64) {
|
||||
if tok == "" {
|
||||
return "", 0
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(tok)
|
||||
if err != nil {
|
||||
return "", 0
|
||||
}
|
||||
var c repCursor
|
||||
if json.Unmarshal(b, &c) != nil {
|
||||
return "", 0
|
||||
}
|
||||
c, _ := web.DecodeCursor[repCursor](tok)
|
||||
return c.Name, c.ID
|
||||
}
|
||||
|
||||
@@ -229,23 +216,11 @@ func nextOrgCursor(sort store.OrgSort, query string, last store.OrgSummary) orgC
|
||||
|
||||
// encodeOrgCursor packs a directory position into an opaque, URL-safe token.
|
||||
func encodeOrgCursor(c orgCursor) string {
|
||||
b, _ := json.Marshal(c)
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
return web.EncodeCursor(c)
|
||||
}
|
||||
|
||||
// decodeOrgCursor reverses encodeOrgCursor. A missing or malformed cursor decodes
|
||||
// to ok=false, i.e. the first page.
|
||||
func decodeOrgCursor(tok string) (orgCursor, bool) {
|
||||
if tok == "" {
|
||||
return orgCursor{}, false
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(tok)
|
||||
if err != nil {
|
||||
return orgCursor{}, false
|
||||
}
|
||||
var c orgCursor
|
||||
if json.Unmarshal(b, &c) != nil {
|
||||
return orgCursor{}, false
|
||||
}
|
||||
return c, true
|
||||
return web.DecodeCursor[orgCursor](tok)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// EncodeCursor packs a keyset position into an opaque, URL-safe token. Callers
|
||||
// pass a small named struct with short JSON tags (the wire form is not stable
|
||||
// API — it only round-trips through the client's ?cursor/?before parameter).
|
||||
func EncodeCursor[T any](pos T) string {
|
||||
b, _ := json.Marshal(pos)
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// DecodeCursor reverses EncodeCursor. A missing or malformed token decodes to the
|
||||
// zero value with ok=false, so a tampered or stale URL just resets paging to the
|
||||
// first page rather than erroring.
|
||||
func DecodeCursor[T any](tok string) (pos T, ok bool) {
|
||||
if tok == "" {
|
||||
return pos, false
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(tok)
|
||||
if err != nil {
|
||||
return pos, false
|
||||
}
|
||||
if json.Unmarshal(b, &pos) != nil {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
return pos, true
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testCursor struct {
|
||||
Name string `json:"n"`
|
||||
ID int64 `json:"i"`
|
||||
At time.Time `json:"t"`
|
||||
}
|
||||
|
||||
func TestCursorRoundTrip(t *testing.T) {
|
||||
want := testCursor{Name: "repeater-alpha", ID: 42, At: time.Unix(1_700_000_000, 0).UTC()}
|
||||
got, ok := DecodeCursor[testCursor](EncodeCursor(want))
|
||||
if !ok {
|
||||
t.Fatal("DecodeCursor: ok=false for a token we just encoded")
|
||||
}
|
||||
if got.Name != want.Name || got.ID != want.ID || !got.At.Equal(want.At) {
|
||||
t.Fatalf("round trip mismatch: got %+v want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCursorTokenIsURLSafe(t *testing.T) {
|
||||
// RawURLEncoding must not emit '+', '/', or '=' — the token rides in a query
|
||||
// parameter unescaped.
|
||||
tok := EncodeCursor(testCursor{Name: "a/b+c==", ID: 1})
|
||||
for _, r := range tok {
|
||||
switch r {
|
||||
case '+', '/', '=':
|
||||
t.Fatalf("token %q contains non-URL-safe rune %q", tok, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeCursorRejectsBadInput(t *testing.T) {
|
||||
// A missing, malformed-base64, or malformed-JSON token must reset to the zero
|
||||
// value with ok=false so a tampered URL just pages from the start.
|
||||
cases := map[string]string{
|
||||
"empty": "",
|
||||
"not base64": "!!!not base64!!!",
|
||||
"base64 non-json": EncodeCursor([]byte("plain string, not this struct's JSON")),
|
||||
}
|
||||
for name, tok := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, ok := DecodeCursor[testCursor](tok)
|
||||
if ok {
|
||||
t.Fatalf("ok=true for %s input %q", name, tok)
|
||||
}
|
||||
if got != (testCursor{}) {
|
||||
t.Fatalf("non-zero cursor %+v for %s input", got, name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user