haveapi 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +15 -0
  3. data/CHANGELOG +15 -0
  4. data/README.md +66 -47
  5. data/doc/create-client.md +14 -5
  6. data/doc/json-schema.erb +16 -2
  7. data/doc/protocol.md +25 -3
  8. data/doc/protocol.plantuml +14 -8
  9. data/haveapi.gemspec +4 -2
  10. data/lib/haveapi.rb +5 -3
  11. data/lib/haveapi/action.rb +34 -6
  12. data/lib/haveapi/action_state.rb +92 -0
  13. data/lib/haveapi/authentication/basic/provider.rb +7 -0
  14. data/lib/haveapi/authentication/token/provider.rb +5 -0
  15. data/lib/haveapi/client_example.rb +83 -0
  16. data/lib/haveapi/client_examples/curl.rb +86 -0
  17. data/lib/haveapi/client_examples/fs_client.rb +116 -0
  18. data/lib/haveapi/client_examples/http.rb +91 -0
  19. data/lib/haveapi/client_examples/js_client.rb +149 -0
  20. data/lib/haveapi/client_examples/php_client.rb +122 -0
  21. data/lib/haveapi/client_examples/ruby_cli.rb +117 -0
  22. data/lib/haveapi/client_examples/ruby_client.rb +106 -0
  23. data/lib/haveapi/context.rb +3 -2
  24. data/lib/haveapi/example.rb +29 -2
  25. data/lib/haveapi/extensions/action_exceptions.rb +2 -2
  26. data/lib/haveapi/extensions/base.rb +1 -1
  27. data/lib/haveapi/extensions/exception_mailer.rb +339 -0
  28. data/lib/haveapi/hooks.rb +1 -1
  29. data/lib/haveapi/parameters/typed.rb +5 -3
  30. data/lib/haveapi/public/css/highlight.css +99 -0
  31. data/lib/haveapi/public/doc/protocol.png +0 -0
  32. data/lib/haveapi/public/js/highlight.pack.js +2 -0
  33. data/lib/haveapi/public/js/highlighter.js +9 -0
  34. data/lib/haveapi/public/js/main.js +32 -0
  35. data/lib/haveapi/public/js/nojs-tabs.js +196 -0
  36. data/lib/haveapi/resources/action_state.rb +196 -0
  37. data/lib/haveapi/server.rb +96 -27
  38. data/lib/haveapi/version.rb +2 -2
  39. data/lib/haveapi/views/main_layout.erb +14 -0
  40. data/lib/haveapi/views/version_page.erb +187 -13
  41. data/lib/haveapi/views/version_sidebar.erb +37 -3
  42. metadata +49 -5
@@ -1,10 +1,12 @@
1
1
  require 'erb'
2
2
  require 'redcarpet'
3
+ require 'cgi'
3
4
 
4
5
  module HaveAPI
5
6
  class Server
6
7
  attr_reader :root, :routes, :module_name, :auth_chain, :versions, :default_version,
7
8
  :extensions
9
+ attr_accessor :action_state
8
10
 
9
11
  include Hookable
10
12
 
@@ -16,6 +18,17 @@ module HaveAPI
16
18
  current_user: 'object returned by the authentication backend',
17
19
  }
18
20
 
21
+ has_hook :description_exception,
22
+ desc: 'Called when an exception occurs when building self-description',
23
+ args: {
24
+ context: 'HaveAPI::Context',
25
+ exception: 'exception instance',
26
+ },
27
+ ret: {
28
+ http_status: 'HTTP status code to send to client',
29
+ message: 'error message sent to the client',
30
+ }
31
+
19
32
  module ServerHelpers
20
33
  def authenticate!(v)
21
34
  require_auth! unless authenticated?(v)
@@ -74,6 +87,26 @@ module HaveAPI
74
87
  markdown :"../../../doc/#{file}"
75
88
  end
76
89
 
90
+ def base_url
91
+ "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}"
92
+ end
93
+
94
+ def host
95
+ request.env['HTTP_HOST'].split(':').first
96
+ end
97
+
98
+ def urlescape(v)
99
+ CGI.escape(v)
100
+ end
101
+
102
+ def sort_hash(hash)
103
+ hash.sort { |a, b| a[0] <=> b[0] }
104
+ end
105
+
106
+ def api_version
107
+ @v
108
+ end
109
+
77
110
  def version
78
111
  HaveAPI::VERSION
79
112
  end
@@ -122,9 +155,12 @@ module HaveAPI
122
155
  set :views, settings.root + '/views'
123
156
  set :public_folder, settings.root + '/public'
124
157
  set :bind, '0.0.0.0'
