Gem Version

Summary

Toast is a Rack application that hooks into Ruby on Rails. It exposes ActiveRecord models as a web service (REST API). The main difference from doing that with Ruby on Rails itself is it's DSL that covers all aspects of an API in one single configuration. For each model and API endpoint you define:

  • what models and attributes are to be exposed
  • what methods are supported (GET, PATCH, DELETE, POST,...)
  • hooks to handle authorization
  • customized handlers

When using Toast there's no Rails controller involved. Model classes and the API configuration is sufficient.

Toast uses a REST/hypermedia style API, which is an own interpretation of the REST idea, not compatible with others like JSON API, Siren etc. It's design is much simpler and based on the idea of traversing opaque URIs.

Other features are:

  • windowing of collections via Range/Content-Range headers (paging)
  • attribute selection per request
  • processing of URI parameters

See the User Manual for a detailed description.

Releases

Toast version ≥ 1.0.2

Works with Rails from version 4.2.9+ up to 6. This version will be tested with upcoming new Rails releases and receives bugfixes and new features.

Toast version 0.9.*

Works with Rails 3 and 4. It has a much different and smaller DSL, which is not compatible with v1. This version will not receive any updates or fixes anymore.

Installation

with Bundler (Gemfile) from Rubygems:

source 'http://rubygems.org'
gem "toast"

from Github:

gem "toast", :git => "https://github.com/robokopp/toast.git"

then run

bundle
rails generate toast init
  create  config/toast-api.rb
  create  config/toast-api

Example

Let the table bananas have the following schema:

 create_table "bananas", :force => true do |t|
   t.string   "name"
   t.integer  "number"
   t.string   "color"
   t.integer  "apple_id"
 end

and let a corresponding model class have this code:

 class Banana < ActiveRecord::Base
   belongs_to :apple
   has_many :coconuts

   scope :less_than_100, -> { where("number < 100") }
 end

Then we can define the API like this (in config/toast-api/banana.rb):

  expose(Banana) {

     readables :color
     writables :name, :number

     via_get {
        allow do |user, model, uri_params|
          true
        end
     }

     via_patch {
        allow do |user, model, uri_params|
          true
        end
     }

     via_delete {
        allow do |user, model, uri_params|
          true
        end
     }

     collection(:less_than_100) {
       via_get {
         allow do |user, model, uri_params|
           true
         end
       }
     }

     collection(:all) {
       max_window 16

       via_get {
         allow do |user, model, uri_params|
           true
         end
       }

       via_post {
         allow do |user, model, uri_params|
           true
         end
       }
     }

     association(:coconuts) {
       via_get {
         allow do |user, model, uri_params|
           true
         end

         handler do |banana, uri_params|
           if uri_params[:max_weight] =~ /\A\d+\z/
             banana.coconuts.where("weight <= #{uri_params[:max_weight]}")
           else
             banana.coconuts
           end.order(:weight)
         end
       }

       via_post {
         allow do |user, model, uri_params|
           true
         end
       }

       via_link {
         allow do |user, model, uri_params|
           true
         end
       }
     }

     association(:apple) {
       via_get {
         allow do |user, model, uri_params|
           true
         end
       }
     }
  }

Note, that all allow-blocks in the above example return true. In practice authorization logic should be applied. An allow-block must be defined for each endpoint because it defaults to return false, which causes a 401 Unauthorized response.

The above definition exposes the model Banana as such:

Get a single resource representation:

GET http://www.example.com/bananas/23
--> 200,  '{"self":     "http://www.example.com/bananas/23"
            "name":     "Fred",
            "number":   33,
            "color":    "yellow",
            "coconuts": "http://www.example.com/bananas/23/coconuts",
            "apple":    "http://www.example.com/bananas/23/apple" }'

The representation of a record is a flat JSON map: namevalue, in case of associations nameURI. The special key self contains the URI from which this record can be fetch alone. self can be treated as a unique ID of the record (globally unique, if under a FQDN).

Get a collection (the :all collection)

GET http://www.example.com/bananas
--> 200,  '[{"self":     "http://www.example.com/bananas/23",
             "name":     "Fred",
             "number":   33,
             "color":    "yellow",
             "coconuts": "http://www.example.com/bananas/23/coconuts",
             "apple":    "http://www.example.com/bananas/23/apple,
            {"self":     "http://www.example.com/bananas/24",
              ... }, ... ]'

The default length of collections is limited to 42, this can be adjusted globally or for each endpoint separately. In this case no more than 16 will be delivered due to the max_window 16 directive.

Get a customized collection

GET http://www.example.com/bananas/less_than_100
--> 200, '[{BANANA}, {BANANA}, ...]'

Any scope or class method returning a relation can be published this way.

Get an associated collection + filter

GET http://www.example.com/bananas/23/coconuts?max_weight=3
--> 200, '[{COCONUT},{COCONUT},...]',

The COCONUT model must be exposed too. URI parameters can be processed in custom handlers for sorting and filtering.

Update a single resource:

PATCH http://www.example.com/bananas/23, '{"name": "Barney", "number": 44, "foo" => "bar"}'
--> 200,  '{"self":     "http://www.example.com/bananas/23"
            "name":     "Barney",
            "number":   44,
            "color":    "yellow",
            "coconuts": "http://www.example.com/bananas/23/coconuts",
            "apple":    "http://www.example.com/bananas/23/apple"}'

Toast ingores unknown attributes, but prints warnings in it's log file. Only attributes from the 'writables' list will be updated.

Create a new record

POST http://www.example.com/bananas, '{"name": "Johnny", "number": 888}'
--> 201, '{"self":     "http://www.example.com/bananas/102"
           "name":     "Johnny",
           "number":    888,
           "color":     null,
           "coconuts": "http://www.example.com/bananas/102/coconuts",
           "apple":    "http://www.example.com/bananas/102/apple }'

Create an associated record

POST http://www.example.com/bananas/23/coconuts, '{COCONUT}'
--> 201,  {"self":"http://www.example.com/coconuts/432, ...}

Delete records

DELETE http://www.example.com/bananas/23
--> 200

Linking records

LINK "http://www.example.com/bananas/23/coconuts",
  Link:  "http://www.example.com/coconuts/31"
--> 200

Toast uses the (unusual) HTTP verbs LINK and UNLINK in order to express the action of linking or unlinking existing resources. The above request will add Coconut#31 to the association Banana#coconuts.