I'm trying to use classes, structs, and Decodable
to make a response hierarchy for easy and automatic decoding of Apple Music API response. The Apple Music API response objects are kind of confusing though, and I'm not sure how to make a structure that allows extensibility in the future when I expand the types that subclass other types. Here's what I have so far:
A ResponseRoot object in the Apple Music API looks like this for me:
class AMResponseRoot : Decodable {
let data: [AMResourceResponse]?
let next: String?
// let results: AppleMusicResults?
// let errors: [AppleMusicError]?
private enum AppleMusicResponseRootCodingKeys : String, CodingKey {
case data = "data"
case next = "next"
case results = "results"
case errors = "errors"
}
required init(from decoder: Decoder) throws {
let rootContainer = try decoder.container(keyedBy: AppleMusicResponseRootCodingKeys.self)
self.data = try rootContainer.decodeIfPresent([AMResourceResponse].self, forKey: .data)
self.next = try rootContainer.decodeIfPresent(String.self, forKey: .next)
// self.results = try rootContainer.decodeIfPresent(AppleMusicResults.self, forKey: .results)
// self.errors = try rootContainer.decodeIfPresent([Error].self, forKey: .errors)
}
}
I've commented out the results
and errors
decoding for now, since I'm not sure how decoding works for those/I don't know what the AppleMusicResponse object will look like.
This ResponseRoot is pretty straightforward, I just have a dynamic class/protocol type for the resource in the data
object array. Now, defining what an AMResourceResponse
is has been difficult, because I want to be able to subclass it or make a class adopt it as a protocol, such as when I have different types of response objects I can expect, like an AMSongResponse
or AMAlbumResponse
or AMPlaylistResponse
, etc.
How can I make it so that each AMResourceResponse
is guaranteed to have the attributes: Attributes
, id: String
, and type: String
fields? Those are the fields I can find that are common between all types of response objects in the data
field in the ResponseRoot
object.
Attributes
should also be defined by the subclass itself, too, because different response objects have different structures for what attributes they return. But all of them will have some sort of Attributes
in the response, so I want to codify that in the response objects well. For example, a Song.Attributes object would have those specific fields, but an Album.Attributes would have different ones.
On top of this, there's a Resource object in the Apple Music API that might be useful for abstracting what a "Resource" means, and maybe it could inform my definition of AMResourceResponse
somehow, idk.
Here's everything I have so far, if it helps. It's really all over the place though.
protocol AMResourceResponse {
struct Attributes {
}
var attributes: Attributes { get }
var id: String { get }
var type: String { get }
}
class AMSongResponse : AMResourceResponse {
struct Attributes : Decodable {
let albumName: String
let artistName: String
let artwork: Artwork
let name: String
let playParams: PlayParams
let trackNumber: Int
private enum AttributesCodingKeys : String, CodingKey {
case albumName = "albumName"
case artistName = "artistName"
case artwork = "artwork"
case name = "name"
case playParams = "playParams"
case trackNumber = "trackNumber"
}
init(from decoder: Decoder) throws {
let attributesContainer = try decoder.container(keyedBy: AttributesCodingKeys.self)
self.albumName = try attributesContainer.decode(String.self, forKey: .albumName)
self.artistName = try attributesContainer.decode(String.self, forKey: .artistName)
self.artwork = try attributesContainer.decode(Artwork.self, forKey: .artwork)
self.name = try attributesContainer.decode(String.self, forKey: .name)
self.playParams = try attributesContainer.decode(PlayParams.self, forKey: .playParams)
self.trackNumber = try attributesContainer.decode(Int.self, forKey: .trackNumber)
}
}
}
struct PlayParams : Decodable {
let id: String
let isLibrary: Bool
let kind: String
private enum PlayParamsCodingKeys : String, CodingKey {
case id = "id"
case isLibrary = "isLibrary"
case kind = "kind"
}
init(from decoder: Decoder) throws {
let playParamsContainer = try decoder.container(keyedBy: PlayParamsCodingKeys.self)
self.id = try playParamsContainer.decode(String.self, forKey: .id)
self.isLibrary = try playParamsContainer.decode(Bool.self, forKey: .isLibrary)
self.kind = try playParamsContainer.decode(String.self, forKey: .kind)
}
}
struct Artwork : Decodable {
let height: Int
let width: Int
let url: String
private enum ArtworkCodingKeys : String, CodingKey {
case height = "height"
case width = "width"
case url = "url"
}
init(from decoder: Decoder) throws {
let artworkContainer = try decoder.container(keyedBy: ArtworkCodingKeys.self)
self.height = try artworkContainer.decode(Int.self, forKey: .height)
self.width = try artworkContainer.decode(Int.self, forKey: .width)
self.url = try artworkContainer.decode(String.self, forKey: .url)
}
}
struct Album : Decodable {
struct Attributes : Decodable {
let artistName: String
let artwork: Artwork
let contentRating: String?
let name: String
let playParams: PlayParams?
let trackCount: Int
}
let attributes: Attributes
}
struct Artist : Decodable {
}
struct Relationships : Decodable {
struct LibraryAlbumRelationship : Decodable {
let albums: [Album]
}
struct LibraryArtistRelationship : Decodable {
let artists: [Artist]
}
let albumRelationships: LibraryAlbumRelationship
let artistRelationships: LibraryArtistRelationship
private enum RelationshipsCodingKeys : String, CodingKey {
case data = "data"
}
init(from decoder: Decoder) throws {
let attributesContainer = try decoder.container(keyedBy: RelationshipsCodingKeys.self)
self.stuff = try attributesContainer.decode(String.self, forKey: .data)
}
}
I'm just really scatterbrained from being deep in this and could use some direction as to how to construct this hierarchy. Thanks :)