125
- set :dump_errors, true
126
- set :raise_errors, true
127
- set :show_exceptions, false
158
+
159
+ if settings.development?
160
+ set :dump_errors, true
161
+ set :raise_errors, true
162
+ set :show_exceptions, false
163
+ end
128
164
 
129
165
  helpers ServerHelpers
130
166
 
@@ -212,11 +248,12 @@ module HaveAPI
212
248
 
213
249
  @sinatra.get "#{@root}doc/readme" do
214
250
  content_type 'text/html'
251
+
215
252
  erb :main_layout do
216
253
  GitHub::Markdown.render(File.new(settings.views + '/../../../README.md').read)
217
254
  end
218
255
  end
219
-
256
+
220
257
  @sinatra.get "#{@root}doc/json-schema" do
221
258
  content_type 'text/html'
222
259
  erb :doc_layout, layout: :main_layout do
@@ -255,7 +292,7 @@ module HaveAPI
255
292
  @auth_chain << HaveAPI.default_authenticate if @auth_chain.empty?
256
293
  @auth_chain.setup(@versions)
257
294
 
258
- @extensions.each { |e| e.enabled }
295
+ @extensions.each { |e| e.enabled(self) }
259
296
 
260
297
  # Mount default version first
261
298
  mount_version(@root, @default_version)
@@ -279,6 +316,7 @@ module HaveAPI
279
316
  user: current_user,
280
317
  params: params
281
318
  ))
319
+
282
320
  content_type 'text/html'
283
321
  erb :doc_layout, layout: :main_layout do
284
322
  @content = erb :version_page
@@ -298,10 +336,20 @@ module HaveAPI
298
336
  )))
299
337
  end
300
338
 
339
+ # Register blocking resource
301
340
  HaveAPI.get_version_resources(@module_name, v).each do |resource|
302
341
  mount_resource(prefix, v, resource, @routes[v][:resources])
303
342
  end
304
343
 
344
+ if action_state
345
+ mount_resource(
346
+ prefix,
347
+ v,
348
+ HaveAPI::Resources::ActionState,
349
+ @routes[v][:resources]
350
+ )
351
+ end
352
+
305
353
  validate_resources(@routes[v][:resources])
306
354
  end
307
355
 
@@ -350,7 +398,11 @@ module HaveAPI
350
398
 
351
399
  def mount_action(v, route)
352
400
  @sinatra.method(route.http_method).call(route.url) do
353
- authenticate!(v) if route.action.auth
401
+ if route.action.auth
402
+ authenticate!(v)
403
+ else
404
+ authenticated?(v)
405
+ end
354
406
 
355
407
  request.body.rewind
356
408
 
