Yonah f48506b1ca
generic: net return full response struct
This is still quite easy to stub, and the extra information is very
useful to the caller.

Signed-off-by: Yonah <contrib@gensokyo.uk>
2025-09-19 21:36:33 +09:00

174 lines
4.9 KiB
Go

package monstersirenfetch
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"os"
)
// AlbumResponse is the response of /api/album/%d/data and /api/album/%d/detail.
type AlbumResponse Response[Album]
// AlbumSong represents a minimal variant of [Song] embedded in the detail variant of [Album].
type AlbumSong struct {
CID StringInt `json:"cid"`
Name string `json:"name"`
Artists []string `json:"artistes"`
}
// Album represents the metadata of an album.
// Field availability is documented for each individual field.
type Album struct {
// CID is available for all variants of [Album].
CID StringInt `json:"cid"`
// Name is available for all variants of [Album].
Name string `json:"name"`
// Intro is available for data and detail variants of [Album].
Intro string `json:"intro,omitempty"`
// Belong is available for data and detail variants of [Album].
Belong string `json:"belong,omitempty"`
// CoverURL is available for all variants of [Album].
CoverURL string `json:"coverUrl"`
// CoverDeURL is available for data and detail variants of [Album].
CoverDeURL string `json:"coverDeUrl,omitempty"`
// Songs is available for the detail variant of [Album].
Songs []AlbumSong `json:"songs,omitempty"`
// Artists is available for base and data variants of [Album].
Artists []string `json:"artistes"`
}
const (
// AlbumVariantCurrent copies the current variant as-is.
AlbumVariantCurrent = iota
// AlbumVariantFull leaves all fields intact in the copy.
// This variant is not returned by any endpoint and is obtained via [Album.Enrich].
AlbumVariantFull
// AlbumVariantDetail zeroes [Album.Artists].
// This variant is returned by /api/album/%s/detail.
AlbumVariantDetail
// AlbumVariantData zeroes [Album.Songs].
// This variant is returned by /api/album/%d/data.
AlbumVariantData
// AlbumVariantBase zeroes [Album.Intro], [Album.Belong], [Album.CoverDeURL], [Album.Songs].
// This variant is included in /api/albums.
AlbumVariantBase
)
// IsFull returns whether [Album] is considered to be its [AlbumVariantFull] variant.
func (a *Album) IsFull() bool { return a.Belong != "" && len(a.Songs) > 0 }
// Copy makes a copy of [Album].
// For an [Album] where the IsFull method returns true, Copy zeroes fields to convert the copy into other variants.
// For [Album] where IsFull returns false, any variant other than [AlbumVariantCurrent] is undefined.
func (a *Album) Copy(v *Album, variant int) bool {
if v == nil || a == nil || v == a {
return false
}
if variant == AlbumVariantCurrent {
*v = *a
return true
}
if !a.IsFull() {
return false
}
switch variant {
case AlbumVariantFull:
*v = *a
case AlbumVariantDetail:
*v = *a
v.Artists = nil
case AlbumVariantData:
*v = *a
v.Songs = nil
case AlbumVariantBase:
*v = *a
v.Intro = ""
v.Belong = ""
v.CoverDeURL = ""
v.Songs = nil
default:
return false
}
return true
}
// Enrich populates the remaining fields of a non-full [Album].
func (a *Album) Enrich(ctx context.Context, n Net) error {
if a == nil || a.IsFull() {
return os.ErrInvalid
}
var v AlbumResponse
u := APIPrefix + "/album/" + a.CID.String() + "/detail"
if resp, err := n.Get(ctx, u); err != nil {
return err
} else if resp.StatusCode != http.StatusOK {
return &ResponseStatusError{u, resp.StatusCode, resp.Body.Close()}
} else {
if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
return errors.Join(resp.Body.Close(), err)
}
if err = resp.Body.Close(); err != nil {
return err
}
}
// these should be unreachable unless the server malfunctions
if a.CID != v.Data.CID {
return &InconsistentEnrichError[StringInt]{"cid", a.CID, v.Data.CID}
}
if a.Name != v.Data.Name {
return &InconsistentEnrichError[string]{"name", a.Name, v.Data.Name}
}
if a.CoverURL != v.Data.CoverURL {
return &InconsistentEnrichError[string]{"coverUrl", a.CoverURL, v.Data.CoverURL}
}
if v.Data.Artists != nil {
return &InconsistentEnrichError[[]string]{"artists", nil, v.Data.Artists}
}
v.Data.Artists = a.Artists
*a = v.Data
return nil
}
// albumDirect is [Album] without its MarshalJSON method.
type albumDirect Album
// albumNullable is [Album] with corresponding nullable string fields.
type albumNullable struct {
CID StringInt `json:"cid"`
Name string `json:"name"`
Intro NullableString `json:"intro"`
Belong NullableString `json:"belong"`
CoverURL string `json:"coverUrl"`
CoverDeURL NullableString `json:"coverDeUrl"`
Songs []AlbumSong `json:"songs,omitempty"`
Artists []string `json:"artistes"`
}
func (a *Album) MarshalJSON() (data []byte, err error) {
buf := new(bytes.Buffer)
e := NewEncoder(buf)
if a.Belong == "" { // this covers both data and detail variants
err = e.Encode((*albumDirect)(a))
data = buf.Bytes()
return
}
return json.Marshal(&albumNullable{a.CID, a.Name,
NullableString(a.Intro), NullableString(a.Belong),
a.CoverURL, NullableString(a.CoverDeURL), a.Songs, a.Artists})
}