481101e58a3562dd18e24c94bf8b3ea55744514e
[arvados.git] / examples / sinatra / explorer.rb
1 #!/usr/bin/env ruby
2
3 # INSTALL
4 #   sudo gem install sinatra liquid
5 # RUN
6 #   ruby examples/sinatra/buzz_api.rb
7
8 root_dir = File.expand_path("../../..", __FILE__)
9 lib_dir = File.expand_path("./lib", root_dir)
10
11 $LOAD_PATH.unshift(lib_dir)
12 $LOAD_PATH.uniq!
13
14 require 'rubygems'
15 begin
16   gem 'rack', '= 1.2.0'
17   require 'rack'
18 rescue LoadError
19   STDERR.puts "Missing dependencies."
20   STDERR.puts "sudo gem install rack -v 1.2.0"
21   exit(1)
22 end
23 begin
24   require 'sinatra'
25   require 'liquid'
26   require 'signet/oauth_1/client'
27   require 'google/api_client'
28 rescue LoadError
29   STDERR.puts "Missing dependencies."
30   STDERR.puts "sudo gem install sinatra liquid signet google-api-client"
31   exit(1)
32 end
33
34 enable :sessions
35
36 CSS = <<-CSS
37 /* http://meyerweb.com/eric/tools/css/reset/ */
38 /* v1.0 | 20080212 */
39
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,
45 b, u, i, center,
46 dl, dt, dd, ol, ul, li,
47 fieldset, form, label, legend,
48 table, caption, tbody, tfoot, thead, tr, th, td {
49   margin: 0;
50   padding: 0;
51   border: 0;
52   outline: 0;
53   font-size: 100%;
54   vertical-align: baseline;
55   background: transparent;
56 }
57 body {
58   line-height: 1;
59 }
60 ol, ul {
61   list-style: none;
62 }
63 blockquote, q {
64   quotes: none;
65 }
66 blockquote:before, blockquote:after,
67 q:before, q:after {
68   content: '';
69   content: none;
70 }
71
72 /* remember to define focus styles! */
73 :focus {
74   outline: 0;
75 }
76
77 /* remember to highlight inserts somehow! */
78 ins {
79   text-decoration: none;
80 }
81 del {
82   text-decoration: line-through;
83 }
84
85 /* tables still need 'cellspacing="0"' in the markup */
86 table {
87   border-collapse: collapse;
88   border-spacing: 0;
89 }
90
91 /* End Reset */
92
93 body {
94   color: #555555;
95   background-color: #ffffff;
96   font-family: 'Helvetica', 'Arial', sans-serif;
97   font-size: 18px;
98   line-height: 27px;
99   padding: 27px 72px;
100 }
101 p {
102   margin-bottom: 27px;
103 }
104 h1 {
105   font-style: normal;
106   font-variant: normal;
107   font-weight: normal;
108   font-family: 'Helvetica', 'Arial', sans-serif;
109   font-size: 36px;
110   line-height: 54px;
111   margin-bottom: 0px;
112 }
113 h2 {
114   font-style: normal;
115   font-variant: normal;
116   font-weight: normal;
117   font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
118   font-size: 14px;
119   line-height: 27px;
120   margin-top: 0px;
121   margin-bottom: 54px;
122   letter-spacing: 0.1em;
123   text-transform: none;
124   text-shadow: rgba(204, 204, 204, 0.75) 0px 1px 0px;
125 }
126 #output h3 {
127   font-style: normal;
128   font-variant: normal;
129   font-weight: bold;
130   font-size: 18px;
131   line-height: 27px;
132   margin: 27px 0px;
133 }
134 #output h3:first-child {
135   margin-top: 0px;
136 }
137 ul, ol, dl {
138   margin-bottom: 27px;
139 }
140 li {
141   margin: 0px 0px;
142 }
143 form {
144   float: left;
145   display: block;
146 }
147 form label, form input, form textarea {
148   font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
149   display: block;
150 }
151 form label {
152   margin-bottom: 5px;
153 }
154 form input {
155   width: 300px;
156   font-size: 14px;
157   padding: 5px;
158 }
159 form textarea {
160   height: 150px;
161   min-height: 150px;
162   width: 300px;
163   min-width: 300px;
164   max-width: 300px;
165 }
166 #output {
167   font-family: 'Monaco', 'Andale Mono', 'Consolas', 'Inconsolata', 'Courier New', monospace;
168   display: inline-block;
169   margin-left: 27px;
170   padding: 27px;
171   border: 1px dotted #555555;
172   width: 1120px;
173   max-width: 100%;
174   min-height: 600px;
175 }
176 #output pre {
177   overflow: auto;
178 }
179 a {
180   color: #000000;
181   text-decoration: none;
182   border-bottom: 1px dotted rgba(112, 56, 56, 0.0);
183 }
184 a:hover {
185   -webkit-transition: all 0.3s linear;
186   color: #703838;
187   border-bottom: 1px dotted rgba(112, 56, 56, 1.0);
188 }
189 p a {
190   border-bottom: 1px dotted rgba(0, 0, 0, 1.0);
191 }
192 h1, h2 {
193   color: #000000;
194 }
195 h3, h4, h5, h6 {
196   color: #333333;
197 }
198 .block {
199   display: block;
200 }
201 button {
202   margin-bottom: 72px;
203   padding: 7px 11px;
204   font-size: 14px;
205 }
206 CSS
207
208 JAVASCRIPT = <<-JAVASCRIPT
209   var uriTimeout = null;
210   $(document).ready(function () {
211     $('#output').hide();
212     var rpcName = $('#rpc-name').text().trim();
213     var serviceId = $('#service-id').text().trim();
214     var getParameters = function() {
215       var parameters = {};
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();
222         }
223       }
224       return parameters;
225     }
226     var updateOutput = function (event) {
227       var request = $('#request').text().trim();
228       var response = $('#response').text().trim();
229       if (request != '' || response != '') {
230         $('#output').show();
231       } else {
232         $('#output').hide();        
233       }
234     }
235     var handleUri = function (event) {
236       updateOutput(event);
237       if (uriTimeout) {
238         clearTimeout(uriTimeout);
239       }
240       uriTimeout = setTimeout(function () {
241         $.ajax({
242           "url": "/template/" + serviceId + "/" + rpcName + "/",
243           "data": getParameters(),
244           "dataType": "text",
245           "ifModified": true,
246           "success": function (data, textStatus, xhr) {
247             updateOutput(event);
248             if (textStatus == 'success') {
249               $('#uri-template').html(data);
250               if (uriTimeout) {
251                 clearTimeout(uriTimeout);
252               }
253             }
254           }
255         });
256       }, 350);
257     }
258     var getResponse = function (event) {
259       $.ajax({
260         "url": "/response/" + serviceId + "/" + rpcName + "/",
261         "type": "POST",
262         "data": getParameters(),
263         "dataType": "html",
264         "ifModified": true,
265         "success": function (data, textStatus, xhr) {
266           if (textStatus == 'success') {
267             $('#response').text(data);
268           }
269           updateOutput(event);
270         }
271       });
272     }
273     var getRequest = function (event) {
274       $.ajax({
275         "url": "/request/" + serviceId + "/" + rpcName + "/",
276         "type": "GET",
277         "data": getParameters(),
278         "dataType": "html",
279         "ifModified": true,
280         "success": function (data, textStatus, xhr) {
281           if (textStatus == 'success') {
282             $('#request').text(data);
283             updateOutput(event);
284             getResponse(event);
285           }
286         }
287       });
288     }
289     var transmit = function (event) {
290       $('#request').html('');
291       $('#response').html('');
292       handleUri(event);
293       updateOutput(event);
294       getRequest(event);
295     }
296     $('form').submit(function (event) { event.preventDefault(); });
297     $('button').click(transmit);
298     $('.parameter').keyup(handleUri);
299     $('.parameter').blur(handleUri);
300   });
301 JAVASCRIPT
302
303 def client
304   @client ||= Google::APIClient.new(
305     :service => 'buzz',
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'
315     )
316   )
317 end
318
319 def service(service_name, service_version)
320   unless service_version
321     service_version = client.latest_service_version(service_name).version
322   end
323   client.discovered_service(service_name, service_version)
324 end
325
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]
331     accu
332   end
333   uri = Addressable::URI.parse(
334     method.uri_template.partial_expand(parameters).pattern
335   )
336   template_variables = method.uri_template.variables
337   query_parameters = method.normalize_parameters(parameters).reject do |k, v|
338     template_variables.include?(k)
339   end
340   if query_parameters.size > 0
341     uri.query_values = (uri.query_values || {}).merge(query_parameters)
342   end
343   # Normalization is necessary because of undesirable percent-escaping
344   # during URI template expansion
345   return uri.normalize.to_s.gsub('%7B', '{').gsub('%7D', '}')
346 end
347
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]
353     accu
354   end
355   body = ''
356   request = client.generate_request(
357     method, parameters.merge("pp" => "1"), body, [], {:signed => false}
358   )
359   method, uri, headers, body = request
360   merged_body = StringIO.new
361   body.each do |chunk|
362     merged_body << chunk
363   end
364   merged_body.rewind
365   <<-REQUEST.strip
366 #{method} #{uri} HTTP/1.1
367
368 #{(headers.map { |k,v| "#{k}: #{v}" }).join('\n')}
369
370 #{merged_body.string}
371 REQUEST
372 end
373
374 post '/response/:service/:method/' do
375   require 'rack/utils'
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]
380     accu
381   end
382   body = ''
383   response = client.execute(
384     method, parameters.merge("pp" => "1"), body, [], {:signed => false}
385   )
386   status, headers, body = response
387   status_message = Rack::Utils::HTTP_STATUS_CODES[status.to_i]
388   merged_body = StringIO.new
389   body.each do |chunk|
390     merged_body << chunk
391   end
392   merged_body.rewind
393   <<-RESPONSE.strip
394 #{status} #{status_message}
395
396 #{(headers.map { |k,v| "#{k}: #{v}" }).join("\n")}
397
398 #{merged_body.string}
399 RESPONSE
400 end
401
402 get '/explore/:service/' do
403   service_name, service_version = params[:service].split("-", 2)
404   service_version = service(service_name, service_version).version
405   variables = {
406     "css" => CSS,
407     "service_name" => service_name,
408     "service_version" => service_version,
409     "methods" => service(service_name, service_version).to_h.keys.sort
410   }
411   Liquid::Template.parse(<<-HTML).render(variables)
412 <!DOCTYPE html>
413 <html>
414   <head>
415     <title>{{service_name}}</title>
416     <style type="text/css">
417       {{css}}
418     </style>
419   </head>
420   <body>
421     <h1>{{service_name}}</h1>
422     <ul>
423       {% for method in methods %}
424         <li>
425           <a href="/explore/{{service_name}}-{{service_version}}/{{method}}/">
426             {{method}}
427           </a>
428         </li>
429       {% endfor %}
430     </ul>
431   </body>
432 </html>
433   HTML
434 end
435
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]
440   variables = {
441     "css" => CSS,
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
452   }
453   Liquid::Template.parse(<<-HTML).render(variables)
454 <!DOCTYPE html>
455 <html>
456   <head>
457     <title>{{service_name}} - {{method}}</title>
458     <style type="text/css">
459       {{css}}
460     </style>
461     <script
462       src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"
463       type="text/javascript">
464     </script>
465     <script type="text/javascript">
466       {{javascript}}
467     </script>
468   </head>
469   <body>
470     <h3 id="service-id">
471       <a href="/explore/{{service_name}}-{{service_version}}/">
472         {{service_name}}-{{service_version}}
473       </a>
474     </h3>
475     <h1 id="rpc-name">{{method}}</h1>
476     <h2>{{http_method}} <span id="uri-template">{{template}}</span></h2>
477     <form>
478       <ul>
479         {% for parameter in required_parameters %}
480           <li>
481             <label for="param-{{parameter}}">{{parameter}}</label>
482             <input id="param-{{parameter}}" name="param-{{parameter}}"
483               class="parameter" type="text" />
484           </li>
485         {% endfor %}
486         {% for parameter in optional_parameters %}
487           <li>
488             <label for="param-{{parameter}}">{{parameter}}</label>
489             <input id="param-{{parameter}}" name="param-{{parameter}}"
490               class="parameter" type="text" />
491           </li>
492         {% endfor %}
493         {% if http_method != 'GET' %}
494         <li>
495           <label for="http-body">body</label>
496           <textarea id="http-body" name="http-body"></textarea>
497         </li>
498         {% endif %}
499       </ul>
500       <button>Transmit</button>
501     </form>
502     <div id="output">
503       <h3>Request</h3>
504       <pre id="request"></pre>
505       <h3>Response</h3>
506       <pre id="response"></pre>
507     </div>
508   </body>
509 </html>
510   HTML
511 end
512
513 get '/favicon.ico' do
514   require 'httpadapter'
515   HTTPAdapter.transmit(
516     ['GET', 'http://www.google.com/favicon.ico', [], ['']],
517     HTTPAdapter::NetHTTPRequestAdapter
518   )
519 end