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