diff --git a/composite.go b/composite.go new file mode 100644 index 0000000..8ea9c4b --- /dev/null +++ b/composite.go @@ -0,0 +1,68 @@ +package monstersirenfetch + +import ( + "os" + "strconv" +) + +// AlbumConflictError is returned by [Flatten] if two or more albums share the same CID. +type AlbumConflictError struct { + Index int + Previous Album + Current Album +} + +func (e *AlbumConflictError) Error() string { + return "album CID conflict on index " + strconv.Itoa(e.Index) + ": " + + "previous album " + e.Previous.Name + ", " + + "current album " + e.Current.Name +} + +// SongConflictError is returned by [Flatten] if two or more songs belonging to the same [Album] share the same CID. +type SongConflictError struct { + Index int + Previous *Song + Current Song +} + +func (e *SongConflictError) Error() string { + return "song CID conflict on index " + strconv.Itoa(e.Index) + ": " + + "previous song " + e.Previous.Name + ", " + + "current song " + e.Current.Name +} + +// CompositeAlbum represents an [Album] with a collection of its associated identifier-only [Song]. +type CompositeAlbum struct { + // Songs is a map of [Song.CID] to identifier-only [Song]. + Songs map[StringInt]*Song + + *Album +} + +// CompositeAlbumsMap is a map of [Album.CID] to [CompositeAlbum]. +type CompositeAlbumsMap map[StringInt]CompositeAlbum + +// Flatten flattens [AlbumsData] and [SongsData] into a [CompositeAlbumsMap]. +// All values in [CompositeAlbum.Songs] and the [CompositeAlbum.Album] field are guaranteed to be non-nil. +func Flatten(albumData AlbumsData, songsData SongsData) (CompositeAlbumsMap, error) { + m := make(CompositeAlbumsMap, len(albumData)) + for i, a := range albumData { + if c, ok := m[a.CID]; ok { + return nil, &AlbumConflictError{Index: i, Previous: *c.Album, Current: a} + } + m[a.CID] = CompositeAlbum{Songs: make(map[StringInt]*Song), Album: &a} + } + + for i, s := range songsData.List { + var c *Song + if a, ok := m[s.AlbumCID]; !ok { + return nil, os.ErrNotExist + } else if c, ok = a.Songs[s.CID]; ok { + return nil, &SongConflictError{Index: i, Previous: c, Current: s} + } else { + a.Songs[s.CID] = &s + } + } + + return m, nil +} diff --git a/composite_test.go b/composite_test.go new file mode 100644 index 0000000..afd1738 --- /dev/null +++ b/composite_test.go @@ -0,0 +1,31 @@ +package monstersirenfetch_test + +import ( + "encoding/json" + "testing" + + . "git.gensokyo.uk/yonah/monstersirenfetch" +) + +func TestFlatten(t *testing.T) { + t.Run("sample", func(t *testing.T) { + var ( + albumsResp AlbumsResponse + songsResp SongsResponse + ) + + if err := json.Unmarshal(albumsJSON, &albumsResp); err != nil { + t.Fatalf("Unmarshal: error = %v", err) + } + if err := json.Unmarshal(songsJSON, &songsResp); err != nil { + t.Fatalf("Unmarshal: error = %v", err) + } + + if m, err := Flatten(albumsResp.Data, songsResp.Data); err != nil { + t.Fatalf("Flatten: error = %v", err) + } else { + // TODO(ophestra): validate this result + t.Logf("Flatten: %#v", m) + } + }) +} diff --git a/song.go b/song.go index 5190fc5..31faae3 100644 --- a/song.go +++ b/song.go @@ -9,6 +9,7 @@ import ( type SongResponse Response[Song] // Song holds the metadata of a song. +// Fields marked with omitempty are only populated when the IsFull method returns true. type Song struct { CID StringInt `json:"cid"` Name string `json:"name"`