0.02
No commit activity in last 3 years
No release in over 3 years
Create restful API easily with the Roda framework
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

~> 2
 Project Readme

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.