internal/control/cursor.go

d4aaf96d88e92486a458b150375b2386ac72fe34
gitbay/internal/control/cursor.go history · blame · raw

127 lines · 3586 bytes

  1package control
  2
  3import (
  4	"encoding/base64"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"reflect"
  9	"strconv"
 10	"strings"
 11
 12	"gitbay.org/gitbay/internal/protocol"
 13)
 14
 15// Cursor pagination. A cursor is opaque to clients: base64url of
 16// "<kind>:<key>", where key is the sort key of the last row of the
 17// previous page. The kind keeps a cursor minted by one command from
 18// being fed to another.
 19
 20const maxPageLimit = 200
 21
 22func encodeCursor(kind, key string) string {
 23	return base64.RawURLEncoding.EncodeToString([]byte(kind + ":" + key))
 24}
 25
 26func decodeCursor(kind, cursor string) (string, error) {
 27	raw, err := base64.RawURLEncoding.DecodeString(cursor)
 28	if err != nil {
 29		return "", errors.New("bad cursor")
 30	}
 31	k, key, ok := strings.Cut(string(raw), ":")
 32	if !ok || k != kind || key == "" {
 33		return "", errors.New("bad cursor")
 34	}
 35	return key, nil
 36}
 37
 38// page carries parsed --limit/--cursor flags. active marks that either
 39// flag was given: only then does the output switch to the paged shape.
 40type page struct {
 41	limit  int
 42	key    string // decoded cursor key, "" means from the start
 43	active bool
 44}
 45
 46// queryLimit is what the store is asked for: one row beyond the page, so
 47// the presence of a following page is known without a second query.
 48func (p page) queryLimit() int {
 49	if p.limit == 0 {
 50		return 0
 51	}
 52	return p.limit + 1
 53}
 54
 55// keyInt returns the cursor key as a number; parsePageFlags has already
 56// validated it for numeric kinds.
 57func (p page) keyInt() int64 {
 58	n, _ := strconv.ParseInt(p.key, 10, 64)
 59	return n
 60}
 61
 62// parsePageFlags strips --limit and --cursor from args. kind names the
 63// cursor namespace; numeric declares the sort key an integer.
 64func parsePageFlags(c *Ctx, args []string, kind string, numeric bool) (rest []string, p page, code int) {
 65	for i := 0; i < len(args); i++ {
 66		switch args[i] {
 67		case "--limit":
 68			if i+1 >= len(args) {
 69				return nil, p, c.fail(protocol.ExitUsage, "--limit requires a value")
 70			}
 71			n, err := strconv.Atoi(args[i+1])
 72			if err != nil || n < 1 || n > maxPageLimit {
 73				return nil, p, c.fail(protocol.ExitUsage, "--limit must be 1 to %d", maxPageLimit)
 74			}
 75			p.limit, p.active = n, true
 76			i++
 77		case "--cursor":
 78			if i+1 >= len(args) {
 79				return nil, p, c.fail(protocol.ExitUsage, "--cursor requires a value")
 80			}
 81			key, err := decodeCursor(kind, args[i+1])
 82			if err == nil && numeric {
 83				_, err = strconv.ParseInt(key, 10, 64)
 84			}
 85			if err != nil {
 86				return nil, p, c.fail(protocol.ExitUsage, "bad cursor")
 87			}
 88			p.key, p.active = key, true
 89			i++
 90		default:
 91			rest = append(rest, args[i])
 92		}
 93	}
 94	return rest, p, -1
 95}
 96
 97// trimPage cuts the probe row and derives the next cursor from the last
 98// row kept.
 99func trimPage[T any](p page, items []T, kind string, key func(T) string) ([]T, string) {
100	if p.limit == 0 || len(items) <= p.limit {
101		return items, ""
102	}
103	items = items[:p.limit]
104	return items, encodeCursor(kind, key(items[len(items)-1]))
105}
106
107// emitPage renders a list result. Without pagination flags the shape is
108// the bare array it has always been; with them the array moves under
109// "items" with the next cursor alongside.
110func (c *Ctx) emitPage(p page, items any, next string, plain func(w io.Writer)) int {
111	if !p.active {
112		return c.emit(items, plain)
113	}
114	if v := reflect.ValueOf(items); v.Kind() == reflect.Slice && v.IsNil() {
115		items = reflect.MakeSlice(v.Type(), 0, 0).Interface()
116	}
117	type out struct {
118		Items any    `json:"items"`
119		Next  string `json:"next,omitempty"`
120	}
121	return c.emit(out{items, next}, func(w io.Writer) {
122		plain(w)
123		if next != "" {
124			fmt.Fprintf(w, "next\t%s\n", next)
125		}
126	})
127}