@@ -370,6 +422,7 @@ module HaveAPI
370
422
  action = route.action.new(request, v, params, body, Context.new(
371
423
  settings.api_server,
372
424
  version: v,
425
+ request: self,
373
426
  action: route.action,
374
427
  url: route.url,
375
428
  params: params,
@@ -381,15 +434,19 @@ module HaveAPI
381
434
  report_error(403, {}, 'Access denied. Insufficient permissions.')
382
435
  end
383
436
 
384
- status, reply, errors = action.safe_exec
437
+ status, reply, errors, http_status = action.safe_exec
438
+ @halted = true
385
439
 
386
- @formatter.format(
387
- status,
388
- status ? reply : nil,
389
- !status ? reply : nil,
390
- errors,
391
- version: false
392
- )
440
+ [
441
+ http_status || 200,
442
+ @formatter.format(
443
+ status,
444
+ status ? reply : nil,
445
+ !status ? reply : nil,
446
+ errors,
447
+ version: false
448
+ ),
449
+ ]
393
450
  end
394
451
 
395
452
  @sinatra.options route.url do |*args|
@@ -398,26 +455,38 @@ module HaveAPI
398
455
 
399
456
  pass if params[:method] && params[:method] != route_method
400
457
 
401
- authenticate!(v) if route.action.auth
458
+ if route.action.auth
459
+ authenticate!(v)
460
+ else
461
+ authenticated?(v)
462
+ end
463
+
464
+ ctx = Context.new(
465
+ settings.api_server,
466
+ version: v,
467
+ request: self,
468
+ action: route.action,
469
+ url: route.url,
470
+ args: args,
471
+ params: params,
472
+ user: current_user,
473
+ endpoint: true
474
+ )
402
475
 
403
476
  begin
404
- desc = route.action.describe(Context.new(
405
- settings.api_server,
406
- version: v,
407
- action: route.action,
408
- url: route.url,
409
- args: args,
410
- params: params,
411
- user: current_user,
412
- endpoint: true
413
- ))
477
+ desc = route.action.describe(ctx)
414
478
 
415
479
  unless desc
416
480
  report_error(403, {}, 'Access denied. Insufficient permissions.')
417
481
  end
418
482
 
419
- rescue ActiveRecord::RecordNotFound
420
- report_error(404, {}, 'Object not found')
483
+ rescue => e
484
+ tmp = settings.api_server.call_hooks_for(:description_exception, args: [ctx, e])
485
+ report_error(
486
+ tmp[:http_status] || 500,
487
+ {},
488
+ tmp[:message] || 'Server error occured'
489
+ )
421
490
  end
422
491
 
423
492
  @formatter.format(true, desc)
@@ -1,4 +1,4 @@
1
1
  module HaveAPI
2
- PROTOCOL_VERSION = '1.0'
3
- VERSION = '0.6.0'
2
+ PROTOCOL_VERSION = '1.1'
3
+ VERSION = '0.7.0'
4
4
  end
@@ -7,6 +7,10 @@
7
7
  <link rel="stylesheet" href="/css/bootstrap.min.css">
8
8
  <script src="/js/jquery-1.11.1.min.js"></script>
9
9
  <script src="/js/bootstrap.min.js"></script>
10
+ <script src="/js/nojs-tabs.js"></script>
11
+ <link rel="stylesheet" href="/css/highlight.css">
12
+ <script src="/js/highlight.pack.js"></script>
13
+ <script src="/js/main.js"></script>
10
14
 
11
15
  <style>
12
16
  /*.resource { margin-left: 10px; }*/
@@ -47,6 +51,16 @@
47
51
  padding: 5px 0px;
48
52
  margin-left: 18px;
49
53
  }
54
+ .tab-hidden {
55
+ display: none;
56
+ }
57
+ pre {
58
+ overflow: auto;
59
+ }
60
+ pre code {
61
+ word-wrap: normal;
62
+ white-space: pre;
63
+ }
50
64
  </style>
51
65
  </head>
52
66
  <body data-spy="scroll" data-target=".table-of-contents">
@@ -21,14 +21,68 @@ def format_validators(validators)
21
21
  end
22
22
  %>
23
23
 
24
- <% def render_resource_body(resource, info, prefix='root', name=nil) %>
24
+ <% def render_auth_body(name, info) %>
25
+ <h2 id="auth-<%= name %>"><%= name.to_s.humanize %></h2>
26
+ <hr>
27
+
28
+ <p><%= info[:description] %></p>
29
+
30
+ <% if name == :token %>
31
+ <dl>
32
+ <dt>HTTP header:</dt>
33
+ <dd><%= info[:http_header] %></dd>
34
+ <dt>Query parameter:</dt>
35
+ <dd><%= info[:query_parameter] %></dd>
36
+ </dl>
37
+ <% end %>
38
+
39
+ <% if info[:resources] %>
40
+ <h2>Resources</h2>
41
+ <% sort_hash(info[:resources]).each do |resource, desc| %>
42
+ <% render_resource_body(resource.to_s, desc, 'auth') %>
43
+ <% end %>
44
+ <% end %>
45
+
46
+ <% baseid = "auth-#{name}" %>
47
+ <div id="<%= baseid %>-tabbar"></div>
48
+
49
+ <div id="<%= baseid %>-examples">
50
+ <% HaveAPI::ClientExample.clients.each_with_index do |client, i| %>
51
+ <div id="<%= "#{baseid}-#{i}" %>">
52
+ <% render_client_auth(client, name, info) %>
53
+ </div>
54
+ <% end %>
55
+ </div>
56
+
57
+ <script type="text/javascript">
58
+ nojsTabs({
59
+ tabs: document.getElementById('<%= "#{baseid}-examples" %>'),
60
+ titleSelector: 'h4',
61
+ tabBar: document.getElementById('<%= "#{baseid}-tabbar" %>'),
62
+ hiddenClass: 'tab-hidden',
63
+ activeClass: 'active',
64
+ createElement: function (el) {
65
+ if (el.tagName == 'UL')
66
+ el.classList.add('nav', 'nav-tabs');
67
+
68
+ else if (el.tagName == 'LI')
69
+ el.setAttribute('role', 'presentation');
70
+ }
71
+ });
72
+ </script>
73
+ <% end %>
74
+
75
+ <% def render_resource_body(resource, info, path = [], prefix='root', name=nil) %>
25
76
  <% name ||= resource.humanize %>
77
+ <% resource_path = path.clone << resource %>
78
+ <% resource_info = info %>
26
79
  <h2 class="resource" id="<%= "#{prefix}-#{resource}" %>"><%= resource.humanize %></h2>
80
+ <hr>
27
81
  <div class="resource-body">
28
82
  <p><%= info[:description] %></p>
29
83
 
30
84
  <div class="actions">
31
- <% info[:actions].each do |action, info| %>
85
+ <% sort_hash(info[:actions]).each do |action, info| %>
32
86
  <h3 id="<%= "#{prefix}-#{resource}-#{action}" %>"><%= name %> # <%= action.capitalize %></h3>
33
87
  <div class="action">
34
88
  <dl>
@@ -40,6 +94,8 @@ end
40
94
  <dd><%= info[:auth] ? 'yes' : 'no' %></dd>
41
95
  <dt>Aliases:</dt>
42
96
  <dd><%= info[:aliases].join(', ') %></dd>
97
+ <dt>Blocking:</dt>
98
+ <dd><%= info[:blocking] ? 'yes' : 'no' %></dd>
43
99
  </dl>
44
100
 
45
101
  <h4>Input parameters</h4>
@@ -54,7 +110,7 @@ end
54
110
  <dd><%= info[:input][:namespace] %></dd>
55
111
  </dl>
56
112
 
57
- <table class="table table-striped table-hover">
113
+ <table class="table table-striped table-hover table-bordered">
58
114
  <tr>
59
115
  <th>Label</th>
60
116
  <th>Name</th>
@@ -91,7 +147,7 @@ end
91
147
  <dd><%= info[:output][:namespace] %></dd>
92
148
  </dl>
93
149
 
94
- <table class="table table-striped table-hover">
150
+ <table class="table table-striped table-hover table-bordered">
95
151
  <tr>
96
152
  <th>Label</th>
97
153
  <th>Name</th>
@@ -117,15 +173,47 @@ end
117
173
 
118
174
  <% unless info[:examples].empty? %>
119
175
  <h4>Examples</h4>
120
- <% info[:examples].each do |example| %>
121
- <h5><%= example[:label] %></h5>
176
+ <% info[:examples].each_with_index do |example, i| %>
177
+ <h5><%= example[:title].empty? ? "Example ##{i}" : example[:title] %></h5>
122
178
  <p><%= example[:comment] %></p>
123
179
 
124
- <h6>Request</h6>
125
- <pre><code><%= JSON.pretty_generate(example[:request]) %></code></pre>
180
+ <% baseid = "example-#{resource_path.join('.')}-#{action}-#{i}" %>
181
+ <%# placeholder for tabs %>
182
+ <div id="<%= "#{baseid}-tabbar" %>"></div>
183
+
184
+ <div id="<%= "#{baseid}-examples" %>">
185
+ <% HaveAPI::ClientExample.clients.each_with_index do |client, j| %>
186
+ <div id="<%= "#{baseid}-#{j}" %>">
187
+ <%
188
+ render_client_example(
189
+ client,
190
+ resource_path,
191
+ resource_info,
192
+ action,
193
+ info,
194
+ example
195
+ )
196
+ %>
197
+ </div>
198
+ <% end %>
199
+ </div>
200
+
201
+ <script type="text/javascript">
202
+ nojsTabs({
203
+ tabs: document.getElementById('<%= "#{baseid}-examples" %>'),
204
+ titleSelector: 'h6',
205
+ tabBar: document.getElementById('<%= "#{baseid}-tabbar" %>'),
206
+ hiddenClass: 'tab-hidden',
207
+ activeClass: 'active',
208
+ createElement: function (el) {
209
+ if (el.tagName == 'UL')
210
+ el.classList.add('nav', 'nav-tabs');
126
211
 
127
- <h6>Response</h6>
128
- <pre><code><%= JSON.pretty_generate(example[:response]) %></code></pre>
212
+ else if (el.tagName == 'LI')
213
+ el.setAttribute('role', 'presentation');
214
+ }
215
+ });
216
+ </script>
129
217
  <% end %>
130
218
  <% end %>
131
219
 
@@ -134,24 +222,110 @@ end
134
222
  </div>
135
223
 
136
224
  <% unless info[:resources].empty? %>
137
- <% info[:resources].each do |r, i| %>
138
- <% render_resource_body(r, i, "#{prefix}-#{resource}", "#{name}.#{r.humanize}") %>
225
+ <% sort_hash(info[:resources]).each do |r, i| %>
226
+ <% render_resource_body(r, i, resource_path, "#{prefix}-#{resource}", "#{name}.#{r.humanize}") %>
139
227
  <% end %>
140
228
  <% end %>
141
229
 
142
230
  </div> <!-- resource -->
143
231
  <% end %>
144
232
 
233
+ <% def render_client_init(client) %>
234
+ <h4><%= client.label %></h4>
235
+ <pre><code class="<%= client.code %>"><%= client.init(host, base_url, api_version) %></code></pre>
236
+ <% end %>
237
+
238
+ <% def render_client_auth(client, method, desc) %>
239
+ <h4><%= client.label %></h4>
240
+ <pre><code class="<%= client.code %>"><%= client.auth(host, base_url, api_version, method, desc) %></code></pre>
241
+ <% end %>
242
+
243
+ <% def render_client_example(client, r_name, resource, a_name, action, example) %>
244
+ <h6><%= client.label %></h6>
245
+ <% sample = client.new(host, base_url, api_version, r_name, resource, a_name, action) %>
246
+ <% if sample.respond_to?(:example) %>
247
+ <pre><code class="<%= client.code %>"><%= sample.example(example) %></code></pre>
248
+
249
+ <% else %>
250
+ <h6>Request</h6>
251
+ <pre><code class="<%= client.code %>"><%= sample.request(example) %></code></pre>
252
+ <h6>Response</h6>
253
+ <pre><code class="<%= client.code %>"><%= sample.response(example) %></code></pre>
254
+ <% end %>
255
+ <% end %>
256
+
257
+
145
258
  <h1 id="api">API v<%= @v %></h1>
259
+
260
+ <ol class="breadcrumb">
261
+ <li><a href="<%= root %>"><%= host %></a></li>
262
+ <li class="active">v<%= @v %></li>
263
+ </ol>
264
+
146
265
  <p>
147
266
  This page contains a list of resources available in API v<%= @v %>, their actions,
148
267
  description, parameters and example usage.
149
268
  </p>
150
269
 
270
+ <p>
271
+ This API is based on the <a href="https://github.com/vpsfreecz/haveapi">HaveAPI</a> framework.
272
+ You can access it using existing clients:
273
+ </p>
274
+
275
+ <ul>
276
+ <li><a href="https://github.com/vpsfreecz/haveapi-client" target="_blank">Ruby library and CLI</a></li>
277
+ <li><a href="https://github.com/vpsfreecz/haveapi-client-js" target="_blank">JavaScript</a></li>
278
+ <li><a href="https://github.com/vpsfreecz/haveapi-client-php" target="_blank">PHP</a></li>
279
+ <li>
280
+ <a href="https://github.com/vpsfreecz/haveapi-webui" target="_blank">Generic web interface</a>
281
+ (<a href="https://webui.haveapi.org/v<%= version %>/#<%= escape(base_url) %>" target="_blank">connect to this API</a>)
282
+ </li>
283
+ <li><a href="https://github.com/vpsfreecz/haveapi-fs" target="_blank">FUSE-based file system</a></li>
284
+ </ul>
285
+
286
+ <p>
287
+ The code examples found on this page are for HaveAPI v<%= version %>, so be sure
288
+ to use clients of the same version.
289
+ </p>
290
+
291
+ <h2>Initialization</h2>
292
+
293
+ <div id="init-tabbar"></div>
294
+
295
+ <div id="init-examples">
296
+ <% HaveAPI::ClientExample.clients.each_with_index do |client, i| %>
297
+ <div id="<%= "init-#{i}" %>">
298
+ <% render_client_init(client) %>
299
+ </div>
300
+ <% end %>
301
+ </div>
302
+
303
+ <script type="text/javascript">
304
+ nojsTabs({
305
+ tabs: document.getElementById('init-examples'),
306
+ titleSelector: 'h4',
307
+ tabBar: document.getElementById('init-tabbar'),
308
+ hiddenClass: 'tab-hidden',
309
+ activeClass: 'active',
310
+ createElement: function (el) {
311
+ if (el.tagName == 'UL')
312
+ el.classList.add('nav', 'nav-tabs');
313
+
314
+ else if (el.tagName == 'LI')
315
+ el.setAttribute('role', 'presentation');
316
+ }
317
+ });
318
+ </script>
319
+
320
+ <h1 id="auth">Authentication methods</h1>
321
+ <% @help[:authentication].each do |name, info| %>
322
+ <% render_auth_body(name, info) %>
323
+ <% end %>
324
+
151
325
  <h1 id="resources">Resources</h1>
152
326
  <p>Follows a list of all resources in this API and their actions.</p>
153
327
 
154
- <% @help[:resources].each do |resource, info| %>
328
+ <% sort_hash(@help[:resources]).each do |resource, info| %>
155
329
  <% render_resource_body(resource, info) %>
156
330
  <% end %>
157
331
  </div>