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: name → value, in case of associations name → URI. 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.