described_routes

DESCRIPTION

Features:

  • Dynamic, framework-neutral, client-friendly ResourceTemplate metadata describing the path/URI structures of your whole site or of specific resources

  • A link header-based discovery protocol, enabling clients to find ResourceTemplate metadata from the resources of any enabled controller

  • Easy integration with Rails

  • JSON, YAML and XML formats, also a bonus plain text report

ATTENTION: 0.8.0 adds Rails integration via Rack middleware; the Rails controller and helpers are hereby deprecated!

INSTALL:

sudo gem install mynyml-rack-respond_to --source http://gems.github.com
sudo gem install described_routes

USAGE:

Build time

In your Rakefile:

require 'tasks/described_routes'

Then:

$ rake --tasks described_routes
rake described_routes:json        # Describe resource structure in JSON format
rake described_routes:xml         # Describe resource structure in XML format
rake described_routes:yaml        # Describe resource structure in YAML format
rake described_routes:text        # Describe resource structure in text (comparable to "rake routes")

The JSON, XML and YAML formats (of which the YAML is the most readable) are designed for program consumption. The more human-friendly text output looks like this:

$ rake --silent described_routes:text
root                   root                                          /
admin_products         admin_products         GET, POST              /admin/products{-prefix|.|format}
  new_admin_product    new_admin_product      GET                    /admin/products/new{-prefix|.|format}
  {product_id}         admin_product          GET, PUT, DELETE       /admin/products/{product_id}{-prefix|.|format}
    edit               edit_admin_product     GET                    /admin/products/{product_id}/edit{-prefix|.|format}
described_routes       described_routes       GET, POST              /described_routes{-prefix|.|format}
  new_described_route  new_described_route    GET                    /described_routes/new{-prefix|.|format}
  {route_name}         described_route        GET, PUT, DELETE       /described_routes/{route_name}{-prefix|.|format}
    edit               edit_described_route   GET                    /described_routes/{route_name}/edit{-prefix|.|format}
pages                  pages                  GET, POST              /pages{-prefix|.|format}
  new_page             new_page               GET                    /pages/new{-prefix|.|format}
  {page_id}            page                   GET, PUT, DELETE       /pages/{page_id}{-prefix|.|format}
    edit               edit_page              GET                    /pages/{page_id}/edit{-prefix|.|format}
    summary            summary_page           GET                    /pages/{page_id}/summary{-prefix|.|format}
    toggle_visibility  toggle_visibility_page POST                   /pages/{page_id}/toggle_visibility{-prefix|.|format}
users                  users                  GET, POST              /users{-prefix|.|format}
  new_user             new_user               GET                    /users/new{-prefix|.|format}
  {user_id}            user                   GET, PUT, DELETE       /users/{user_id}{-prefix|.|format}
    edit               edit_user              GET                    /users/{user_id}/edit{-prefix|.|format}
    articles           user_articles          GET, POST              /users/{user_id}/articles{-prefix|.|format}
      new_user_article new_user_article       GET                    /users/{user_id}/articles/new{-prefix|.|format}
      recent           recent_user_articles   GET                    /users/{user_id}/articles/recent{-prefix|.|format}
      {article_id}     user_article           GET, PUT, DELETE       /users/{user_id}/articles/{article_id}{-prefix|.|format}
        edit           edit_user_article      GET                    /users/{user_id}/articles/{article_id}/edit{-prefix|.|format}
    profile            user_profile           GET, PUT, DELETE, POST /users/{user_id}/profile{-prefix|.|format}
      edit             edit_user_profile      GET                    /users/{user_id}/profile/edit{-prefix|.|format}
      new              new_user_profile       GET                    /users/{user_id}/profile/new{-prefix|.|format}

You can specify the base URL of your application by appending “BASE=http://…” to the rake command line. The output will then include full URI templates.

Run time

In your config/environment.rb, add the DescribedRoutes::Middleware::Rails middleware in the Rails::Initializer.run block:

