package domain import ( "bytes" "encoding/base64" "fmt" "net/url" "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, } }