Added Sinatra API explorer example app.
authorBob Aman <bobaman@google.com>
Sat, 9 Oct 2010 02:41:47 +0000 (02:41 +0000)
committerBob Aman <bobaman@google.com>
Sat, 9 Oct 2010 02:41:47 +0000 (02:41 +0000)
git-svn-id: https://google-api-ruby-client.googlecode.com/svn/trunk@52 c1d61fac-ed7f-fcc1-18f7-ff78120a04ef

examples/sinatra/explorer.rb [new file with mode: 0644]

diff --git a/examples/sinatra/explorer.rb b/examples/sinatra/explorer.rb
new file mode 100644 (file)
index 0000000..fb0d748
--- /dev/null
@@ -0,0 +1,509 @@
+#!/usr/bin/env ruby
+
+# INSTALL
+#   sudo gem install sinatra liquid
+# RUN
+#   ruby examples/sinatra/buzz_api.rb
+
+root_dir = File.expand_path("../../..", __FILE__)
+lib_dir = File.expand_path("./lib", root_dir)
+
+$LOAD_PATH.unshift(lib_dir)
+$LOAD_PATH.uniq!
+
+require 'rubygems'
+begin
+  require 'sinatra'
+  require 'liquid'
+  require 'signet/oauth_1/client'
+  require 'google/api_client'
+rescue LoadError
+  STDERR.puts "Missing dependencies."
+  STDERR.puts "sudo gem install sinatra liquid signet google-api-client"
+  exit(1)
+end
+
+CSS = <<-CSS
+/* http://meyerweb.com/eric/tools/css/reset/ */
+/* v1.0 | 20080212 */
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  outline: 0;
+  font-size: 100%;
+  vertical-align: baseline;
+  background: transparent;
+}
+body {
+  line-height: 1;
+}
+ol, ul {
+  list-style: none;
+}
+blockquote, q {
+  quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+  content: '';
+  content: none;
+}
+
+/* remember to define focus styles! */
+:focus {
+  outline: 0;
+}
+
+/* remember to highlight inserts somehow! */
+ins {
+  text-decoration: none;
+}
+del {
+  text-decoration: line-through;
+}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+/* End Reset */
+
+body {
+  color: #555555;
+  background-color: #ffffff;
+  font-family: 'Helvetica', 'Arial', sans-serif;
+  font-size: 18px;
+  line-height: 27px;
+  padding: 27px 72px;
+}
+p {
+  margin-bottom: 27px;
+}
+h1 {
+  font-style: normal;
+  font-variant: normal;
+  font-weight: normal;
+  font-family: 'Helvetica', 'Arial', sans-serif;
+  font-size: 36px;
+  line-height: 54px;
+  margin-bottom: 0px;
+}
+h2 {
+  font-style: normal;
+  font-variant: normal;
+  font-weight: normal;
+  font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
+  font-size: 14px;
+  line-height: 27px;
+  margin-top: 0px;
+  margin-bottom: 54px;
+  letter-spacing: 0.1em;
+  text-transform: none;
+  text-shadow: rgba(204, 204, 204, 0.75) 0px 1px 0px;
+}
+#output h3 {
+  font-style: normal;
+  font-variant: normal;
+  font-weight: bold;
+  font-size: 18px;
+  line-height: 27px;
+  margin: 27px 0px;
+}
+#output h3:first-child {
+  margin-top: 0px;
+}
+ul, ol, dl {
+  margin-bottom: 27px;
+}
+li {
+  margin: 0px 0px;
+}
+form {
+  float: left;
+  display: block;
+}
+form label, form input, form textarea {
+  font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
+  display: block;
+}
+form label {
+  margin-bottom: 5px;
+}
+form input {
+  width: 300px;
+  font-size: 14px;
+  padding: 5px;
+}
+form textarea {
+  height: 150px;
+  min-height: 150px;
+  width: 300px;
+  min-width: 300px;
+  max-width: 300px;
+}
+#output {
+  font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
+  display: inline-block;
+  margin-left: 27px;
+  padding: 27px;
+  border: 1px dotted #555555;
+  width: 1120px;
+  max-width: 100%;
+  min-height: 600px;
+}
+#output pre {
+  overflow: auto;
+}
+a {
+  color: #000000;
+  text-decoration: none;
+  border-bottom: 1px dotted rgba(112, 56, 56, 0.0);
+}
+a:hover {
+  -webkit-transition: all 0.3s linear;
+  color: #703838;
+  border-bottom: 1px dotted rgba(112, 56, 56, 1.0);
+}
+p a {
+  border-bottom: 1px dotted rgba(0, 0, 0, 1.0);
+}
+h1, h2 {
+  color: #000000;
+}
+h3, h4, h5, h6 {
+  color: #333333;
+}
+.block {
+  display: block;
+}
+button {
+  margin-bottom: 72px;
+  padding: 7px 11px;
+  font-size: 14px;
+}
+CSS
+
+JAVASCRIPT = <<-JAVASCRIPT
+  var uriTimeout = null;
+  $(document).ready(function () {
+    $('#output').hide();
+    var rpcName = $('#rpc-name').text().trim();
+    var serviceId = $('#service-id').text().trim();
+    var getParameters = function() {
+      var parameters = {};
+      var fields = $('.parameter').parents('li');
+      for (var i = 0; i < fields.length; i++) {
+        var input = $(fields[i]).find('input');
+        var label = $(fields[i]).find('label');
+        if (input.val() && input.val() != "") {
+          parameters[label.text()] = input.val();
+        }
+      }
+      return parameters;
+    }
+    var updateOutput = function (event) {
+      var request = $('#request').text().trim();
+      var response = $('#response').text().trim();
+      if (request != '' || response != '') {
+        $('#output').show();
+      } else {
+        $('#output').hide();        
+      }
+    }
+    var handleUri = function (event) {
+      updateOutput(event);
+      if (uriTimeout) {
+        clearTimeout(uriTimeout);
+      }
+      uriTimeout = setTimeout(function () {
+        $.ajax({
+          "url": "/template/" + serviceId + "/" + rpcName + "/",
+          "data": getParameters(),
+          "dataType": "text",
+          "ifModified": true,
+          "success": function (data, textStatus, xhr) {
+            updateOutput(event);
+            if (textStatus == 'success') {
+              $('#uri-template').html(data);
+              if (uriTimeout) {
+                clearTimeout(uriTimeout);
+              }
+            }
+          }
+        });
+      }, 350);
+    }
+    var getResponse = function (event) {
+      $.ajax({
+        "url": "/response/" + serviceId + "/" + rpcName + "/",
+        "type": "POST",
+        "data": getParameters(),
+        "dataType": "html",
+        "ifModified": true,
+        "success": function (data, textStatus, xhr) {
+          if (textStatus == 'success') {
+            $('#response').text(data);
+          }
+          updateOutput(event);
+        }
+      });
+    }
+    var getRequest = function (event) {
+      $.ajax({
+        "url": "/request/" + serviceId + "/" + rpcName + "/",
+        "type": "GET",
+        "data": getParameters(),
+        "dataType": "html",
+        "ifModified": true,
+        "success": function (data, textStatus, xhr) {
+          if (textStatus == 'success') {
+            $('#request').text(data);
+            updateOutput(event);
+            getResponse(event);
+          }
+        }
+      });
+    }
+    var transmit = function (event) {
+      $('#request').html('');
+      $('#response').html('');
+      handleUri(event);
+      updateOutput(event);
+      getRequest(event);
+    }
+    $('form').submit(function (event) { event.preventDefault(); });
+    $('button').click(transmit);
+    $('.parameter').keyup(handleUri);
+    $('.parameter').blur(handleUri);
+  });
+JAVASCRIPT
+
+def client
+  @client ||= Google::APIClient.new(
+    :service => 'buzz',
+    :authorization => Signet::OAuth1::Client.new(
+      :temporary_credential_uri =>
+        'https://www.google.com/accounts/OAuthGetRequestToken',
+      :authorization_uri =>
+        'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken',
+      :token_credential_uri =>
+        'https://www.google.com/accounts/OAuthGetAccessToken',
+      :client_credential_key => 'anonymous',
+      :client_credential_secret => 'anonymous'
+    )
+  )
+end
+
+def service(service_name, service_version)
+  unless service_version
+    service_version = client.latest_service(service_name).version
+  end
+  client.discovered_service(service_name, service_version)
+end
+
+get '/template/:service/:method/' do
+  service_name, service_version = params[:service].split("-", 2)
+  method = service(service_name, service_version).to_h[params[:method].to_s]
+  parameters = method.parameters.inject({}) do |accu, parameter|
+    accu[parameter] = params[parameter.to_sym] if params[parameter.to_sym]
+    accu
+  end
+  uri = Addressable::URI.parse(
+    method.uri_template.partial_expand(parameters).pattern
+  )
+  template_variables = method.uri_template.variables
+  query_parameters = method.normalize_parameters(parameters).reject do |k, v|
+    template_variables.include?(k)
+  end
+  if query_parameters.size > 0
+    uri.query_values = (uri.query_values || {}).merge(query_parameters)
+  end
+  # Normalization is necessary because of undesirable percent-escaping
+  # during URI template expansion
+  return uri.normalize.to_s.gsub('%7B', '{').gsub('%7D', '}')
+end
+
+get '/request/:service/:method/' do
+  service_name, service_version = params[:service].split("-", 2)
+  method = service(service_name, service_version).to_h[params[:method].to_s]
+  parameters = method.parameters.inject({}) do |accu, parameter|
+    accu[parameter] = params[parameter.to_sym] if params[parameter.to_sym]
+    accu
+  end
+  body = ''
+  request = client.generate_request(
+    method, parameters.merge("pp" => "1"), body, [], {:signed => false}
+  )
+  method, uri, headers, body = request
+  merged_body = StringIO.new
+  body.each do |chunk|
+    merged_body << chunk
+  end
+  merged_body.rewind
+  <<-REQUEST.strip
+#{method} #{uri} HTTP/1.1
+
+#{(headers.map { |k,v| "#{k}: #{v}" }).join('\n')}
+
+#{merged_body.string}
+REQUEST
+end
+
+post '/response/:service/:method/' do
+  require 'rack/utils'
+  service_name, service_version = params[:service].split("-", 2)
+  method = service(service_name, service_version).to_h[params[:method].to_s]
+  parameters = method.parameters.inject({}) do |accu, parameter|
+    accu[parameter] = params[parameter.to_sym] if params[parameter.to_sym]
+    accu
+  end
+  body = ''
+  response = client.execute(
+    method, parameters.merge("pp" => "1"), body, [], {:signed => false}
+  )
+  status, headers, body = response
+  status_message = Rack::Utils::HTTP_STATUS_CODES[status.to_i]
+  merged_body = StringIO.new
+  body.each do |chunk|
+    merged_body << chunk
+  end
+  merged_body.rewind
+  <<-RESPONSE.strip
+#{status} #{status_message}
+
+#{(headers.map { |k,v| "#{k}: #{v}" }).join("\n")}
+
+#{merged_body.string}
+RESPONSE
+end
+
+get '/explore/:service/' do
+  service_name, service_version = params[:service].split("-", 2)
+  service_version = service(service_name, service_version).version
+  variables = {
+    "css" => CSS,
+    "service_name" => service_name,
+    "service_version" => service_version,
+    "methods" => service(service_name, service_version).to_h.keys.sort
+  }
+  Liquid::Template.parse(<<-HTML).render(variables)
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>{{service_name}}</title>
+    <style type="text/css">
+      {{css}}
+    </style>
+  </head>
+  <body>
+    <h1>{{service_name}}</h1>
+    <ul>
+      {% for method in methods %}
+        <li>
+          <a href="/explore/{{service_name}}-{{service_version}}/{{method}}/">
+            {{method}}
+          </a>
+        </li>
+      {% endfor %}
+    </ul>
+  </body>
+</html>
+  HTML
+end
+
+get '/explore/:service/:method/' do
+  service_name, service_version = params[:service].split("-", 2)
+  service_version = service(service_name, service_version).version
+  method = service(service_name, service_version).to_h[params[:method].to_s]
+  variables = {
+    "css" => CSS,
+    "javascript" => JAVASCRIPT,
+    "http_method" => (method.description['httpMethod'] || 'GET'),
+    "service_name" => service_name,
+    "service_version" => service_version,
+    "method" => params[:method].to_s,
+    "required_parameters" =>
+      method.required_parameters,
+    "optional_parameters" =>
+      method.optional_parameters.sort,
+    "template" => method.uri_template.pattern
+  }
+  Liquid::Template.parse(<<-HTML).render(variables)
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>{{service_name}} - {{method}}</title>
+    <style type="text/css">
+      {{css}}
+    </style>
+    <script
+      src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"
+      type="text/javascript">
+    </script>
+    <script type="text/javascript">
+      {{javascript}}
+    </script>
+  </head>
+  <body>
+    <h3 id="service-id">
+      <a href="/explore/{{service_name}}-{{service_version}}/">
+        {{service_name}}-{{service_version}}
+      </a>
+    </h3>
+    <h1 id="rpc-name">{{method}}</h1>
+    <h2>{{http_method}} <span id="uri-template">{{template}}</span></h2>
+    <form>
+      <ul>
+        {% for parameter in required_parameters %}
+          <li>
+            <label for="param-{{parameter}}">{{parameter}}</label>
+            <input id="param-{{parameter}}" name="param-{{parameter}}"
+              class="parameter" type="text" />
+          </li>
+        {% endfor %}
+        {% for parameter in optional_parameters %}
+          <li>
+            <label for="param-{{parameter}}">{{parameter}}</label>
+            <input id="param-{{parameter}}" name="param-{{parameter}}"
+              class="parameter" type="text" />
+          </li>
+        {% endfor %}
+        {% if http_method != 'GET' %}
+        <li>
+          <label for="http-body">body</label>
+          <textarea id="http-body" name="http-body"></textarea>
+        </li>
+        {% endif %}
+      </ul>
+      <button>Transmit</button>
+    </form>
+    <div id="output">
+      <h3>Request</h3>
+      <pre id="request"></pre>
+      <h3>Response</h3>
+      <pre id="response"></pre>
+    </div>
+  </body>
+</html>
+  HTML
+end
+
+get '/favicon.ico' do
+  require 'httpadapter'
+  HTTPAdapter.transmit(
+    ['GET', 'http://www.google.com/favicon.ico', [], ['']],
+    HTTPAdapter::NetHTTPRequestAdapter
+  )
+end