RAMLfications

Lynn Root

Backend Engineer @ Spotify

@roguelynn

What Is RAML?

RAML stands for RESTful API Modeling Language.

Very similar to Swagger and Blueprint, it's used to describe REST APIs.

What it looks like:

#%RAML 0.8
title: Spotify Web API
version: v1
baseUri: https://api.spotify.com/{version}
mediaType: application/json
documentation:
  - title: Spotify Web API Docs
    content: |
      Welcome to the _Spotify Web API_ specification. For more information about
      how to use the API, check out [developer site](https://developer.spotify.com/web-api/).
/albums:
  displayName: several-albums
  get:
    description: |
      [Get Several Albums](https://developer.spotify.com/web-api/get-several-albums/)
    queryParameters:
      ids:
        displayName: Spotify Album IDs
        type: string
        description: A comma-separated list of IDs
        required: true
        example: "382ObEPsp2rxGrnsizN5TX,1A2GTWGtFfWp7KSQTwWOyo,2noRn2Aes5aoNVsU6iWThc"
      market:
        displayName: Market
        description: The market (an ISO 3166-1 alpha-2 country code)
        type: string
        example: ES
        required: false
    responses:
      200:
        body:
          application/json:
            example: !include example/get-albums-example.json

Why?

Why a description language for your API?

  • Have a single source-of-truth reference for your API
  • Machine & human-ish readable
  • Clear definition of own versioned API specification

Why RAML?

  • not limited to describing in JSON (Swagger only supports JSON Schema)
  • Can support different API versioning (Blueprint can as well)
  • Include sample representations (Blueprint can as well)
  • Allows for including of external files from either local filesystem or via an HTTP request - no need for one massive .raml file (Neither Swagger nor Blueprints supports this)
  • It's not WADL

Why did Spotify choose RAML

  • "new kid on the block" & very active development - also means it's very young
  • more readable from our PoV
    • actual syntax & file structure
    • external file inclusions
  • open-source spec

Setting the scene

I hated answering repeated questions

So I built a thing

It takes our RAML file

And makes an interactive console

In [3]:
from IPython.display import Image
In [4]:
Image(filename='api-console-snapshot.png')
Out[4]:
In [6]:
Image(filename='api-console-snapshot-resp.png')
Out[6]:

enter: RAMLfications

TL;DR: Python reference implementation for RAML

Let's play:

$ pip install ramlfications

using RAMLfications within your library

In [8]:
from ramlfications import parse
In [16]:
RAML_FILE = "spotify-web-api.raml"
api = parse(RAML_FILE)
In [17]:
api
Out[17]:
RootNode(title='Spotify Web API')
In [29]:
#metadata
api.title
Out[29]:
'Spotify Web API'
In [19]:
api.version
Out[19]:
'v1'
In [30]:
api.base_uri
Out[30]:
'https://api.spotify.com/v1'
In [67]:
api.protocols
Out[67]:
['HTTPS']
In [28]:
# security schemes
api.security_schemes
Out[28]:
[SecurityScheme(name='oauth_2_0')]
In [27]:
oauth = api.security_schemes[0]
oauth.settings.get("scopes")
Out[27]:
['playlist-read-private',
 'playlist-modify-public',
 'playlist-modify-private',
 'user-library-read',
 'user-library-modify',
 'user-read-private',
 'user-read-birthdate',
 'user-read-email',
 'user-follow-read',
 'user-follow-modify']
In [31]:
# API endpoints
res = api.resources
res
Out[31]:
[ResourceNode(method='get', path='/albums'),
 ResourceNode(method='get', path='/albums/{id}'),
 ResourceNode(method='get', path='/albums/{id}/tracks'),
 ResourceNode(method='get', path='/artists'),
 ResourceNode(method='get', path='/artists/{id}'),
 ResourceNode(method='get', path='/artists/{id}/top-tracks'),
 ResourceNode(method='get', path='/artists/{id}/related-artists'),
 ResourceNode(method='get', path='/artists/{id}/albums'),
 ResourceNode(method='get', path='/tracks'),
 ResourceNode(method='get', path='/tracks/{id}'),
 ResourceNode(method='get', path='/search'),
 ResourceNode(method='get', path='/me'),
 ResourceNode(method='get', path='/me/tracks'),
 ResourceNode(method='put', path='/me/tracks'),
 ResourceNode(method='delete', path='/me/tracks'),
 ResourceNode(method='get', path='/me/tracks/contains'),
 ResourceNode(method='get', path='/users/{user_id}'),
 ResourceNode(method='get', path='/users/{user_id}/playlists'),
 ResourceNode(method='post', path='/users/{user_id}/playlists'),
 ResourceNode(method='get', path='/users/{user_id}/playlists/{playlist_id}'),
 ResourceNode(method='put', path='/users/{user_id}/playlists/{playlist_id}'),
 ResourceNode(method='get', path='/users/{user_id}/playlists/{playlist_id}/tracks'),
 ResourceNode(method='post', path='/users/{user_id}/playlists/{playlist_id}/tracks'),
 ResourceNode(method='put', path='/users/{user_id}/playlists/{playlist_id}/tracks'),
 ResourceNode(method='delete', path='/users/{user_id}/playlists/{playlist_id}/tracks'),
 ResourceNode(method='put', path='/users/{user_id}/playlists/{playlist_id}/followers'),
 ResourceNode(method='delete', path='/users/{user_id}/playlists/{playlist_id}/followers'),
 ResourceNode(method='get', path='/users/{user_id}/playlists/{playlist_id}/followers/contains'),
 ResourceNode(method='get', path='/browse/new-releases'),
 ResourceNode(method='get', path='/browse/featured-playlists'),
 ResourceNode(method='get', path='/browse/categories'),
 ResourceNode(method='get', path='/browse/categories/{category_id}'),
 ResourceNode(method='get', path='/browse/categories/{category_id}/playlists'),
 ResourceNode(method='put', path='/me/following'),
 ResourceNode(method='delete', path='/me/following'),
 ResourceNode(method='get', path='/me/following/contains')]
In [36]:
get_an_album = res[1]
get_an_album.uri_params
Out[36]:
[URIParameter(name='id')]
In [54]:
get_an_album.method
Out[54]:
'get'
In [55]:
get_an_album.description
Out[55]:
[Get an Album](https://developer.spotify.com/web-api/get-album/)
In [56]:
get_an_album.description.html
Out[56]:
u'<p><a href="https://developer.spotify.com/web-api/get-album/">Get an Album</a></p>\n'
In [58]:
get_an_album.display_name
Out[58]:
'album'
In [68]:
get_an_album.path
Out[68]:
'/albums/{id}'
In [69]:
get_an_album.absolute_uri
Out[69]:
'https://api.spotify.com/v1/albums/{id}'
In [70]:
# parameters
uri_param = get_an_album.uri_params[0]
uri_param.name
Out[70]:
'id'
In [40]:
uri_param.required
Out[40]:
True
In [41]:
uri_param.example
Out[41]:
'4aawyAB9vmqN3uQ7FjRGTy'
In [42]:
get_an_album.parent
Out[42]:
ResourceNode(method='get', path='/albums')
In [71]:
# API traits
api.traits
Out[71]:
[TraitNode(name='filterable'), TraitNode(name='paged')]
In [72]:
paged = api.traits[1]
paged.query_params
Out[72]:
[QueryParameter(name='limit'), QueryParameter(name='offset')]
In [73]:
query_param = _[0]
In [74]:
query_param.name
Out[74]:
'limit'
In [75]:
query_param.raw
Out[75]:
OrderedDict([('displayName', 'Limit'), ('description', 'The maximum number of track objects to return'), ('type', 'integer'), ('example', 10), ('minimum', 0), ('default', 20), ('maximum', 50), ('required', False)])

fun from the command line

validate your RAML file

In [81]:
!ramlfications validate spotify-web-api.raml
Success! Valid RAML file: spotify-web-api.raml
In [82]:
!ramlfications validate invalid.raml
Error validating file invalid.raml: 'FTP' not a valid protocol for a RAML-defined API.

Your API does in fact support FTP?

Just add it to your config file!

$ cat raml_config.ini
[custom]
protocols = FTP
In [84]:
!ramlfications validate --config raml_config.ini invalid.raml
Success! Valid RAML file: invalid.raml

visualize your RAML file

In [76]:
!ramlfications tree spotify-web-api.raml
===============
Spotify Web API
===============
Base URI: https://api.spotify.com/v1
|- /albums
|  - /albums/{id}
|    - /albums/{id}/tracks
|- /artists
|  - /artists/{id}
|    - /artists/{id}/top-tracks
|    - /artists/{id}/related-artists
|    - /artists/{id}/albums
|- /tracks
|  - /tracks/{id}
|- /search
|- /me
|  - /me/tracks
|  - /me/tracks
|  - /me/tracks
|    - /me/tracks/contains
|- /users/{user_id}
|- /users/{user_id}/playlists
|- /users/{user_id}/playlists
|  - /users/{user_id}/playlists/{playlist_id}
|  - /users/{user_id}/playlists/{playlist_id}
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|    - /users/{user_id}/playlists/{playlist_id}/followers
|    - /users/{user_id}/playlists/{playlist_id}/followers
|      - /users/{user_id}/playlists/{playlist_id}/followers/contains

|- /browse/featured-playlists
|- /browse/categories
|  - /browse/categories/{category_id}
|    - /browse/categories/{category_id}/playlists
|- /me/following
|- /me/following
|  - /me/following/contains
In [78]:
# MOAR!
!ramlfications tree spotify-web-api.raml -v
===============
Spotify Web API
===============
Base URI: https://api.spotify.com/v1
|- /albums
|  ⌙ GET
|  - /albums/{id}
|    ⌙ GET
|    - /albums/{id}/tracks
|      ⌙ GET
|- /artists
|  ⌙ GET
|  - /artists/{id}
|    ⌙ GET
|    - /artists/{id}/top-tracks
|      ⌙ GET
|    - /artists/{id}/related-artists
|      ⌙ GET
|    - /artists/{id}/albums
|      ⌙ GET
|- /tracks
|  ⌙ GET
|  - /tracks/{id}
|    ⌙ GET
|- /search
|  ⌙ GET
|- /me
|  ⌙ GET
|  - /me/tracks
|    ⌙ GET
|  - /me/tracks
|    ⌙ PUT
|  - /me/tracks
|    ⌙ DELETE
|    - /me/tracks/contains
|      ⌙ GET
|- /users/{user_id}
|  ⌙ GET
|- /users/{user_id}/playlists
|  ⌙ GET
|- /users/{user_id}/playlists
|  ⌙ POST
|  - /users/{user_id}/playlists/{playlist_id}
|    ⌙ GET
|  - /users/{user_id}/playlists/{playlist_id}
|    ⌙ PUT
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ GET
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ POST
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ PUT
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ DELETE
|    - /users/{user_id}/playlists/{playlist_id}/followers
|      ⌙ PUT
|    - /users/{user_id}/playlists/{playlist_id}/followers
|      ⌙ DELETE
|      - /users/{user_id}/playlists/{playlist_id}/followers/contains
|        ⌙ GET
|- /browse/new-releases
|  ⌙ GET
|- /browse/featured-playlists
|  ⌙ GET
|- /browse/categories
|  ⌙ GET
|  - /browse/categories/{category_id}
|    ⌙ GET
|    - /browse/categories/{category_id}/playlists
|      ⌙ GET
|- /me/following
|  ⌙ PUT
|- /me/following
|  ⌙ DELETE
|  - /me/following/contains
|    ⌙ GET
In [79]:
# I want MOAR
!ramlfications tree spotify-web-api.raml -vv
===============
Spotify Web API
===============
Base URI: https://api.spotify.com/v1
|- /albums
|  ⌙ GET
|     Query Params
|      ⌙ ids
|      ⌙ market
|  - /albums/{id}
|    ⌙ GET
|       Query Params
|        ⌙ market
|       URI Params
|        ⌙ id
|    - /albums/{id}/tracks
|      ⌙ GET
|         Query Params
|          ⌙ limit
|          ⌙ offset
|          ⌙ market
|         URI Params
|          ⌙ id
|- /artists
|  ⌙ GET
|     Query Params
|      ⌙ ids
|  - /artists/{id}
|    ⌙ GET
|       URI Params
|        ⌙ id
|    - /artists/{id}/top-tracks
|      ⌙ GET
|         Query Params
|          ⌙ country
|         URI Params
|          ⌙ id
|    - /artists/{id}/related-artists
|      ⌙ GET
|         URI Params
|          ⌙ id
|    - /artists/{id}/albums
|      ⌙ GET
|         Query Params
|          ⌙ limit
|          ⌙ offset
|          ⌙ album_type
|          ⌙ market
|         URI Params
|          ⌙ id
|- /tracks
|  ⌙ GET
|     Query Params
|      ⌙ ids
|      ⌙ market
|  - /tracks/{id}
|    ⌙ GET
|       Query Params
|        ⌙ market
|       URI Params
|        ⌙ id
|- /search
|  ⌙ GET
|     Query Params
|      ⌙ limit
|      ⌙ offset
|      ⌙ q
|      ⌙ type
|      ⌙ market
|- /me
|  ⌙ GET
|  - /me/tracks
|    ⌙ GET
|       Query Params
|        ⌙ limit
|        ⌙ offset
|        ⌙ market
|  - /me/tracks
|    ⌙ PUT
|       Query Params
|        ⌙ ids
|  - /me/tracks
|    ⌙ DELETE
|       Query Params
|        ⌙ ids
|    - /me/tracks/contains
|      ⌙ GET
|         Query Params
|          ⌙ ids
|- /users/{user_id}
|  ⌙ GET
|     URI Params
|      ⌙ user_id
|- /users/{user_id}/playlists
|  ⌙ GET
|     Query Params
|      ⌙ limit
|      ⌙ offset
|     URI Params
|      ⌙ user_id
|- /users/{user_id}/playlists
|  ⌙ POST
|     URI Params
|      ⌙ user_id
|  - /users/{user_id}/playlists/{playlist_id}
|    ⌙ GET
|       Query Params
|        ⌙ fields
|       URI Params
|        ⌙ playlist_id
|        ⌙ user_id
|  - /users/{user_id}/playlists/{playlist_id}
|    ⌙ PUT
|       URI Params
|        ⌙ playlist_id
|        ⌙ user_id
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ GET
|         Query Params
|          ⌙ fields
|          ⌙ limit
|          ⌙ offset
|          ⌙ market
|         URI Params
|          ⌙ playlist_id
|          ⌙ user_id
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ POST
|         Query Params
|          ⌙ position
|          ⌙ uris
|         URI Params
|          ⌙ playlist_id
|          ⌙ user_id
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ PUT
|         URI Params
|          ⌙ playlist_id
|          ⌙ user_id
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ DELETE
|         URI Params
|          ⌙ playlist_id
|          ⌙ user_id
|    - /users/{user_id}/playlists/{playlist_id}/followers
|      ⌙ PUT
|         URI Params
|          ⌙ playlist_id
|          ⌙ user_id
|    - /users/{user_id}/playlists/{playlist_id}/followers
|      ⌙ DELETE
|         URI Params
|          ⌙ playlist_id
|          ⌙ user_id
|      - /users/{user_id}/playlists/{playlist_id}/followers/contains
|        ⌙ GET
|           Query Params
|            ⌙ ids
|           URI Params
|            ⌙ playlist_id
|            ⌙ user_id
|- /browse/new-releases
|  ⌙ GET
|     Query Params
|      ⌙ limit
|      ⌙ offset
|      ⌙ country
|- /browse/featured-playlists
|  ⌙ GET
|     Query Params
|      ⌙ limit
|      ⌙ offset
|      ⌙ country
|      ⌙ locale
|      ⌙ timestamp
|- /browse/categories
|  ⌙ GET
|     Query Params
|      ⌙ country
|      ⌙ locale
|      ⌙ limit
|      ⌙ offset
|  - /browse/categories/{category_id}
|    ⌙ GET
|       Query Params
|        ⌙ country
|        ⌙ locale
|       URI Params
|        ⌙ category_id
|    - /browse/categories/{category_id}/playlists
|      ⌙ GET
|         Query Params
|          ⌙ country
|          ⌙ limit
|          ⌙ offset
|         URI Params
|          ⌙ category_id
|- /me/following
|  ⌙ PUT
|     Query Params
|      ⌙ type
|      ⌙ ids
|- /me/following
|  ⌙ DELETE
|     Query Params
|      ⌙ type
|      ⌙ ids
|  - /me/following/contains
|    ⌙ GET
|       Query Params
|        ⌙ type
|        ⌙ ids
In [80]:
# I WANT MOOOOARRRRR
!ramlfications tree spotify-web-api.raml -vvv
===============
Spotify Web API
===============
Base URI: https://api.spotify.com/v1
|- /albums
|  ⌙ GET
|     Query Params
|      ⌙ ids: Spotify Album IDs
|      ⌙ market: Market
|  - /albums/{id}
|    ⌙ GET
|       Query Params
|        ⌙ market: Market
|       URI Params
|        ⌙ id: Spotify Album ID
|    - /albums/{id}/tracks
|      ⌙ GET
|         Query Params
|          ⌙ limit: Limit
|          ⌙ offset: Offset
|          ⌙ market: Market
|         URI Params
|          ⌙ id: Spotify Album ID
|- /artists
|  ⌙ GET
|     Query Params
|      ⌙ ids: Spotify Artist IDs
|  - /artists/{id}
|    ⌙ GET
|       URI Params
|        ⌙ id: Spotify Artist ID
|    - /artists/{id}/top-tracks
|      ⌙ GET
|         Query Params
|          ⌙ country: Country
|         URI Params
|          ⌙ id: Spotify Artist ID
|    - /artists/{id}/related-artists
|      ⌙ GET
|         URI Params
|          ⌙ id: Spotify Artist ID
|    - /artists/{id}/albums
|      ⌙ GET
|         Query Params
|          ⌙ limit: Limit
|          ⌙ offset: Offset
|          ⌙ album_type: Album Type (single, album, appears_on, compilation)
|          ⌙ market: Market
|         URI Params
|          ⌙ id: Spotify Artist ID
|- /tracks
|  ⌙ GET
|     Query Params
|      ⌙ ids: Spotify Track IDs
|      ⌙ market: Market
|  - /tracks/{id}
|    ⌙ GET
|       Query Params
|        ⌙ market: Market
|       URI Params
|        ⌙ id: Spotify Track ID
|- /search
|  ⌙ GET
|     Query Params
|      ⌙ limit: Limit
|      ⌙ offset: Offset
|      ⌙ q: Query
|      ⌙ type: Item Type (album, artist, track, playlist)
|      ⌙ market: Market
|- /me
|  ⌙ GET
|  - /me/tracks
|    ⌙ GET
|       Query Params
|        ⌙ limit: Limit
|        ⌙ offset: Offset
|        ⌙ market: Market
|  - /me/tracks
|    ⌙ PUT
|       Query Params
|        ⌙ ids: Spotify Track IDs
|  - /me/tracks
|    ⌙ DELETE
|       Query Params
|        ⌙ ids: Spotify Track IDs
|    - /me/tracks/contains
|      ⌙ GET
|         Query Params
|          ⌙ ids: Spotify Track IDs
|- /users/{user_id}
|  ⌙ GET
|     URI Params
|      ⌙ user_id: User ID
|- /users/{user_id}/playlists
|  ⌙ GET
|     Query Params
|      ⌙ limit: Limit
|      ⌙ offset: Offset
|     URI Params
|      ⌙ user_id: Owner ID
|- /users/{user_id}/playlists
|  ⌙ POST
|     URI Params
|      ⌙ user_id: Owner ID
|  - /users/{user_id}/playlists/{playlist_id}
|    ⌙ GET
|       Query Params
|        ⌙ fields: Fields
|       URI Params
|        ⌙ playlist_id: Playlist ID
|        ⌙ user_id: Owner ID
|  - /users/{user_id}/playlists/{playlist_id}
|    ⌙ PUT
|       URI Params
|        ⌙ playlist_id: Playlist ID
|        ⌙ user_id: Owner ID
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ GET
|         Query Params
|          ⌙ fields: Fields
|          ⌙ limit: Limit
|          ⌙ offset: Offset
|          ⌙ market: Market
|         URI Params
|          ⌙ playlist_id: Playlist ID
|          ⌙ user_id: Owner ID
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ POST
|         Query Params
|          ⌙ position: Position (append by default)
|          ⌙ uris: Spotify Track URIs
|         URI Params
|          ⌙ playlist_id: Playlist ID
|          ⌙ user_id: Owner ID
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ PUT
|         URI Params
|          ⌙ playlist_id: Playlist ID
|          ⌙ user_id: Owner ID
|    - /users/{user_id}/playlists/{playlist_id}/tracks
|      ⌙ DELETE
|         URI Params
|          ⌙ playlist_id: Playlist ID
|          ⌙ user_id: Owner ID
|    - /users/{user_id}/playlists/{playlist_id}/followers
|      ⌙ PUT
|         URI Params
|          ⌙ playlist_id: Playlist ID
|          ⌙ user_id: Owner ID
|    - /users/{user_id}/playlists/{playlist_id}/followers
|      ⌙ DELETE
|         URI Params
|          ⌙ playlist_id: Playlist ID
|          ⌙ user_id: Owner ID
|      - /users/{user_id}/playlists/{playlist_id}/followers/contains
|        ⌙ GET
|           Query Params
|            ⌙ ids: Spotify user IDs
|           URI Params
|            ⌙ playlist_id: Playlist ID
|            ⌙ user_id: Owner ID
|- /browse/new-releases
|  ⌙ GET
|     Query Params
|      ⌙ limit: Limit
|      ⌙ offset: Offset
|      ⌙ country: Country
|- /browse/featured-playlists
|  ⌙ GET
|     Query Params
|      ⌙ limit: Limit
|      ⌙ offset: Offset
|      ⌙ country: Country
|      ⌙ locale: Locale
|      ⌙ timestamp: Timestamp
|- /browse/categories
|  ⌙ GET
|     Query Params
|      ⌙ country: Country
|      ⌙ locale: Locale
|      ⌙ limit: Limit
|      ⌙ offset: Offset
|  - /browse/categories/{category_id}
|    ⌙ GET
|       Query Params
|        ⌙ country: Country
|        ⌙ locale: Locale
|       URI Params
|        ⌙ category_id: Category ID
|    - /browse/categories/{category_id}/playlists
|      ⌙ GET
|         Query Params
|          ⌙ country: Country
|          ⌙ limit: Limit
|          ⌙ offset: Offset
|         URI Params
|          ⌙ category_id: Category ID
|- /me/following
|  ⌙ PUT
|     Query Params
|      ⌙ type: Item Type
|      ⌙ ids: Spotify IDs
|- /me/following
|  ⌙ DELETE
|     Query Params
|      ⌙ type: Item Type
|      ⌙ ids: Spotify IDs
|  - /me/following/contains
|    ⌙ GET
|       Query Params
|        ⌙ type: Item Type
|        ⌙ ids: Spotify IDs

What's next?

Coming soon:

  • Documentation generator based off of RAML
  • API console

FIN

Docs: ramlfications.readthedocs.org

Code: github.com/spotify/ramlfications

Slides: rogue.ly/ramlfications

IPython Notebook: rogue.ly/sf-ipynb

Thanks!