4 # sudo gem install sinatra liquid
6 # ruby examples/sinatra/buzz_api.rb
8 root_dir = File.expand_path("../../..", __FILE__)
9 lib_dir = File.expand_path("./lib", root_dir)
11 $LOAD_PATH.unshift(lib_dir)
19 STDERR.puts "Missing dependencies."
20 STDERR.puts "sudo gem install rack -v 1.2.0"
26 require 'signet/oauth_1/client'
27 require 'google/api_client'
29 STDERR.puts "Missing dependencies."
30 STDERR.puts "sudo gem install sinatra liquid signet google-api-client"
37 /* http://meyerweb.com/eric/tools/css/reset/ */
40 html, body, div, span, applet, object, iframe,
41 h1, h2, h3, h4, h5, h6, p, blockquote, pre,
42 a, abbr, acronym, address, big, cite, code,
43 del, dfn, em, font, img, ins, kbd, q, s, samp,
44 small, strike, strong, sub, sup, tt, var,
46 dl, dt, dd, ol, ul, li,
47 fieldset, form, label, legend,
48 table, caption, tbody, tfoot, thead, tr, th, td {
54 vertical-align: baseline;
55 background: transparent;
66 blockquote:before, blockquote:after,
72 /* remember to define focus styles! */
77 /* remember to highlight inserts somehow! */
79 text-decoration: none;
82 text-decoration: line-through;
85 /* tables still need 'cellspacing="0"' in the markup */
87 border-collapse: collapse;
95 background-color: #ffffff;
96 font-family: 'Helvetica', 'Arial', sans-serif;
106 font-variant: normal;
108 font-family: 'Helvetica', 'Arial', sans-serif;
115 font-variant: normal;
117 font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
122 letter-spacing: 0.1em;
123 text-transform: none;
124 text-shadow: rgba(204, 204, 204, 0.75) 0px 1px 0px;
128 font-variant: normal;
134 #output h3:first-child {
147 form label, form input, form textarea {
148 font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
167 font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
168 display: inline-block;
171 border: 1px dotted #555555;
181 text-decoration: none;
182 border-bottom: 1px dotted rgba(112, 56, 56, 0.0);
185 -webkit-transition: all 0.3s linear;
187 border-bottom: 1px dotted rgba(112, 56, 56, 1.0);
190 border-bottom: 1px dotted rgba(0, 0, 0, 1.0);
208 JAVASCRIPT = <<-JAVASCRIPT
209 var uriTimeout = null;
210 $(document).ready(function () {
212 var rpcName = $('#rpc-name').text().trim();
213 var serviceId = $('#service-id').text().trim();
214 var getParameters = function() {
216 var fields = $('.parameter').parents('li');
217 for (var i = 0; i < fields.length; i++) {
218 var input = $(fields[i]).find('input');
219 var label = $(fields[i]).find('label');
220 if (input.val() && input.val() != "") {
221 parameters[label.text()] = input.val();
226 var updateOutput = function (event) {
227 var request = $('#request').text().trim();
228 var response = $('#response').text().trim();
229 if (request != '' || response != '') {
235 var handleUri = function (event) {
238 clearTimeout(uriTimeout);
240 uriTimeout = setTimeout(function () {
242 "url": "/template/" + serviceId + "/" + rpcName + "/",
243 "data": getParameters(),
246 "success": function (data, textStatus, xhr) {
248 if (textStatus == 'success') {
249 $('#uri-template').html(data);
251 clearTimeout(uriTimeout);
258 var getResponse = function (event) {
260 "url": "/response/" + serviceId + "/" + rpcName + "/",
262 "data": getParameters(),
265 "success": function (data, textStatus, xhr) {
266 if (textStatus == 'success') {
267 $('#response').text(data);
273 var getRequest = function (event) {
275 "url": "/request/" + serviceId + "/" + rpcName + "/",
277 "data": getParameters(),
280 "success": function (data, textStatus, xhr) {
281 if (textStatus == 'success') {
282 $('#request').text(data);
289 var transmit = function (event) {
290 $('#request').html('');
291 $('#response').html('');
296 $('form').submit(function (event) { event.preventDefault(); });
297 $('button').click(transmit);
298 $('.parameter').keyup(handleUri);
299 $('.parameter').blur(handleUri);
304 @client ||= Google::APIClient.new(
306 :authorization => Signet::OAuth1::Client.new(
307 :temporary_credential_uri =>
308 'https://www.google.com/accounts/OAuthGetRequestToken',
309 :authorization_uri =>
310 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken',
311 :token_credential_uri =>
312 'https://www.google.com/accounts/OAuthGetAccessToken',
313 :client_credential_key => 'anonymous',
314 :client_credential_secret => 'anonymous'
319 def service(service_name, service_version)
320 unless service_version
321 service_version = client.latest_service_version(service_name).version
323 client.discovered_service(service_name, service_version)
326 get '/template/:service/:method/' do
327 service_name, service_version = params[:service].split("-", 2)
328 method = service(service_name, service_version).to_h[params[:method].to_s]
329 parameters = method.parameters.inject({}) do |accu, parameter|
330 accu[parameter] = params[parameter.to_sym] if params[parameter.to_sym]
333 uri = Addressable::URI.parse(
334 method.uri_template.partial_expand(parameters).pattern
336 template_variables = method.uri_template.variables
337 query_parameters = method.normalize_parameters(parameters).reject do |k, v|
338 template_variables.include?(k)
340 if query_parameters.size > 0
341 uri.query_values = (uri.query_values || {}).merge(query_parameters)
343 # Normalization is necessary because of undesirable percent-escaping
344 # during URI template expansion
345 return uri.normalize.to_s.gsub('%7B', '{').gsub('%7D', '}')
348 get '/request/:service/:method/' do
349 service_name, service_version = params[:service].split("-", 2)
350 method = service(service_name, service_version).to_h[params[:method].to_s]
351 parameters = method.parameters.inject({}) do |accu, parameter|
352 accu[parameter] = params[parameter.to_sym] if params[parameter.to_sym]
356 request = client.generate_request(
357 method, parameters.merge("pp" => "1"), body, [], {:signed => false}
359 method, uri, headers, body = request
360 merged_body = StringIO.new
366 #{method} #{uri} HTTP/1.1
368 #{(headers.map { |k,v| "#{k}: #{v}" }).join('\n')}
370 #{merged_body.string}
374 post '/response/:service/:method/' do
376 service_name, service_version = params[:service].split("-", 2)
377 method = service(service_name, service_version).to_h[params[:method].to_s]
378 parameters = method.parameters.inject({}) do |accu, parameter|
379 accu[parameter] = params[parameter.to_sym] if params[parameter.to_sym]
383 response = client.execute(
384 method, parameters.merge("pp" => "1"), body, [], {:signed => false}
386 status, headers, body = response
387 status_message = Rack::Utils::HTTP_STATUS_CODES[status.to_i]
388 merged_body = StringIO.new
394 #{status} #{status_message}
396 #{(headers.map { |k,v| "#{k}: #{v}" }).join("\n")}
398 #{merged_body.string}
402 get '/explore/:service/' do
403 service_name, service_version = params[:service].split("-", 2)
404 service_version = service(service_name, service_version).version
407 "service_name" => service_name,
408 "service_version" => service_version,
409 "methods" => service(service_name, service_version).to_h.keys.sort
411 Liquid::Template.parse(<<-HTML).render(variables)
415 <title>{{service_name}}</title>
416 <style type="text/css">
421 <h1>{{service_name}}</h1>
423 {% for method in methods %}
425 <a href="/explore/{{service_name}}-{{service_version}}/{{method}}/">
436 get '/explore/:service/:method/' do
437 service_name, service_version = params[:service].split("-", 2)
438 service_version = service(service_name, service_version).version
439 method = service(service_name, service_version).to_h[params[:method].to_s]
442 "javascript" => JAVASCRIPT,
443 "http_method" => (method.description['httpMethod'] || 'GET'),
444 "service_name" => service_name,
445 "service_version" => service_version,
446 "method" => params[:method].to_s,
447 "required_parameters" =>
448 method.required_parameters,
449 "optional_parameters" =>
450 method.optional_parameters.sort,
451 "template" => method.uri_template.pattern
453 Liquid::Template.parse(<<-HTML).render(variables)
457 <title>{{service_name}} - {{method}}</title>
458 <style type="text/css">
462 src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"
463 type="text/javascript">
465 <script type="text/javascript">
471 <a href="/explore/{{service_name}}-{{service_version}}/">
472 {{service_name}}-{{service_version}}
475 <h1 id="rpc-name">{{method}}</h1>
476 <h2>{{http_method}} <span id="uri-template">{{template}}</span></h2>
479 {% for parameter in required_parameters %}
481 <label for="param-{{parameter}}">{{parameter}}</label>
482 <input id="param-{{parameter}}" name="param-{{parameter}}"
483 class="parameter" type="text" />
486 {% for parameter in optional_parameters %}
488 <label for="param-{{parameter}}">{{parameter}}</label>
489 <input id="param-{{parameter}}" name="param-{{parameter}}"
490 class="parameter" type="text" />
493 {% if http_method != 'GET' %}
495 <label for="http-body">body</label>
496 <textarea id="http-body" name="http-body"></textarea>
500 <button>Transmit</button>
504 <pre id="request"></pre>
506 <pre id="response"></pre>
513 get '/favicon.ico' do
514 require 'httpadapter'
515 HTTPAdapter.transmit(
516 ['GET', 'http://www.google.com/favicon.ico', [], ['']],
517 HTTPAdapter::NetHTTPRequestAdapter