Introduction
We’re building a SDK to programmatically interact with Spotify using Ruby. It focuses on being as familiar and native to Ruby developers, from hobbyists to experts, and allowing them to be creative with music technology.
Even more importantly, we’re focusing on building software that isn’t just expected to be abandoned; but rather help to experiment and define better standards for consistently maintained community software.
Read more about the standards we’re aiming to achieve on GitHub.
Objectives
As mentioned in our README.md
, we have four objectives with this SDK:
-
🧒 Thoughtfully inclusive for beginners. Everything we do should think about beginners from the start. From having an enforced Code of Conduct policy to building great documentation, tooling, and an empathetic feedback process. Designing for beginners is designing for longevity.
-
☁️ Agnostic to minor changes. APIs change all the time. We should be opinionated enough that our software should break with major changes, but flexible enough to work perfectly fine with minor changes. Our code should only depend on critical data, such as IDs.
-
🌈 Delightful for developers. Writing the SDK and using the SDK should be equally delightful. Granted, this is a challenging goal; but with solid information architecture, well-crafted opinions, clear and helpful error messages, and software that doesn’t get in your way - we will create quite lovely software.
-
✨ A maintained production-level. It doesn’t take experts to write production-level code; all it takes is considerate guidance from the community. We should write software that we and others trust to do what it is intended to do. We care about Semantic Versioning for clear version changes.
Getting Started
There’s multiple ways you can contribute, and we’ll cover all of them:
Website
This refers to the website you are currently viewing. It is bundled in the master
branch of bih/spotify-ruby in the docs/
folder, even though it is not part of the source code itself. It is not compiled inside of the gem itself.
The source code is compiled using Ruby and Jekyll. It is kindly hosted for free through GitHub Pages.
Setting up Jekyll
You’ll want to run these commands in your Terminal:
$ git clone ssh://git@github.com/bih/spotify-ruby.git
$ cd spotify-ruby/docs/
$ gem install bundler
$ bundle install
$ bundle exec jekyll serve --watch --open-url
The last command creates a local server and reloads whenever you make changes. Don’t forget that if you edit the _config.yml
you will need to restart the server by pressing Ctrl + C and then re-running the last command.
More information and troubleshooting can be found in Jekyll’s Installation guide.
Folder Structure
In the docs/
folder, see all the respective content:
Type | Description | Location |
---|---|---|
Markdown | Website Documentation | docs/assets/**/* |
Images | Content Assets | docs/assets/**/* |
Images | Layout Assets | docs/theme/assets/**/* |
CSS | SASS Stylesheets | docs/theme/_sass/*.scss |
HTML | HTML Components | docs/theme/_includes/*.html |
HTML | HTML Layouts | docs/theme/_layouts/*.html |
SDK
This covers all of the code in spotify-ruby
, excluding the docs/
folder (which was covered in Website above).
Versioning
We follow the Semantic Versioning convention in our versioning. Here’s a excerpt that will explain the convention better:
Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes,
MINOR version when you add functionality in a backwards-compatible manner, and
PATCH version when you make backwards-compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
As a general rule of thumb, we always preference backwards compatibility as much as possible. It helps us achieve our 🌈 Delightful for Developers objective for this SDK.
Configuration
In our project, we have the core files in the root folder /
:
Type | File | Description |
---|---|---|
Local Config | .rspec | Our config for running our testing framework, RSpec. |
Local Config | .rubocop.yml | Our config for our Ruby static code analyzer tool, Rubocop. |
Local Config | .ruby-version | The Ruby version we’re using to build spotify-ruby . |
Local Config | .rvm-version | Our RVM-specific Ruby version we’re using to build spotify-ruby . |
Local Config | Gemfile | Used for running bundler install during installation. |
External Config | .travis.yml | Our config for our continuous integration provider, Travis CI. |
External Config | spotify-ruby.gemspec | Used for configuring the spotify-ruby gem. |
Tooling | Rakefile | Used for running rake helper commands. |
Documentation | LICENSE | A distributed excerpt of our source code license. |
Documentation | CODE_OF_CONDUCT.md | Our official Code of Conduct policy. |
Documentation | COVERAGE.md | Our SDK coverage status of all Spotify Developer APIs. |
SDK Folder Structure
In the lib/spotify/
and spec/
folders, we have a specific folder structure that we’ll explain:
lib/spotify/
Type | File | Description |
---|---|---|
Ruby | version.rb | Stores the Spotify::VERSION constant. See Versioning for more details. |
Ruby | accounts.rb | The source file for Spotify::Accounts and authenticating with the SDK. |
Ruby | accounts/session.rb | ↳ A subclass of Spotify::Accounts that deals with session management. |
Ruby | sdk.rb | The source file for Spotify::SDK and interfacing with the Spotify Platform. |
Ruby | sdk/base.rb | ↳ The core class for creating a SDK Component. |
Ruby | sdk/model.rb | ↳ The core class for creating a SDK Model. |
All other files in lib/spotify/sdk/
are either components or models. It is possible to identify what type of object they are by seeing where they inherit from.
# frozen_string_literal: true
module Spotify
class SDK
class Me < Base # This is a component.
end
class User < Model # This is a model.
end
end
end
spec/
Type | File | Description |
---|---|---|
Ruby | spec_helper.rb | The core Ruby file which configures our RSpec, WebMock and testing environment. |
Mocks | factories.rb | The source of truth for generating “mock” Ruby objects for the SDK. Powered by FactoryBot. |
Fixtures | support/fixtures/ | The folder containing sample Spotify API responses to be used with WebMock. |
All files in the spec/
with the *_spec.rb
filename are RSpec tests. They depend on WebMock for mocking API responses and FactoryBot for mocking Ruby objects.
To see a full list of all RSpec tests, run this in your Terminal:
$ ls -l spec/**/*_spec.rb
Adding a Component
Components are subclasses of Spotify::SDK
and typically represent a category of features for the Spotify Platform. For example, Connect
represents a category of features - listing devices, controlling them, reading current playback, etc. Another example would be Me
- listing my information, my top tracks, my saved tracks, etc.
In the bin/
folder, we have provided a Rails-like generator that will generate the relevant files for you to add a new component:
Generating Component
For example, if you’d like to generate a component called Friends
run the following command:
$ bin/generate_component friends
It will then generate the following files for you:
$ cat lib/spotify/sdk/friends.rb
$ cat spec/lib/spotify/sdk/friends_spec.rb
Mounting Component
Then you’ll need to do two manual steps in lib/spotify/sdk.rb
:
-
Include the component at the top:
# Components require "spotify/sdk/friends"
-
Then mount the component as
friends
inSpotify::SDK::SDK_COMPONENTS
:SDK_COMPONENTS = { ..., friends: Spotify::SDK::Friends }.freeze
That’s it! We have setup a component. You can go ahead and write some fun logic! 🙂
Writing Component Logic
In our component, we can create a method called hi
:
# frozen_string_literal: true
module Spotify
class SDK
class Friend < Base
##
# Hello world!
#
# @see https://bih.github.io/spotify-ruby/documentation/contributing/
#
# @param [Class] param_name Description
# @return [String] text Description
#
def hi
"Hello world!"
end
end
end
end
The comments above are used by YARD to generate our SDK Reference on https://rubydoc.info.
Debugging Component Logic
As we’ve mounted our component as friends
already, we can use the following code to test it in our console by running bin/console
:
$ bin/console
[1] pry(main)> @accounts = Spotify::Accounts.new(client_id: "[client id]", client_secret: "[client secret]", redirect_uri: "[redirect_uri]")
=> #<Spotify::Accounts:0x007fb1ac8fba28>
[2] pry(main)> @session = Spotify::Accounts::Session.from_refresh_token(@accounts, "[refresh token]")
=> #<Spotify::Accounts::Session:0x007fb1ac8d9360>
[3] pry(main)> @sdk = Spotify::SDK.new(@session)
=> #<Spotify::SDK:0x007fb1abbafd10>
[4] pry(main)> @sdk.friends
=> #<Spotify::SDK::Friends:0x007fd7d31784e8>
[5] pry(main)> @sdk.friends.hi
=> "Hello world"
Testing Component Logic
In our generated spec/lib/spotify/sdk/friends_spec.rb
file, we can write some RSpec tests:
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Spotify::SDK::Friends do
let(:session) { build(:session, access_token: "access_token") }
subject { Spotify::SDK.new(session).friends }
describe "#hi" do
it "returns 'Hello world'" do
expect(subject.hi).to eq "Hello world"
end
end
end
And then you can execute tests by running rake ci
in the root directory.
Sample Component Implementation
For an example of a good implementation, see the following files for Spotify::SDK::Me
component:
- Implementation: lib/spotify/sdk/me.rb
- RSpec Tests: spec/lib/spotify/sdk/me_spec.rb
- Fixture: spec/support/fixtures/get/v1/me/object.json
Adding a Model
Models are typically classes that hold data. They often contain functions to manipulate or perform actions with that data. An example is a Device
model, it would contain a method like pause!
which will take the id
and send a PUT /v1/me/player/pause?device_id={id}
HTTP command.
Protip: The models we write use Ruby’s OpenStruct data structure; it is designed for flexible Hash objects. It fits in our second objective of “being agnostic to minor changes”; in cases where Spotify may return null
or contain additional fields, our codebase will not break.
In our bin/
folder, similarly to components, we have a Rails-like generator that will generate the relevant files for you to add a new model:
Generating Model
For example, if you’d like to generate a model called Device
run the following command:
$ bin/generate_model device
It will then generate the following files for you:
$ cat lib/spotify/sdk/device.rb
$ cat spec/lib/spotify/sdk/device_spec.rb
$ cat spec/factories/device.rb
Mounting a Model
Then you’ll need to do one step in lib/spotify/sdk.rb
:
- Include the model at the top:
# Models require "spotify/sdk/device"
Writing Model Logic
In our model, we can create a method called pause!
:
# frozen_string_literal: true
module Spotify
class SDK
class Device < Model
##
# Pause this device.
#
# @see https://bih.github.io/spotify-ruby/documentation/contributing/
#
# @param [Class] param_name Description
# @return [String] text Description
#
def pause!
parent.send_http_request(:put, "/v1/me/player/play?device_id=#{id}", {
http_options: {
expect_nil: true # This API returns a blank response. This is OK.
}
})
self # Let's return itself, so we can support chaining methods.
end
end
end
end
And then we can initialize the SDK with two parameters:
- A
Hash
payload. For example{ device_id: "id of device here" }
- An instance of a valid Component (see Friends above)
You can see an example in Debugging Model Logic below.
Debugging Model Logic
With our new Spotify::SDK::Device
model, we can now run this in bin/console
:
$ bin/console
[1] pry(main)> @accounts = Spotify::Accounts.new(client_id: "[client id]", client_secret: "[client secret]", redirect_uri: "[redirect_uri]")
=> #<Spotify::Accounts:0x007fb1ac8fba28>
[2] pry(main)> @session = Spotify::Accounts::Session.from_refresh_token(@accounts, "[refresh token]")
=> #<Spotify::Accounts::Session:0x007fb1ac8d9360>
[3] pry(main)> @sdk = Spotify::SDK.new(@session)
=> #<Spotify::SDK:0x007fb1abbafd10>
[4] pry(main)> @base = Spotify::SDK::Base.new(@sdk)
=> #<Spotify::SDK::Base:0x007fb1ac11dd10>
[5] pry(main)> @device = Spotify::SDK::Device.new({ id: 1234, is_active: true, is_private_session: false, is_restricted: false, name: "Device Name", type: "Smartphone", volume_percent: 24 }, @base)
=> #<Spotify::SDK::Connect::Device id=1234, is_active=true, is_private_session=false, is_restricted=false, name="Device Name", type="Smartphone", volume_percent=24>
[6] pry(main)> @device.pause!
=> #<Spotify::SDK::Connect::Device id=1234, is_active=true, is_private_session=false, is_restricted=false, name="Device Name", type="Smartphone", volume_percent=24>
Testing Model Logic
For models, we have a slightly different approach to testing them. As they handle API response data from Spotify, we would need to mock what those responses look like to ensure the SDK is able to respond in the way we expect it to.
Usually we’d need to add the following:
-
Mock Ruby model object: FactoryBot
-
Typically adding this Ruby code to
spec/factories.rb
:factory :device, class: Spotify::SDK::Device do association :parent, factory: :base skip_create initialize_with { new(attributes, parent) } end
-
-
Mock API response: Fixtures
- Typically adding the expected JSON response for the model in
spec/support/fixtures/
with the criteria: - Filename format convention:
spec/support/fixtures/{method_type}/{version}/{endpoint}/{custom_name}.json
- Example:
spec/support/fixtures/v1/get/me/player/devices/active-list.json
- Typically adding the expected JSON response for the model in
And then similarly to Components, we’ll need to add tests for the Spotify::SDK::Device
object we created in spec/lib/spotify/sdk/device_spec.rb
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Spotify::SDK::Device do
let(:raw_data) { read_fixture("get/v1/me/player/devices/active-list") }
let(:session) { build(:session, access_token: "access_token") }
let(:sdk_base) { Spotify::SDK::Base.new(Spotify::SDK.new(session)) }
subject { Spotify::SDK::Device.new(raw_data, sdk_base) }
describe "#to_h" do
it "returns the correct value" do
expect(subject.to_h).to eq raw_data
end
end
describe "#pause!" do
it "should make an api call" do
stub = stub_request(:put, "https://api.spotify.com/v1/me/player/pause?device_id=#{raw_data[:id]}")
.with(headers: {Authorization: "Bearer access_token"})
subject.pause!
expect(stub).to have_been_requested
end
it "should return itself" do
expect(subject.pause!).to be subject
end
end
end
And you can run all of the tests by running rake ci
in the root directory.
Sample Model Implementation
For an example of a good implementation, see the following files for Spotify::SDK::Connect::Device
model:
- Implementation: lib/spotify/sdk/connect/device.rb
- RSpec Tests: spec/lib/spotify/sdk/connect/device_spec.rb
- Ruby Object Mock: spec/factories.rb
- Spotify API Response Mocks / Fixtures:
- Response w/ active list: spec/support/fixtures/get/v1/me/player/devices/active-list.json
- Response w/ inactive list: spec/support/fixtures/get/v1/me/player/devices/inactive-list.json
- Response w/ empty list: spec/support/fixtures/get/v1/me/player/devices/empty-list.json
Testing
On top of writing tests for Components and Models, we have additional tools & tests for quality assurance in other areas:
Type | Tool | Command | Purpose |
---|---|---|---|
Linter | Rubocop | rake rubocop | To lint Ruby syntax for anti-patterns, readability, and more. |
Test Coverage | SimpleCov | rake spec | To calculate test coverage (how much of our code has tests). |
Documentation | YARD | rake yard | To generate API Reference documentation. Note: it has been mentioned in this guide. |
Debugger | Pry | bin/console | Improved console-based debugging. |
Creating a Pull Request
We’d love to see you suggest changes and make a pull request! In order to make a pull request, you’ll need to fork the bih/spotify-ruby repository. Then make the relevant changes and create a PR to bih/spotify-ruby:master
on GitHub.
Protip: First time making a pull request? Check out this guide from GitHub!
We will then review your changes, and then approve/merge it for you.
- If the change is with
docs/
, once merged GitHub Pages will automatically deploy your changes to bih.github.io/spotify-ruby. It can sometimes take up to 15 minutes to propagate. - If the change is with the SDK, we will publish the changes in the next MAJOR/MINOR release. Urgent changes will result in an expedited PATCH release, as per the Semantic Versioning convention.
If we have reviewed it, we will most likely suggest changes for you. The feedback will need to be addressed by you or another community member. We may close PRs that may be inactive, unclear, or controversial.
Add Your Profile as a Contributor
If you’re contributing changes to any file in docs/documentation/
, it is possible to mention yourself as an official contributor!
It’s possible to see at the top of this page, or in the screenshot below:
To add yourself, in the relevant .md
file, at the top you should see:
contributors:
- https://github.com/bih
All you need to do is to create a new line with your GitHub username:
contributors:
- https://github.com/bih
- https://github.com/[your github username here]
That’s all - we’ll use your public GitHub avatar and give you some 💖!