feat(api): add conversion helpers for library DTOs
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
// uuidToString renders a pgtype.UUID as a canonical hyphenated string.
|
||||
// Returns "" when the UUID is invalid (unset). Duplicated from the subsonic
|
||||
// package intentionally — the two packages must not depend on each other.
|
||||
func uuidToString(u pgtype.UUID) string {
|
||||
if !u.Valid {
|
||||
return ""
|
||||
}
|
||||
b := u.Bytes
|
||||
const hex = "0123456789abcdef"
|
||||
out := make([]byte, 36)
|
||||
j := 0
|
||||
for i, x := range b {
|
||||
if i == 4 || i == 6 || i == 8 || i == 10 {
|
||||
out[j] = '-'
|
||||
j++
|
||||
}
|
||||
out[j] = hex[x>>4]
|
||||
out[j+1] = hex[x&0x0f]
|
||||
j += 2
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// parseUUID accepts the canonical hyphenated form produced by uuidToString.
|
||||
// Returns ok=false on any malformed input so handlers can 400 cleanly.
|
||||
func parseUUID(s string) (pgtype.UUID, bool) {
|
||||
var u pgtype.UUID
|
||||
if err := u.Scan(s); err != nil {
|
||||
return pgtype.UUID{}, false
|
||||
}
|
||||
return u, u.Valid
|
||||
}
|
||||
|
||||
// yearFromDate returns the 4-digit year from a pgtype.Date, or 0 when the
|
||||
// date is unset. Album release years are optional at scan time.
|
||||
func yearFromDate(d pgtype.Date) int {
|
||||
if !d.Valid {
|
||||
return 0
|
||||
}
|
||||
return d.Time.Year()
|
||||
}
|
||||
|
||||
// durationMsToSec converts millisecond durations (as stored in tracks.duration_ms)
|
||||
// to seconds, rounding to nearest. Callers display seconds; sub-second precision
|
||||
// is not useful in UI.
|
||||
func durationMsToSec(ms int32) int {
|
||||
return int((ms + 500) / 1000)
|
||||
}
|
||||
|
||||
// coverURL returns the /api relative URL for an album's cover art. The
|
||||
// endpoint ships in Plan 3; until then it 404s, and the SPA does not wire
|
||||
// <img src> to it.
|
||||
func coverURL(albumID pgtype.UUID) string {
|
||||
return "/api/albums/" + uuidToString(albumID) + "/cover"
|
||||
}
|
||||
|
||||
// streamURL returns the /api relative URL for a track's audio stream. The
|
||||
// endpoint ships in Plan 3.
|
||||
func streamURL(trackID pgtype.UUID) string {
|
||||
return "/api/tracks/" + uuidToString(trackID) + "/stream"
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func TestUUIDToString(t *testing.T) {
|
||||
var u pgtype.UUID
|
||||
if err := u.Scan("00112233-4455-6677-8899-aabbccddeeff"); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
got := uuidToString(u)
|
||||
want := "00112233-4455-6677-8899-aabbccddeeff"
|
||||
if got != want {
|
||||
t.Errorf("uuidToString = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
var zero pgtype.UUID
|
||||
if s := uuidToString(zero); s != "" {
|
||||
t.Errorf("uuidToString(invalid) = %q, want empty", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUUID(t *testing.T) {
|
||||
u, ok := parseUUID("00112233-4455-6677-8899-aabbccddeeff")
|
||||
if !ok || !u.Valid {
|
||||
t.Fatalf("parseUUID valid: ok=%v valid=%v", ok, u.Valid)
|
||||
}
|
||||
if _, ok := parseUUID("not-a-uuid"); ok {
|
||||
t.Error("parseUUID accepted garbage")
|
||||
}
|
||||
if _, ok := parseUUID(""); ok {
|
||||
t.Error("parseUUID accepted empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYearFromDate(t *testing.T) {
|
||||
var d pgtype.Date
|
||||
if y := yearFromDate(d); y != 0 {
|
||||
t.Errorf("yearFromDate(invalid) = %d, want 0", y)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurationMsToSec(t *testing.T) {
|
||||
cases := []struct {
|
||||
ms int32
|
||||
want int
|
||||
}{
|
||||
{0, 0},
|
||||
{500, 1}, // rounds up
|
||||
{499, 0}, // rounds down
|
||||
{1500, 2}, // rounds up
|
||||
{60000, 60},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := durationMsToSec(c.ms); got != c.want {
|
||||
t.Errorf("durationMsToSec(%d) = %d, want %d", c.ms, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoverURLAndStreamURL(t *testing.T) {
|
||||
var u pgtype.UUID
|
||||
if err := u.Scan("00112233-4455-6677-8899-aabbccddeeff"); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
if got := coverURL(u); got != "/api/albums/00112233-4455-6677-8899-aabbccddeeff/cover" {
|
||||
t.Errorf("coverURL = %q", got)
|
||||
}
|
||||
if got := streamURL(u); got != "/api/tracks/00112233-4455-6677-8899-aabbccddeeff/stream" {
|
||||
t.Errorf("streamURL = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user