require 'described_routes/middleware/rails'

Rails::Initializer.run do |config|
  config.middleware.use DescribedRoutes::Middleware::Rails
end

There are two integration steps for run time support:

You (or your client application) can now browse to any of the following top level addresses:

  • …/described_routes

  • …/described_routes.txt

  • …/described_routes.json

  • …/described_routes.xml

  • …/described_routes.yaml

and for the named route “users” (say):

  • …/described_routes/users

  • …/described_routes/users.txt

  • …/described_routes/users.json

  • …/described_routes/users.xml

  • …/described_routes/users.yaml

In the absence of content negotiation, requests to addresses without format extensions redirect to the respective .txt address.

Note that

Example:

$ curl http://localhost:3000/described_routes/users.txt
users                  users                GET, POST              http://localhost:3000/users{-prefix|.|format}
  new_user             new_user             GET                    http://localhost:3000/users/new{-prefix|.|format}
  {user_id}            user                 GET, PUT, DELETE       http://localhost:3000/users/{user_id}{-prefix|.|format}
    edit               edit_user            GET                    http://localhost:3000/users/{user_id}/edit{-prefix|.|format}
    articles           user_articles        GET, POST              http://localhost:3000/users/{user_id}/articles{-prefix|.|format}
      new_user_article new_user_article     GET                    http://localhost:3000/users/{user_id}/articles/new{-prefix|.|format}
      recent           recent_user_articles GET                    http://localhost:3000/users/{user_id}/articles/recent{-prefix|.|format}
      {article_id}     user_article         GET, PUT, DELETE       http://localhost:3000/users/{user_id}/articles/{article_id}{-prefix|.|format}
        edit           edit_user_article    GET                    http://localhost:3000/users/{user_id}/articles/{article_id}/edit{-prefix|.|format}
    profile            user_profile         GET, PUT, DELETE, POST http://localhost:3000/users/{user_id}/profile{-prefix|.|format}
      edit             edit_user_profile    GET                    http://localhost:3000/users/{user_id}/profile/edit{-prefix|.|format}
      new              new_user_profile     GET                    http://localhost:3000/users/{user_id}/profile/new{-prefix|.|format}

Partial template expansion

Any query parameters passed to the controller will be used to pre-populate the templates. In this example, the article_id and format parameters have been replaced, leaving article_id:

$ curl "http://localhost:3000/described_routes/user.text?format=json&user_id=dojo"
user                 user                 GET, PUT, DELETE       http://localhost:3000/users/dojo.json
  edit               edit_user            GET                    http://localhost:3000/users/dojo/edit.json
  articles           user_articles        GET, POST              http://localhost:3000/users/dojo/articles.json
    new_user_article new_user_article     GET                    http://localhost:3000/users/dojo/articles/new.json
    recent           recent_user_articles GET                    http://localhost:3000/users/dojo/articles/recent.json
    {article_id}     user_article         GET, PUT, DELETE       http://localhost:3000/users/dojo/articles/{article_id}.json
      edit           edit_user_article    GET                    http://localhost:3000/users/dojo/articles/{article_id}/edit.json
  profile            user_profile         GET, PUT, DELETE, POST http://localhost:3000/users/dojo/profile.json
    edit             edit_user_profile    GET                    http://localhost:3000/users/dojo/profile/edit.json
    new              new_user_profile     GET                    http://localhost:3000/users/dojo/profile/new.json

More typically, JSON, YAML or XML format would be requested. Their addresses can be referenced in <link> elements in the <head> section of an HTML page or (better) in HTTP headers, so any resource - regardless of format - can easily link to its own instance-specific metadata.

JSON example (after pretty printing):

