core/internal/domain/utils.go

212 lines
3.9 KiB
Go

package domain
import (
"bytes"
"encoding/base64"
"fmt"
"net/url"
"slices"
"strconv"
"strings"
"time"
)
func parseURL(rawURL string) (*url.URL, error) {
u, err := url.ParseRequestURI(rawURL)
if err != nil {
return nil, InvalidURLError{
URL: rawURL,
}
}
return u, nil
}
const (
cursorSeparator = ","
cursorKeyValueSeparator = "="
)
type keyValuePair struct {
key string
value any
}
func (kvp keyValuePair) valueToString() string {
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 {
return ""
}
values := make([]string, len(kvps))
n := len(cursorSeparator) * (len(kvps) - 1)
for i, kvp := range kvps {
value := kvp.valueToString()
n += len(kvp.key) + len(value) + len(cursorKeyValueSeparator)
values[i] = value
}
var b bytes.Buffer
b.Grow(n)
for i, kvp := range kvps {
if b.Len() > 0 {
b.WriteString(cursorSeparator)
}
b.WriteString(kvp.key)
b.WriteString(cursorKeyValueSeparator)
b.WriteString(values[i])
}
return base64.StdEncoding.EncodeToString(b.Bytes())
}
type cursorKeyNotFoundError struct {
key string
}
func (e cursorKeyNotFoundError) Error() string {
return "key " + e.key + " not found"
}
type decodedCursor map[string]string
const (
encodedCursorMinLength = 1
encodedCursorMaxLength = 1000
)
func decodeCursor(s string) (decodedCursor, error) {
if err := validateStringLen(s, encodedCursorMinLength, encodedCursorMaxLength); err != nil {
return nil, err
}
decodedBytes, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, ErrInvalidCursor
}
decoded := string(decodedBytes)
kvps := strings.Split(decoded, cursorSeparator)
m := make(decodedCursor, len(kvps))
for _, kvp := range kvps {
k, v, found := strings.Cut(kvp, cursorKeyValueSeparator)
if !found {
return nil, ErrInvalidCursor
}
m[k] = v
}
return m, nil
}
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
}
func newSortFromString[T interface {
String() string
}](s string, allowed ...T) (T, error) {
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
var zero T
return zero, UnsupportedSortStringError{
Sort: s,
}
}
func isSortInConflict[T interface {
~uint8
}](s1 T, s2 T) bool {
ss := []T{s1, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}