RESTRack
- serving JSON and XML with REST and pleasure.
Description
RESTRack is a Rack-based MVC
framework that makes it extremely easy to develop RESTful
data services. It is inspired by Rails, and follows a few of its conventions. But it has no routes
file, routing relationships are done through supplying custom code blocks to class methods such as "has_relationship_to" or
"has_mapped_relationships_to".
RESTRack aims at being lightweight and easy to use. It will automatically render JSON and
XML for the data structures you return in your actions (any structure parsable by the
"json" and "xml-simple" gems, respectively).
If you supply a view for a controller action, you do that using a builder file. Builder files are stored in the
view directory grouped by controller name subdirectories (view/<controller>/<action>.xml.builder
). XML format
requests will then render the view template with the builder gem, rather than generating XML with XmlSimple.
Rationale
Why RESTRack when there is Rails?
Rails is a powerful tool for full web applications. RESTRack is targeted at making
development of lightweight data services as easy as possible, while still giving you a performant and extensible
framework. The primary goal of of the development of RESTRack was to add as little as possible to the framework to give
the web developer a good application space for developing JSON and XML services.
Rails 3 instantiates approximately 80K more objects than RESTRack to do a hello world or nothing type response with
the default setup. Trimming Rails down by eliminating ActiveRecord, ActionMailer, and ActiveResource, it still
instantiates over 47K more objects than RESTRack.
OK, so why RESTRack when there is Sinatra?
RESTRack provides a full, albeit small, framework for developing RESTful MVC applications.
Installation
<sudo> gem install restrack
CLI Usage:
Generate a new service (FooBar::WebService)
- restrack generate service foo_bar
- restrack gen serv foo_bar
- restrack g s foo_bar
Generate a new controller (FooBar::BazController)
- restrack generate controller baz
- restrack gen cont baz
- restrack g c baz
Generate a new controller that descends from another (FooBar::NewController < FooBar::BazController)
- restrack generate controller new descendant_from baz
- restrack g controller new parent baz
Start up a server on default rackup port 9292
Start up a server on port 3456
- restrack server 3456
- restrack s 3456
REST action method names
All default RESTful controller method names align with their Rails counterparts, with two additional actions being
supported(*).
HTTP Verb: | GET | PUT | POST | DELETE
Collection URI (/widgets/): | index | replace | create | *drop
Element URI (/widgets/42): | show | update | *add | destroy
Automatic response data serialization
JSON
Objects returned from resource controller methods will have the "to_json" method called to serialize response output.
Controllers should return objects that respond to "to_json". RESTRack includes the JSON gem, which implements this method
on Ruby's standard lib simple data types (Array, Hash, String etc).
# GET /widgets/42.json
def show(id) # id will be 42
widget = Widget.find(id)
return widget
# The widget.to_json will be called and the resultant JSON sent as response body.
end
# GET /widgets/42
def show(id) # id will be 42
widget = Widget.find(id)
return widget
# The widget.to_json will be called unless the default response type is set to :XML in config/constants.yaml,
# in which case the widget.to_xml method will be called.
end
XML
RESTRack will convert the data structures that your actions return to JSON by default. You can change the default
by setting :DEFAULT_FORMAT to :XML in config/constants.yml
.
With Builder
Custom XML serialization can be done by providing Builder gem templates in views/<controller>/<action>.xml.builder
.
Custom Serialization Method
When XML is requested, objects returned from resource controller methods will have the "to_xml" method called to serialize
response output if an XML builder template file is not provided. If the response object does not respond to "to_xml", then
the object will be sent to XmlSimple for serialization.
With XmlSimple
RESTRack will attempt to serialize the data structures that your action methods return automatically using the
xml-simple gem. Complex objects may not serialize correctly, or you may want to define a particular structure for your
XML, in which case a builder template should be defined.
# GET /widgets/42.xml
def show(id) # id will be 42
widget = Widget.find(id)
return widget
# Template file views/widgets/show.xml.builder will be used to render the XML if it exists.
# If not, the widget.to_xml method will be called and the resultant XML sent as response body,
# or, if widget does not respond to "to_xml", then XmlSimple will be used to serialize the data object.
end
Accepting parameters and generating a response
Input parameters are accessible through the @params object. This is a merged hash containing the POST and GET parameters,
which can be accessed separately through @post_params and @get_params.
# GET /widgets/list.xml?offset=100&limit=50
def list
widget_list = Widget.limit( @params['limit'], @params['offset'] )
return widget_list
end
URLs and Controller relationships
RESTRack enforces a strict URL pattern through the construct of controller relationships, rather than a routing file.
Defining a controller for a resource means that you plan to expose that resource to requests to your service.
Defining a controller relationship means that you plan to expose a path from this resource to another.
"pass_through_to"
An open, or pass-through, path can be defined via the "pass_through_to" class method for resource controllers. This
exposes URL patterns like the following:
GET /foo/123/bar/234 <= simple pass-through from Foo 123 to show Bar 234
GET /foo/123/bar <= simple pass-through from Foo 123 to Bar index
"has_relationship_to"
A direct path to a single related resource's controller can be defined with the "has_relationship_to" method. This
allows you to define a one-to-one relationship from this resource to a related resource, which means that the id of
the related resource is implied through the id of the caller. The caller has one relation through a custom code block
passed to "has_relationship_to". The code block takes the caller resource's id and evaluates to the relation
resource's id, for example a PeopleController might define a one-to-one relationship like so:
has_relationship_to( :people, :as spouse ) do |id|
People.find(id).spouse.id
end
This exposes URL patterns like the following:
GET /people/Sally/spouse <= direct route to show Sally's spouse
PUT /people/Henry/spouse <= direct route to update Henry's spouse
POST /people/Jane/spouse <= direct route to add Jane's spouse
"has_relationships_to" and "has_defined_relationships_to"
A direct path to many related resources' controller can be defined with the "has_relationships_to" and
"has_defined_relationships_to" methods. These allows you to define one-to-many relationships. They work similar to
"has_relationship_to", except that they accept code blocks which evaluate to arrays of related child ids. Each
resource in the parent's relation list is then accessed through its array index (zero-based) in the URL. An example
of exposing the list of a People resource's children in this manner follows:
has_relationships_to( :people, :as => children ) do |id|
People.find(id).children.collect {|child| child.id}
end
Which exposes URLs similar to:
GET /people/Nancy/children/0 <= direct route to show child 0
DELETE /people/Robert/children/100 <= direct route to destroy child 100
An example of "has_defined_relationships_to":
has_defined_relationships_to( :people, :as => children ) do |id|
People.find(id).children.collect {|child| child.id}
end
exposes URL patterns:
GET /people/Nancy/children/George <= route to show child George
DELETE /people/Robert/children/Jerry <= route to destroy child Jerry
"has_mapped_relationships_to"
Multiple named one-to-many relationships can be exposed with the "has_mapped_relationships_to" method. This allows
you to define many named or keyword paths to related resources. The method's code block should accepts the parent id
and return a hash where the keys are your relationship names and the values are the child resource ids. For example,
within a PeopleController the following definition:
has_mapped_relationships_to( :people ) do |id|
{
'father' => People.find(id).father.id,
'mother' => People.find(id).mother.id,
'boss' => People.find(id).boss.id,
'assistant' => People.find(id).assistant.id
}
end
This would expose the following URL patterns:
GET /people/Fred/people/father => show the father of Fred
PUT /people/Fred/people/assistant => update Fred's assistant
POST /people/Fred/people/boss => add Fred's boss
DELETE /people/Luke/people/mother => destroy Luke's father
Setting the data type of the id - "keyed_with_type"
Resource id data types can be defined with the "keyed_with_type" class method within resource controllers. The
default data type of String is used if a different type is not specified.
Logging/Logging Level
RESTRack outputs to two logs, the standard log (or error log) and the request log. Paths and logging levels for these
can be configured in config/constants.yaml
. RESTRack uses Logger from Ruby-stdlib.
Inputs
Query string parameters
Available to controllers in the @params
instance variable.
POST data
Available to controllers in the @input
instance variable.
Constant Definition (config/constants.yaml
)
Required Configuration Settings
:LOG
Sets the location of the error log.
:REQUEST_LOG
Sets the location of the request log.
:LOG_LEVEL
Sets the the logging level of the error log, based on the Ruby Logger object. Supply these as a symbol, with valid
values being :DEBUG, :INFO, :WARN, etc.
:REQUEST_LOG_LEVEL
Sets the the logging level of the request log, similar to :LOG_LEVEL.
Optional Configuration Settings
:DEFAULT_FORMAT
Sets the default format for the response. This is the format that the response will take if no extension is appended to
the request string (i.e. /foo/123
rather than /foo/123.xml
). Services will have a default format of JSON if this
configuration option is not defined.
:DEFAULT_RESOURCE
Set this option in config/constants.yaml to use an implied root resource controller. To make /foo/123
also be accessible
at /123
:
:DEFAULT_RESOURCE: foo
:ROOT_RESOURCE_ACCEPT
This defines an array of resources that can be accessed as the first resource in the URL chain, without being proxied
through another relation.
:ROOT_RESOURCE_ACCEPT: [ 'foo', 'bar' ]
:ROOT_RESOURCE_DENY
This defines an array of resources that cannot be accessed without proxying though another controller.
:ROOT_RESOURCE_DENY: [ 'baz' ]
:SHOW_STACK
If defined, server error messages will contain the stack trace. This is not recommended when these errors could possibly
be delivered to the client.
:SHOW_STACK: true
License
Copyright (c) 2010 Chris St. John
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.