haveapi 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>