Roda plugin for RESTful APIs
Quick start
Install gem with
gem 'roda-rest_api' #Gemfile
or
gem install roda-rest_api #Manual
Create rack app
#api.ru
require 'roda/rest_api'
require 'json'
class App < Roda
plugin :rest_api
route do |r|
r.api do
r.version 3 do
r.resource :things do |things|
things.list {|param| ['foo', 'bar']}
things.routes :index
end
end
end
end
end
run App
And run with
bundle exec rackup api.ru
Try it out on:
curl http://127.0.0.1:9292/api/v3/things
Usage
route do |r|
r.api path:'', subdomain:'api' do # 'mount' on api.example.com/v1/...
r.version 1 do
#define all 7 routes:
# index - GET /v1/songs
# show - GET /v1/songs/:id
# create - POST /v1/songs
# update - PUT | PATCH /v1/songs/:id
# destroy - DELETE /v1/songs/:id
# edit - GET /v1/songs/:id/edit
# new - GET /v1/songs/new
# call permit to whitelist allowed parameters for save callback
r.resource :songs do |songs|
songs.list { |params| Song.where(params).all } #index
songs.one { |params| Song[params[:id]] } #show, edit, new
songs.delete { |params| Song[params[:id]].destroy } #destroy
songs.save { |atts| Song.create_or_update(atts) } #create, update
songs.permit :title, author: [:name, :address]
end
#define 2 routes and custom serializer, custom primary key:
# index - GET /v1/artists
# show - GET /v1/artists/:id
r.resource :artists, content_type: 'application/xml', primary_key: :artist_id do |artists|
artists.list { |params| Artist.where(params).all }
artists.one { |params| Artist[params[:artist_id]] }
artists.serialize { |result| ArtistSerializer.xml(result) }
artists.routes :index, :show
end
#define 6 singleton routes:
# show - GET /v1/profile
# create - POST /v1/profile
# update - PUT | PATCH /v1/profile
# destroy - DELETE /v1/profile
# edit - GET /v1/profile/edit
# new - GET /v1/profile/new
r.resource :profile, singleton: true do |profile|
profile.one { |params| current_user.profile } #show, edit, new
profile.save { |atts| current_user.profile.create_or_update(atts) } #create, update
profile.delete { |params| current_user.profile.destroy } #destroy
profile.permit :name, :address
end
#define nested routes
# index - GET /v1/albums/:parent_id/songs
# show - GET /v1/albums/:parent_id/songs/:id
# index - GET /v1/albums/:album_id/artwork
# index - GET /v1/albums/favorites
# show - GET /v1/albums/favorites/:id
r.resource :albums do |albums|
r.resource :songs do |songs|
songs.list { |params| Song.where({ :album_id => params[:parent_id] }) }
songs.one { |params| Song[params[:id]] }
songs.routes :index, :show
end
r.resource :artwork, parent_key: :album_id do |artwork|
artwork.list { |params| Artwork.where({ :album_id => params[:album_id] }).all }
artwork.routes :index
end
r.resource :favorites, bare: true do |favorites|
favorites.list { |params| Favorite.where(params).all }
favorites.one { |params| Favorite[params[:id]] ) }
favorites.routes :index, :show
end
end
#call block before route is called
r.resource :user, singleton: true do |user|
user.save {|atts| User.create_or_update(atts) }
user.routes :create # public
user.routes :update do # private
authenticate!
end
end
#define custom routes
r.resource :albums do
r.index do # GET /v1/albums
# list albums
end
r.create do # POST /v1/albums
# create album
end
r.show do |id| # GET /v1/albums/:id
# show album
end
r.update do |id| # PATCH | PUT /v1/albums/:id
# update album
end
r.destroy do |id| # DELETE /v1/albums/:id
# delete album
end
r.edit do |id| # GET /v1/albums/:id/edit
# edit album
end
r.new do # GET /v1/albums/new
# new album
end
end
end
end
end
###Options
The plugin supports several options serialize, content_type, wrapper and id_pattern to modify processing of the request. Besides these, any number of custom options can be passed, which can be handy for the wrapper option. Each option can be specified and overridden at the api, version or resource level.
####Serialization and content type
A serializer is an object that responds to :serialize and returns a string. Optionally it can provide the content_type, which may also be specified inline. Serialization can also be specified within a block inside the resource.
class XMLSerializer
def serialize(result) #required
result.to_xml
end
def content_type #optional
'text/xml'
end
end
class CSVSerializer
def serialize(result)
result.to_csv
end
end
class App < Roda
plugin :rest_api
route do |r|
r.api serializer: XMLSerializer.new
r.resource :things do |things|
things.list {|param| ['foo', 'bar']}
things.routes :index
end
r.resource :objects, serializer: CSVSerializer.new, content_type: 'text/csv' do |objects|
objects.one {|param| Object.find(param) }
objects.routes :show
end
r.resource :items do |items|
items.list {|param| Item.where(param) }
items.routes :index
items.serialize content_type: 'text/plain' do |result| #inline specification
result.to_s
end
end
end
end
end
####Wrapper
A wrapper module can be specified, containing one or more 'around_*' methods. These methods should yield with the passed arguments. Wrappers can be used for cleaning up incoming parameters, database transactions, authorization checking or serialization. A resource can hold a generic :resource option, that can be used for providing extra info to the wrapper. It can be useful to set a custom option like model_class on a resource when using wrappers.
module Wrapper
def around_save(atts)
# actions before save
result = yield(atts) # call save action
#actions after save
result
end
# around_one, around_list, around_delete
end
module SpecialWrapper
def around_delete(atts)
model_class = opts[:model_class]
if current_user.can_delete(model_class[atts[:id]])
yield(atts)
else
#not allowed
end
end
def current_user
@request.current_user
end
end
class App < Roda
plugin :rest_api
route do |r|
r.api wrapper: Wrapper
r.resource :things do |things|
things.one {|params| Thing.find(params) } # will be called inside the 'Wrapper#around_one' method
things.list {|params| Thing.where(params) } # will be called inside the 'Wrapper#around_list' method
things.save {|atts| Thing.create_or_update(atts) } # will be called inside the 'Wrapper#around_save' method
things.delete {|params| Thing.destroy(params) } # will be called inside the 'Wrapper#around_delete' method
end
r.resource :items, wrapper: SpecialWrapper, resource: {model_class: Item} do |items|
items.delete {|params| Item.destroy(params) } # will be called inside the 'SpecialWrapper#around_delete' method
end
end
end
end
####ID pattern
To support various id formats a regex can be specified to match custom id formats.
uuid_pat = /(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})/
r.api id_pattern: uuid_pat
r.resource :things do |things|
things.one {|params| Thing.find(params) }
r.resource :parts, id_pattern: /part(\d+)/ do |parts|
parts.one {|params| Part.find(params) }
end
end
end
# responds to /things/7e554915-210b-4dxe-a88b-3a09a5e790ge/parts/part123
Caveat
This plugin catches StandardError when performing the data access methods (list, one, save, delete) and will return a 404 or 422 response code when an error is thrown. When ENV['RACK_ENV'] is set to 'development' the error will be raised, but in all other cases it will fail silently. Be aware that ENV['RACK_ENV'] may be blank, so you won't see any errors even in development. A better approach is to develop and test the data access methods in isolation.