The musiclib app is a partially completed API that will power client apps similar to something like Spotify. Your job is to continue adding functionality, specifically to add the concept of playlists for users.
Here is a breakdown of the existing data model for the application:
User
attribute | type |
---|---|
id | integer |
string | |
api_key | string |
created_at | datetime |
updated_at | datetime |
Artist
attribute | type |
---|---|
id | integer |
name | string |
created_at | datetime |
updated_at | datetime |
Album
attribute | type |
---|---|
id | integer |
name | string |
available | boolean |
artist_id | integer |
created_at | datetime |
updated_at | datetime |
Song
attribute | type |
---|---|
id | integer |
title | string |
track_number | integer |
length_seconds | integer |
album_id | integer |
created_at | datetime |
updated_at | datetime |
Here is a breakdown of the existing resource API for the application:
verb | resource | route | controller#action | note |
---|---|---|---|---|
GET | album | /api/v1/albums | api/v1/albums#index | list all available albums |
GET | album | /api/v1/artists/:artist_id/albums | api/v1/albums#index | list all available albums for a specific artist |
GET | album | /api/v1/albums/:id | api/v1/albums#show | get a specific album |
GET | artist | /api/v1/artists | api/v1/artists#index | list all artists |
GET | artist | /api/v1/artists/:id | api/v1/artists#show | get a specific artist |
GET | song | /api/v1/songs | api/v1/songs#index | list all songs |
GET | song | /api/v1/albums/:album_id/songs | api/v1/songs#index | list all songs for a specific album |
GET | song | /api/v1/songs/:id | api/v1/songs#show | get a specific song |
GET | user | /api/v1/users | api/v1/users#index | list all users |
POST | user | /api/v1/users | api/v1/users#create | create a new user |
GET | user | /api/v1/users/:id | api/v1/users#show | get a specific user |
Clone the app and run bundle install. Then, create, migrate, and seed the database.
Start up the rails server and test out some of the endpoints:
- list albums
- list an artist's albums
- get an album
- list artists
- get an artist
- list songs
- list an album's songs
- get a song
The client dev team has requested the following routes be made available for playlists and playlist songs. Add these routes to the musiclib application.
verb | resource | route | controller#action | note |
---|---|---|---|---|
GET | playlist | /api/v1/playlists/:id | api/v1/playlists#show | get a specific playlist |
GET | playlist | /api/v1/user/:user_id/playlists | api/v1/playlists#index | list all playlists for a specific user |
POST | playlist | /api/v1/user/:user_id/playlists | api/v1/playlist#create | create a playlist for a specific user |
GET | song | /api/v1/playlist/:playlist_id/songs | api/v1/songs#index | list all songs |
POST | song | /api/v1/playlist/:playlist_id/songs | api/v1/songs#create | add a song to a playlist |
- Add the playlist route to show a specific playlist.
- Nested within the user resource, add the playlist routes to list and create playlists.
- Nested within the playlist resource, add the song routes to list and create songs.
- Limit routes to only the ones expected by the client app team.
Create playlist controller and implement the index
, create
and show
actions to respond to the routes you created.
index
should only work if nested under user.show
should only work without nesting.create
should only work if nested under a user.
Playlist
attribute | type |
---|---|
id | integer |
name | string |
user_id | integer |
created_at | datetime |
updated_at | datetime |
PlaylistSong
attribute | type |
---|---|
id | integer |
playlist_id | integer |
song_id | integer |
created_at | datetime |
updated_at | datetime |
We need to add a playlist model to the app so that users can create playlists and add songs to them.
- Add a Playlist ActiveRecord model to the app.
- Create and run a migration to set the attributes of the playlist model in the database to match the table above.
- Add a validation to the model that ensures playlists always have names.
We need a way to associate songs and playlists but we can't do it directly because songs can belong to many playlists and playlists can have many songs. So we'll use an intermediary model to represent the concept of a 'playlist song' to capture the relationship and store it in the database.
- Add a PlaylistSong ActiveRecord model to the app
- Create and run a migration to set the attributes of the playlist model in the database to match the table above
Now, we need to set up the various active record associations in the model files to let rails now how our playlist and playlist songs related to themselves and other models.
- Add an association to user represting that it can have many associated playlists
- Add an association to playlist represting that it can have many associated playlist_songs
- Add an association to playlist represting that it can have many songs via the playlist_songs association
- Add associations to playlist_song represting that a playlist_song belongs to both a playlist and a song
- Add associations to song represting that they can have many playlist_songs and have_many playlists via playlist_songs
The length_seconds
value in the Albums controller is calculated directly in the controller and is unavailable to the rest of the application. Writing code like this inside the controller makes it impossible to reuse and is generally considered a bad practice. Since it works based on an instance of a model, let's convert the functionality to an instance method on the Album model.
- Add a
length_seconds
method to the model and convert the call in the controller.
The starting code:
album.songs.reduce(0) { |length, song| length + song.length_seconds }
The ending code:
album.length_seconds
The method should total up all of the songs' length_seconds
on an album.
Like the length_seconds
value, the sorting of songs is done in the song controller. This behavior is not reusable by the rest of the system. Because sorting is done against the list of songs, and not an individual song, it doesn't make sense to move into the model. Let's extract a service object called SongSorter
. It should be placed in the app/services
directory and should be initialized with two arguments: 1) the list of songs and 2) the sort value. It should have one method named sort
which returns the sorted songs.
- Create
SongSorter
inapp/services
. - Instantiate and call
sort
onSongSorter
in the SongsController instead of the current conditional block. - The sorting should work regardless of whether you are listing by album, playlist, or all songs.
The album#index controller action has two queries that order by the album name and searches for only albums that are available. This common query should be converted into a scope on album named available
that lists only available albums ordered by name.
- Convert album query to scope named
available
. - Use
available
scope instead of thewhere
andorder
queries for both albums and artist's albums.
The query also results in an n+1 query because each album's songs are loaded after querying for the list of albums. Prevent this n+1 query in the controller by including the songs in the query.
- Solve inefficient n+1 query by including songs
The spec/models/album_spec.rb
file has 5 tests that need to be written.
- Test that an album can be valid when properly set up
- Test that an album is invalid without a name
- Test that an album has exactly the expected attributes
- Test that the available scope returns expected results from the database
- Test that the length_seconds method returns expected length
The spec/requests/api/v1/albums/get_album_spec.rb
needs to be completed by testing that the expected album is successfully returned from the endpoint.
Uncomment the file located spec/routing/api/v1/api_routes_spec.rb
and run the full spec suite. This file tests the routes that are defined in config/routes.rb
and ensures they go to the appropriate controller. If all of the routes for playlist and songs were properly added, this spec should pass.