$ curl "http://localhost:3000/described_routes/user_articles.yaml?user_id=dojo&format=json"
{
   "name":"user_articles",
   "rel":"articles",
   "path_template":"\/users\/dojo\/articles.json",
   "uri_template":"http:\/\/localhost:3000\/users\/dojo\/articles.json",
   "options":["GET", "POST"],
   "resource_templates":[
      {
         "name":"new_user_article",
         "options":["GET"],
         "path_template":"\/users\/dojo\/articles\/new.json",
         "uri_template":"http:\/\/localhost:3000\/users\/dojo\/articles\/new.json",
         "rel":"new_user_article"
      },
      {
         "name":"recent_user_articles",
         "options":["GET"],
         "path_template":"\/users\/dojo\/articles\/recent.json",
         "uri_template":"http:\/\/localhost:3000\/users\/dojo\/articles\/recent.json",
         "rel":"recent"
      },
      {
         "name":"user_article",
         "resource_templates":[
            {
               "name":"edit_user_article",
               "options":["GET"],
               "path_template":"\/users\/dojo\/articles\/{article_id}\/edit.json",
               "uri_template":"http:\/\/localhost:3000\/users\/dojo\/articles\/{article_id}\/edit.json",
               "rel":"edit",
               "params":["article_id"]
            }
         ],
         "options":["GET", "PUT", "DELETE"],
         "path_template":"\/users\/dojo\/articles\/{article_id}.json",
         "uri_template":"http:\/\/localhost:3000\/users\/dojo\/articles\/{article_id}.json",
         "params":["article_id"]
      }
   ]
}

Link Header Discovery Protocol

A discovery protocol based on link headers is added automatically by the middleware (controller changes are no longer required). This protocol is understood by path-to (enabling client APIs to be bootstrapped easily) and the link headers can be regarded as adding useful type information to resources.

Regular resources are given a link header that points to that resource’s ResourceTemplate metadata. That in turn is given a link header that points to the ResourceTemplates metadata for the entire application. The root resource has a link header that points to the ResourceTemplates metadata directly.

For further information on link headers, see the draft spec tools.ietf.org/id/draft-nottingham-http-link-header-06.txt and the link_header gem.

DATA STRUCTURES and FORMATS

Natural structure

The YAML and JSON representations appear as simple array and hash structures. Each resource is represented by a hash of attributes (one of which may be a list of child resources); the top level structure is an array of parentless resources.

Attributes:

name

A Rails-generated route name

rel

An indication of a child resource’s relationship to its parent

options

A list of HTTP methods supported by the resource

path_template

A template for the resource’s path, in the style of URI Template but as a relative path

uri_template

A template for the resource’s URI (generated only if the root URI is known at generation time)

params

A list of parameters required by path_template

optional_params

A list of optional parameters that may be incorporated by the path_template

Empty or blank attributes are omitted.

Note that only named routes are considered. Pre-Rails 2.3 “formatted routes” are explicitly excluded, and for Rails 2.3 onwards, "format" is the only entry likely to appear in the optional_parameters attribute.

XML

This follows the natural structure but with the following modifications:

  • A ResourceTemplate element for each resource template

  • A ResourceTemplates element for each list of resources (top level or subresources)

  • Params and OptionalParams elements for params and optional_params, each containing param elements

  • A single options element contains the applicable HTTP methods as a comma-separated list

Calls to parse_xml will at present result in NoMethodError exceptions being raised.

Samples

See test_rails_app/test/fixtures for sample outputs in each of the supported formats.

CUSTOMISATION

It is possible to customise the data collected from Rails, for example to hide sensitive routes:

DescribedRoutes::RailsRoutes.parsed_hook = lambda {|a| a.reject{|h| h["name"] =~ /^admin/}}

This hook operates on the raw “parsed” (Array/Hash) data before conversion to ResourceTemplate objects.

REQUIREMENTS:

Rails, for the Rake tasks and Rails controller. The ResourceTemplate class and its formats are however Rails-independent.

The addressable gem, 2.1.0 or above. This is now available as a regular gem install from Rubyforge.

Author

Mike Burrows (asplake), email [email protected], website positiveincline.com (see articles tagged described_routes)