Skip to content

Instantly share code, notes, and snippets.

@avdi
Created October 30, 2009 22:28
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save avdi/222779 to your computer and use it in GitHub Desktop.
Save avdi/222779 to your computer and use it in GitHub Desktop.
Rack::Stereoscope
require 'sinatra'
require 'json'
require 'addressable/uri'
require File.expand_path('rack_stereoscope.rb', File.dirname(__FILE__))
configure do
use Rack::Reloader;
use Rack::Lint;
use Rack::Stereoscope;
end
helpers do
def rel(path)
host = request.host
port = request.port
Addressable::URI.join("http://#{host}:#{port}", path).to_s
end
end
get '/' do
content_type 'application/json'
{
:explanation => "A fake API to demonstrate Stereoscope",
:list => [
"Item 1",
"Item 2",
"Item 3"
],
:assocations => {
"foo" => "bar",
"baz" => "buz"
},
:uri => rel('/foo'),
:uri_template => rel('/foo/{dir}?param1={param1}&param2={param2}'),
:tabular => [
{
:id => 1,
:name => "Plan 9 from Outer Space",
:date => "1959-07-01"
},
{
:id => 2,
:name => "Bride of the Monster",
:date => "1956-05-11"
},
{ :id => 3,
:name => "Glen or Glenda",
:date => "1953-01-01"
}
]
}.to_json
end
require 'markaby'
require 'json'
require 'nokogiri'
require 'rack/accept_media_types'
require 'addressable/template'
# Rack::Stereoscope - bringing a new dimension to your RESTful API
#
# Stereoscope is inspired by the idea that software should be explorable. Put
# stereoscope in front of your RESTful API, and you get an interactive,
# explorable HTML interface to your API for free. Use it to manually test your
# API from a browser. Use it to make your API self-documenting. Use it to
# quickly prototype new API features and get a visual feel for the data
# structures.
#
# Stereoscope is designed to be unobtrusive. It will not interpose itself unless
# the request asks for HTML (i.e. it comes from a browser). If the request
# requests no explicit content type; or if it requests a content-type other than
# HTML, Stereoscope stays out of the way.
#
# This middleware is especially well-suited to presenting APIs that are heavily
# hyperlinked (and if your API doesn't have hyperlinks, why
# not?[1]). Stereoscope does it's best to recognize URLs and make them
# clickable. What's more, Stereoscope supports URI Templates[2]. If your data
# includes URL templates such as the following:
#
# http://example.org/{foo}?bar={bar}
#
# Stereoscope will render a form which enables the user to experiment with
# different expansions of the URI template.
#
# Limitations:
# * Currently only supports JSON data
# * Only link-ifies fully-qualified URLs; relative URLs are not supported
# * Read-only exploration; no support for POSTs, PUTs, or DELETEs.
#
# [1] http://www.theamazingrando.com/blog/?p=107
# [2] http://bitworking.org/projects/URI-Templates/
module Rack
class Stereoscope
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if Rack::AcceptMediaTypes.new(env['HTTP_ACCEPT']).include?('text/html')
status, headers, body = @app.call(env)
if request.path == '/__stereoscope_expand_template__'
expand_template(request)
else
present_data(request, status, headers, body)
end
else
@app.call(env)
end
end
def present_data(request, status, headers, body)
response = Rack::Response.new("", status, headers)
response.write(build_page(body, request, response))
response['Content-Type'] = 'text/html'
response.finish
end
def expand_template(request)
template = Addressable::Template.new(request['__template__'])
url = template.expand(request.params)
response = Rack::Response.new
response.redirect(url.to_s)
response.finish
end
def build_page(content, request, response)
this = self
mab = Markaby::Builder.new
mab.html do
head do
title request.path
end
body do
h1 "#{response.status} #{request.url}"
if !content.to_s.empty?
h2 "Response:"
case response.content_type
when 'application/json' then
div do
this.data_to_html(JSON.parse(content.join), mab)
end
when 'text/plain' then
p content.join
else
text Nokogiri::HTML(content.join).css('body').inner_html
end
else
p "(No content)"
end
h2 "Raw:"
tt do
raw_content = case response.content_type
when 'application/json'
JSON.pretty_generate(JSON.parse(content.join))
else
content.join
end
pre raw_content
end
end
end
mab.to_s
end
def data_to_html(data, builder)
this = self
case data
when Hash
builder.dl do
data.each_pair do |key, value|
dt do
this.data_to_html(key, builder)
end
dd do
this.data_to_html(value, builder)
end
end
end
when Array
if tabular?(data)
table_to_html(data, builder)
else
list_to_html(data, builder)
end
when String
if url?(data)
if url_template?(data)
template_to_html(data, builder)
else
url_to_html(data, builder)
end
else
builder.div do
data.split("\n").each do |line|
builder.span line
builder.br
end
end
end
else
builder.span do data end
end
end
def url?(text)
Addressable::URI.parse(text.to_s).ip_based?
end
def url_template?(text)
!Addressable::Template.new(text.to_s).variables.empty?
end
def tabular?(data)
data.kind_of?(Array) &&
data.all?{|e| e.kind_of?(Hash)} &&
data[1..-1].all?{|e| e.keys == data.first.keys}
end
def url_to_html(url, builder)
builder.a(url.to_s, :href => url.to_s)
end
def template_to_html(text, builder)
template = Addressable::Template.new(text)
builder.div(:class => 'url-template-form') do
p text
form(:method => 'GET', :action => '/__stereoscope_expand_template__') do
input(:type => 'hidden', :name => '__template__', :value => text)
template.variables.each do |variable|
div(:class => 'url-template-variable') do
label do
text "#{variable}: "
input(:type => 'text', :name => variable)
end
end
end
input(:type => 'submit')
end
end
end
def list_to_html(data, builder)
this = self
builder.ol do
data.each do |value|
li do
this.data_to_html(value, builder)
end
end
end
end
def table_to_html(data, builder)
this = self
builder.table do
headers = data.first.keys
thead do
headers.each do |header|
th do
this.data_to_html(header, builder)
end
end
end
tbody do
data.each do |row|
tr do
row.each do |key, value|
td do
this.data_to_html(value, builder)
end
end
end
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment