2023-12-21 06:28:53 +00:00
|
|
|
package domain
|
|
|
|
|
|
|
|
import (
|
2024-02-06 07:11:05 +00:00
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
2024-02-19 07:17:38 +00:00
|
|
|
"fmt"
|
2023-12-21 06:28:53 +00:00
|
|
|
"net/url"
|
2024-02-19 07:17:38 +00:00
|
|
|
"strconv"
|
2024-02-06 07:11:05 +00:00
|
|
|
"strings"
|
2024-02-19 07:17:38 +00:00
|
|
|
"time"
|
2023-12-21 06:28:53 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func parseURL(rawURL string) (*url.URL, error) {
|
|
|
|
u, err := url.ParseRequestURI(rawURL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, InvalidURLError{
|
|
|
|
URL: rawURL,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return u, nil
|
|
|
|
}
|
2024-02-06 07:11:05 +00:00
|
|
|
|
|
|
|
const (
|
|
|
|
cursorSeparator = ","
|
|
|
|
cursorKeyValueSeparator = "="
|
|
|
|
)
|
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
type keyValuePair struct {
|
|
|
|
key string
|
|
|
|
value any
|
|
|
|
}
|
|
|
|
|
2024-03-05 06:09:21 +00:00
|
|
|
func (kvp keyValuePair) valueToString() string {
|
2024-02-19 07:17:38 +00:00
|
|
|
switch v := kvp.value.(type) {
|
|
|
|
case string:
|
|
|
|
return v
|
|
|
|
case int:
|
|
|
|
return strconv.Itoa(v)
|
|
|
|
case bool:
|
|
|
|
return strconv.FormatBool(v)
|
|
|
|
case float64:
|
|
|
|
return strconv.FormatFloat(v, 'g', -1, 64)
|
|
|
|
case time.Time:
|
|
|
|
return v.Format(time.RFC3339)
|
|
|
|
case fmt.Stringer:
|
|
|
|
return v.String()
|
|
|
|
default:
|
|
|
|
return fmt.Sprintf("%v", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func encodeCursor(kvps []keyValuePair) string {
|
|
|
|
if len(kvps) == 0 {
|
2024-02-06 07:11:05 +00:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
values := make([]string, len(kvps))
|
|
|
|
n := len(cursorSeparator) * (len(kvps) - 1)
|
|
|
|
|
|
|
|
for i, kvp := range kvps {
|
2024-03-05 06:09:21 +00:00
|
|
|
value := kvp.valueToString()
|
2024-02-19 07:17:38 +00:00
|
|
|
n += len(kvp.key) + len(value) + len(cursorKeyValueSeparator)
|
|
|
|
values[i] = value
|
2024-02-06 07:11:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
b.Grow(n)
|
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
for i, kvp := range kvps {
|
2024-02-06 07:11:05 +00:00
|
|
|
if b.Len() > 0 {
|
|
|
|
_, _ = b.WriteString(cursorSeparator)
|
|
|
|
}
|
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
_, _ = b.WriteString(kvp.key)
|
2024-02-06 07:11:05 +00:00
|
|
|
_, _ = b.WriteString(cursorKeyValueSeparator)
|
2024-02-19 07:17:38 +00:00
|
|
|
_, _ = b.WriteString(values[i])
|
2024-02-06 07:11:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return base64.StdEncoding.EncodeToString(b.Bytes())
|
|
|
|
}
|
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
type cursorKeyNotFoundError struct {
|
|
|
|
key string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e cursorKeyNotFoundError) Error() string {
|
|
|
|
return "key " + e.key + " not found"
|
|
|
|
}
|
|
|
|
|
|
|
|
type decodedCursor map[string]string
|
|
|
|
|
2024-02-07 07:17:47 +00:00
|
|
|
const (
|
|
|
|
encodedCursorMinLength = 1
|
|
|
|
encodedCursorMaxLength = 1000
|
|
|
|
)
|
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
func decodeCursor(s string) (decodedCursor, error) {
|
2024-02-07 07:17:47 +00:00
|
|
|
if err := validateStringLen(s, encodedCursorMinLength, encodedCursorMaxLength); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-02-06 07:11:05 +00:00
|
|
|
decodedBytes, err := base64.StdEncoding.DecodeString(s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ErrInvalidCursor
|
|
|
|
}
|
|
|
|
decoded := string(decodedBytes)
|
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
kvps := strings.Split(decoded, cursorSeparator)
|
2024-02-06 07:11:05 +00:00
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
m := make(decodedCursor, len(kvps))
|
2024-02-06 07:11:05 +00:00
|
|
|
|
2024-02-19 07:17:38 +00:00
|
|
|
for _, kvp := range kvps {
|
|
|
|
k, v, found := strings.Cut(kvp, cursorKeyValueSeparator)
|
2024-02-06 07:11:05 +00:00
|
|
|
if !found {
|
|
|
|
return nil, ErrInvalidCursor
|
|
|
|
}
|
|
|
|
m[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
return m, nil
|
|
|
|
}
|
2024-02-19 07:17:38 +00:00
|
|
|
|
|
|
|
func (c decodedCursor) string(k string) (string, error) {
|
|
|
|
val, ok := c[k]
|
|
|
|
if !ok {
|
|
|
|
return "", cursorKeyNotFoundError{k}
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c decodedCursor) int(k string) (int, error) {
|
|
|
|
valStr, ok := c[k]
|
|
|
|
if !ok {
|
|
|
|
return 0, cursorKeyNotFoundError{k}
|
|
|
|
}
|
|
|
|
|
|
|
|
val, err := strconv.Atoi(valStr)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c decodedCursor) float64(k string) (float64, error) {
|
|
|
|
valStr, ok := c[k]
|
|
|
|
if !ok {
|
|
|
|
return 0, cursorKeyNotFoundError{k}
|
|
|
|
}
|
|
|
|
|
|
|
|
val, err := strconv.ParseFloat(valStr, 64)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c decodedCursor) bool(k string) (bool, error) {
|
|
|
|
valStr, ok := c[k]
|
|
|
|
if !ok {
|
|
|
|
return false, cursorKeyNotFoundError{k}
|
|
|
|
}
|
|
|
|
|
|
|
|
val, err := strconv.ParseBool(valStr)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c decodedCursor) time(k string) (time.Time, error) {
|
|
|
|
valStr, ok := c[k]
|
|
|
|
if !ok {
|
|
|
|
return time.Time{}, cursorKeyNotFoundError{k}
|
|
|
|
}
|
|
|
|
|
|
|
|
val, err := time.Parse(time.RFC3339, valStr)
|
|
|
|
if err != nil {
|
|
|
|
return time.Time{}, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, nil
|
|
|
|
}
|