haveapi 0.18.1 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/haveapi.gemspec +2 -1
  3. data/lib/haveapi/action.rb +72 -31
  4. data/lib/haveapi/authentication/base.rb +1 -1
  5. data/lib/haveapi/authentication/basic/provider.rb +2 -2
  6. data/lib/haveapi/authentication/chain.rb +4 -4
  7. data/lib/haveapi/authentication/oauth2/config.rb +62 -15
  8. data/lib/haveapi/authentication/oauth2/provider.rb +111 -17
  9. data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +36 -0
  10. data/lib/haveapi/authentication/token/config.rb +1 -0
  11. data/lib/haveapi/authorization.rb +19 -12
  12. data/lib/haveapi/client_examples/js_client.rb +11 -1
  13. data/lib/haveapi/client_examples/php_client.rb +43 -1
  14. data/lib/haveapi/context.rb +21 -2
  15. data/lib/haveapi/example.rb +9 -9
  16. data/lib/haveapi/hooks.rb +23 -23
  17. data/lib/haveapi/metadata.rb +1 -1
  18. data/lib/haveapi/model_adapter.rb +14 -14
  19. data/lib/haveapi/model_adapters/active_record.rb +20 -20
  20. data/lib/haveapi/output_formatter.rb +4 -4
  21. data/lib/haveapi/output_formatters/base.rb +1 -1
  22. data/lib/haveapi/parameters/resource.rb +22 -22
  23. data/lib/haveapi/parameters/typed.rb +7 -7
  24. data/lib/haveapi/params.rb +24 -22
  25. data/lib/haveapi/resource.rb +9 -3
  26. data/lib/haveapi/resources/action_state.rb +16 -16
  27. data/lib/haveapi/route.rb +3 -2
  28. data/lib/haveapi/server.rb +113 -98
  29. data/lib/haveapi/spec/mock_action.rb +7 -7
  30. data/lib/haveapi/spec/spec_methods.rb +8 -8
  31. data/lib/haveapi/tasks/yard.rb +2 -2
  32. data/lib/haveapi/validator.rb +13 -13
  33. data/lib/haveapi/validator_chain.rb +6 -6
  34. data/lib/haveapi/validators/acceptance.rb +2 -2
  35. data/lib/haveapi/validators/confirmation.rb +4 -4
  36. data/lib/haveapi/validators/exclusion.rb +4 -4
  37. data/lib/haveapi/validators/format.rb +4 -4
  38. data/lib/haveapi/validators/inclusion.rb +3 -3
  39. data/lib/haveapi/validators/length.rb +1 -1
  40. data/lib/haveapi/validators/numericality.rb +3 -3
  41. data/lib/haveapi/validators/presence.rb +2 -2
  42. data/lib/haveapi/version.rb +1 -1
  43. data/lib/haveapi/views/version_page/auth_body.erb +6 -4
  44. data/lib/haveapi/views/version_page/resource_body.erb +2 -0
  45. data/lib/haveapi.rb +1 -0
  46. data/spec/authorization_spec.rb +28 -28
  47. data/spec/envelope_spec.rb +4 -4
  48. data/spec/parameters/typed_spec.rb +3 -3
  49. data/spec/params_spec.rb +2 -2
  50. data/spec/validators/acceptance_spec.rb +2 -2
  51. data/spec/validators/confirmation_spec.rb +4 -4
  52. data/spec/validators/exclusion_spec.rb +2 -2
  53. data/spec/validators/format_spec.rb +5 -5
  54. data/spec/validators/inclusion_spec.rb +8 -8
  55. data/spec/validators/presence_spec.rb +1 -1
  56. metadata +19 -4
@@ -178,12 +178,14 @@ module HaveAPI
178
178
 
179
179
  if @direction == :input
180
180
  ret[:parameters] = context.authorization.filter_input(
181
- @params,
182
- ModelAdapters::Hash.output(context, ret[:parameters]))
181
+ @params,
182
+ ModelAdapters::Hash.output(context, ret[:parameters])
183
+ )
183
184
  else
184
185
  ret[:parameters] = context.authorization.filter_output(
185
- @params,
186
- ModelAdapters::Hash.output(context, ret[:parameters]))
186
+ @params,
187
+ ModelAdapters::Hash.output(context, ret[:parameters])
188
+ )
187
189
  end
188
190
 
189
191
  ret
@@ -205,11 +207,11 @@ module HaveAPI
205
207
  end
206
208
 
207
209
  case layout
208
- when :object, :hash
209
- params[namespace] ||= {}
210
+ when :object, :hash
211
+ params[namespace] ||= {}
210
212
 
211
- when :object_list, :hash_list
212
- params[namespace] ||= []
213
+ when :object_list, :hash_list
214
+ params[namespace] ||= []
213
215
  end
214
216
  end
215
217
 
@@ -298,14 +300,14 @@ module HaveAPI
298
300
 
299
301
  def valid_layout?(params)
300
302
  case layout
301
- when :object, :hash
302
- params[namespace].is_a?(Hash)
303
+ when :object, :hash
304
+ params[namespace].is_a?(Hash)
303
305
 
304
- when :object_list, :hash_list
305
- params[namespace].is_a?(Array)
306
+ when :object_list, :hash_list
307
+ params[namespace].is_a?(Array)
306
308
 
307
- else
308
- false
309
+ else
310
+ false
309
311
  end
310
312
  end
311
313
 
@@ -313,16 +315,16 @@ module HaveAPI
313
315
  ns = namespace
314
316
 
315
317
  case layout
316
- when :object, :hash
317
- yield(ns ? params[namespace] : params)
318
+ when :object, :hash
319
+ yield(ns ? params[namespace] : params)
318
320
 
319
- when :object_list, :hash_list
320
- (ns ? params[namespace] : params).each do |object|
321
- yield(object)
322
- end
321
+ when :object_list, :hash_list
322
+ (ns ? params[namespace] : params).each do |object|
323
+ yield(object)
324
+ end
323
325
 
324
- else
325
- false
326
+ else
327
+ false
326
328
  end
327
329
  end
328
330
 
@@ -59,20 +59,21 @@ module HaveAPI
59
59
  singular ? resource_name.singularize.underscore : resource_name.tableize
60
60
  end
61
61
 
62
- def self.routes(prefix='/')
62
+ def self.routes(prefix='/', resource_path: [])
63
63
  ret = []
64
64
  prefix = "#{prefix}#{@route || rest_name}/"
65
+ new_resource_path = resource_path + [resource_name.underscore]
65
66
 
66
67
  actions do |a|
67
68
  # Call used_by for selected model adapters. It is safe to do
68
69
  # only when all classes are loaded.
69
70
  a.initialize
70
71
 
71
- ret << Route.new(a.build_route(prefix).chomp('/'), a)
72
+ ret << Route.new(a.build_route(prefix).chomp('/'), a, new_resource_path)
72
73
  end
73
74
 
74
75
  resources do |r|
75
- ret << {r => r.routes(prefix)}
76
+ ret << {r => r.routes(prefix, resource_path: new_resource_path)}
76
77
  end
77
78
 
78
79
  ret
@@ -83,6 +84,9 @@ module HaveAPI
83
84
 
84
85
  context.resource = self
85
86
 
87
+ orig_resource_path = context.resource_path
88
+ context.resource_path = context.resource_path + [resource_name.underscore]
89
+
86
90
  hash[:actions].each do |action, path|
87
91
  context.action = action
88
92
  context.path = path
@@ -97,6 +101,8 @@ module HaveAPI
97
101
  ret[:resources][resource.resource_name.underscore] = resource.describe(children, context)
98
102
  end
99
103
 
104
+ context.resource_path = orig_resource_path
105
+
100
106
  ret
101
107
  end
102
108
 
@@ -26,12 +26,12 @@ module HaveAPI::Resources
26
26
  module Mixin
27
27
  def state_to_hash(state)
28
28
  hash = {
29
- id: state.id,
30
- label: state.label,
31
- status: state.status,
32
- created_at: state.created_at,
33
- updated_at: state.updated_at,
34
- can_cancel: state.can_cancel?,
29
+ id: state.id,
30
+ label: state.label,
31
+ status: state.status,
32
+ created_at: state.created_at,
33
+ updated_at: state.updated_at,
34
+ can_cancel: state.can_cancel?,
35
35
  }
36
36
 
37
37
  if state.finished?
@@ -68,10 +68,10 @@ module HaveAPI::Resources
68
68
  def exec
69
69
  ret = []
70
70
  actions = @context.server.action_state.list_pending(
71
- current_user,
72
- input[:offset],
73
- input[:limit],
74
- input[:order].to_sym
71
+ current_user,
72
+ input[:offset],
73
+ input[:limit],
74
+ input[:order].to_sym
75
75
  )
76
76
 
77
77
  actions.each do |state|
@@ -110,8 +110,8 @@ module HaveAPI::Resources
110
110
 
111
111
  loop do
112
112
  state = @context.server.action_state.new(
113
- current_user,
114
- id: params[:action_state_id]
113
+ current_user,
114
+ id: params[:action_state_id]
115
115
  )
116
116
 
117
117
  error('action state not found') unless state.valid?
@@ -147,8 +147,8 @@ module HaveAPI::Resources
147
147
 
148
148
  def exec
149
149
  state = @context.server.action_state.new(
150
- current_user,
151
- id: params[:action_state_id]
150
+ current_user,
151
+ id: params[:action_state_id]
152
152
  )
153
153
 
154
154
  return state_to_hash(state) if state.valid?
@@ -168,8 +168,8 @@ module HaveAPI::Resources
168
168
 
169
169
  def exec
170
170
  state = @context.server.action_state.new(
171
- current_user,
172
- id: params[:action_state_id]
171
+ current_user,
172
+ id: params[:action_state_id]
173
173
  )
174
174
 
175
175
  error('action state not found') unless state.valid?
data/lib/haveapi/route.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  module HaveAPI
2
2
  class Route
3
- attr_reader :path, :sinatra_path, :action
3
+ attr_reader :path, :sinatra_path, :action, :resource_path
4
4
 
5
- def initialize(path, action)
5
+ def initialize(path, action, resource_path)
6
6
  @path = path
7
7
  @sinatra_path = path.gsub(/:([a-zA-Z\-_]+)/, '{\1}')
8
8
  @action = action
9
+ @resource_path = resource_path
9
10
  end
10
11
 
11
12
  def http_method
@@ -17,21 +17,33 @@ module HaveAPI
17
17
  has_hook :post_authenticated,
18
18
  desc: 'Called after the user was authenticated',
19
19
  args: {
20
- current_user: 'object returned by the authentication backend',
20
+ current_user: 'object returned by the authentication backend',
21
21
  }
22
22
 
23
23
  has_hook :description_exception,
24
24
  desc: 'Called when an exception occurs when building self-description',
25
25
  args: {
26
- context: 'HaveAPI::Context',
27
- exception: 'exception instance',
26
+ context: 'HaveAPI::Context',
27
+ exception: 'exception instance',
28
28
  },
29
29
  ret: {
30
- http_status: 'HTTP status code to send to client',
31
- message: 'error message sent to the client',
30
+ http_status: 'HTTP status code to send to client',
31
+ message: 'error message sent to the client',
32
32
  }
33
33
 
34
34
  module ServerHelpers
35
+ def setup_formatter
36
+ return if @formatter
37
+ @formatter = OutputFormatter.new
38
+
39
+ unless @formatter.supports?(request.accept)
40
+ @halted = true
41
+ halt 406, "Not Acceptable\n"
42
+ end
43
+
44
+ content_type @formatter.content_type, charset: 'utf-8'
45
+ end
46
+
35
47
  def authenticate!(v)
36
48
  require_auth! unless authenticated?(v)
37
49
  end
@@ -47,11 +59,11 @@ module HaveAPI
47
59
  def access_control
48
60
  if request.env['HTTP_ORIGIN'] && request.env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
49
61
  halt 200, {
50
- 'Access-Control-Allow-Origin' => '*',
51
- 'Access-Control-Allow-Methods' => 'GET,POST,OPTIONS,PATCH,PUT,DELETE',
52
- 'Access-Control-Allow-Credentials' => 'false',
53
- 'Access-Control-Allow-Headers' => settings.api_server.allowed_headers,
54
- 'Access-Control-Max-Age' => (60*60).to_s
62
+ 'Access-Control-Allow-Origin' => '*',
63
+ 'Access-Control-Allow-Methods' => 'GET,POST,OPTIONS,PATCH,PUT,DELETE',
64
+ 'Access-Control-Allow-Credentials' => 'false',
65
+ 'Access-Control-Allow-Headers' => settings.api_server.allowed_headers,
66
+ 'Access-Control-Max-Age' => (60*60).to_s
55
67
  }, ''
56
68
  end
57
69
  end
@@ -66,12 +78,16 @@ module HaveAPI
66
78
  end
67
79
 
68
80
  def require_auth!
69
- report_error(401, {'WWW-Authenticate' => 'Basic realm="Restricted Area"'},
70
- 'Action requires user to authenticate')
81
+ report_error(
82
+ 401,
83
+ {'WWW-Authenticate' => 'Basic realm="Restricted Area"'},
84
+ 'Action requires user to authenticate'
85
+ )
71
86
  end
72
87
 
73
88
  def report_error(code, headers, msg)
74
89
  @halted = true
90
+
75
91
  content_type @formatter.content_type, charset: 'utf-8'
76
92
  halt code, headers, @formatter.format(false, nil, msg, version: false)
77
93
  end
@@ -151,13 +167,12 @@ module HaveAPI
151
167
  @extensions = []
152
168
  end
153
169
 
154
- # Include specific version +v+ of API.
155
- # +v+ can be one of:
156
- # [:all] use all available versions
157
- # [Array] use all versions in +Array+
158
- # [version] include only concrete version
159
- # +default+ is set only when including concrete version. Use
160
- # set_default_version otherwise.
170
+ # Include specific version `v` of API.
171
+ #
172
+ # `default` is set only when including concrete version. Use {set_default_version}
173
+ # otherwise.
174
+ #
175
+ # @param v [:all, Array<String>, String]
161
176
  def use_version(v, default: false)
162
177
  @versions ||= []
163
178
 
@@ -178,7 +193,7 @@ module HaveAPI
178
193
  end
179
194
 
180
195
  # Load routes for all resource from included API versions.
181
- # All routes are mounted under prefix +path+.
196
+ # All routes are mounted under prefix `path`.
182
197
  # If no default version is set, the last included version is used.
183
198
  def mount(prefix='/')
184
199
  @root = prefix
@@ -198,19 +213,11 @@ module HaveAPI
198
213
  set :show_exceptions, false
199
214
  end
200
215
 
216
+ helpers Sinatra::Cookies
201
217
  helpers ServerHelpers
202
218
  helpers DocHelpers
203
219
 
204
220
  before do
205
- @formatter = OutputFormatter.new
206
-
207
- unless @formatter.supports?(request.accept)
208
- @halted = true
209
- halt 406, "Not Acceptable\n"
210
- end
211
-
212
- content_type @formatter.content_type, charset: 'utf-8'
213
-
214
221
  if request.env['HTTP_ORIGIN']
215
222
  headers 'Access-Control-Allow-Origin' => '*',
216
223
  'Access-Control-Allow-Credentials' => 'false'
@@ -218,6 +225,7 @@ module HaveAPI
218
225
  end
219
226
 
220
227
  not_found do
228
+ setup_formatter
221
229
  report_error(404, {}, 'Action not found') unless @halted
222
230
  end
223
231
 
@@ -236,9 +244,9 @@ module HaveAPI
236
244
  authenticated?(settings.api_server.default_version)
237
245
 
238
246
  @api = settings.api_server.describe(Context.new(
239
- settings.api_server,
240
- user: current_user,
241
- params: params
247
+ settings.api_server,
248
+ user: current_user,
249
+ params: params
242
250
  ))
243
251
 
244
252
  content_type 'text/html'
@@ -246,30 +254,31 @@ module HaveAPI
246
254
  end
247
255
 
248
256
  @sinatra.options @root do
257
+ setup_formatter
249
258
  access_control
250
259
  authenticated?(settings.api_server.default_version)
251
260
  ret = nil
252
261
 
253
262
  case params[:describe]
254
- when 'versions'
255
- ret = {
256
- versions: settings.api_server.versions,
257
- default: settings.api_server.default_version
258
- }
259
-
260
- when 'default'
261
- ret = settings.api_server.describe_version(Context.new(
262
- settings.api_server,
263
- version: settings.api_server.default_version,
264
- user: current_user, params: params
265
- ))
263
+ when 'versions'
264
+ ret = {
265
+ versions: settings.api_server.versions,
266
+ default: settings.api_server.default_version
267
+ }
268
+
269
+ when 'default'
270
+ ret = settings.api_server.describe_version(Context.new(
271
+ settings.api_server,
272
+ version: settings.api_server.default_version,
273
+ user: current_user, params: params
274
+ ))
266
275
 
267
- else
268
- ret = settings.api_server.describe(Context.new(
269
- settings.api_server,
270
- user: current_user,
271
- params: params
272
- ))
276
+ else
277
+ ret = settings.api_server.describe(Context.new(
278
+ settings.api_server,
279
+ user: current_user,
280
+ params: params
281
+ ))
273
282
  end
274
283
 
275
284
  @formatter.format(true, ret)
@@ -348,10 +357,10 @@ module HaveAPI
348
357
 
349
358
  @v = v
350
359
  @help = settings.api_server.describe_version(Context.new(
351
- settings.api_server,
352
- version: v,
353
- user: current_user,
354
- params: params
360
+ settings.api_server,
361
+ version: v,
362
+ user: current_user,
363
+ params: params
355
364
  ))
356
365
 
357
366
  content_type 'text/html'
@@ -362,14 +371,15 @@ module HaveAPI
362
371
  end
363
372
 
364
373
  @sinatra.options prefix do
374
+ setup_formatter
365
375
  access_control
366
376
  authenticated?(v)
367
377
 
368
378
  @formatter.format(true, settings.api_server.describe_version(Context.new(
369
- settings.api_server,
370
- version: v,
371
- user: current_user,
372
- params: params
379
+ settings.api_server,
380
+ version: v,
381
+ user: current_user,
382
+ params: params
373
383
  )))
374
384
  end
375
385
 
@@ -380,10 +390,10 @@ module HaveAPI
380
390
 
381
391
  if action_state
382
392
  mount_resource(
383
- prefix,
384
- v,
385
- HaveAPI::Resources::ActionState,
386
- @routes[v][:resources]
393
+ prefix,
394
+ v,
395
+ HaveAPI::Resources::ActionState,
396
+ @routes[v][:resources]
387
397
  )
388
398
  end
389
399
 
@@ -406,8 +416,8 @@ module HaveAPI
406
416
  resource.routes(prefix).each do |route|
407
417
  if route.is_a?(Hash)
408
418
  hash[resource][:resources][route.keys.first] = mount_nested_resource(
409
- v,
410
- route.values.first
419
+ v,
420
+ route.values.first
411
421
  )
412
422
 
413
423
  else
@@ -435,6 +445,8 @@ module HaveAPI
435
445
 
436
446
  def mount_action(v, route)
437
447
  @sinatra.method(route.http_method).call(route.sinatra_path) do
448
+ setup_formatter
449
+
438
450
  if route.action.auth
439
451
  authenticate!(v)
440
452
  else
@@ -457,14 +469,15 @@ module HaveAPI
457
469
  end
458
470
 
459
471
  action = route.action.new(request, v, params, body, Context.new(
460
- settings.api_server,
461
- version: v,
462
- request: self,
463
- action: route.action,
464
- path: route.path,
465
- params: params,
466
- user: current_user,
467
- endpoint: true
472
+ settings.api_server,
473
+ version: v,
474
+ request: self,
475
+ action: route.action,
476
+ path: route.path,
477
+ params: params,
478
+ user: current_user,
479
+ endpoint: true,
480
+ resource_path: route.resource_path,
468
481
  ))
469
482
 
470
483
  unless action.authorized?(current_user)
@@ -475,18 +488,19 @@ module HaveAPI
475
488
  @halted = true
476
489
 
477
490
  [
478
- http_status || 200,
479
- @formatter.format(
480
- status,
481
- status ? reply : nil,
482
- !status ? reply : nil,
483
- errors,
484
- version: false
485
- ),
491
+ http_status || 200,
492
+ @formatter.format(
493
+ status,
494
+ status ? reply : nil,
495
+ !status ? reply : nil,
496
+ errors,
497
+ version: false
498
+ ),
486
499
  ]
487
500
  end
488
501
 
489
502
  @sinatra.options route.sinatra_path do |*args|
503
+ setup_formatter
490
504
  access_control
491
505
  route_method = route.http_method.to_s.upcase
492
506
 
@@ -499,15 +513,16 @@ module HaveAPI
499
513
  end
500
514
 
501
515
  ctx = Context.new(
502
- settings.api_server,
503
- version: v,
504
- request: self,
505
- action: route.action,
506
- path: route.path,
507
- args: args,
508
- params: params,
509
- user: current_user,
510
- endpoint: true
516
+ settings.api_server,
517
+ version: v,
518
+ request: self,
519
+ action: route.action,
520
+ path: route.path,
521
+ args: args,
522
+ params: params,
523
+ user: current_user,
524
+ endpoint: true,
525
+ resource_path: route.resource_path,
511
526
  )
512
527
 
513
528
  begin
@@ -520,9 +535,9 @@ module HaveAPI
520
535
  rescue => e
521
536
  tmp = settings.api_server.call_hooks_for(:description_exception, args: [ctx, e])
522
537
  report_error(
523
- tmp[:http_status] || 500,
524
- {},
525
- tmp[:message] || 'Server error occured'
538
+ tmp[:http_status] || 500,
539
+ {},
540
+ tmp[:message] || 'Server error occured'
526
541
  )
527
542
  end
528
543
 
@@ -534,8 +549,8 @@ module HaveAPI
534
549
  context.version = @default_version
535
550
 
536
551
  ret = {
537
- default_version: @default_version,
538
- versions: {default: describe_version(context)},
552
+ default_version: @default_version,
553
+ versions: {default: describe_version(context)},
539
554
  }
540
555
 
541
556
  @versions.each do |v|
@@ -548,10 +563,10 @@ module HaveAPI
548
563
 
549
564
  def describe_version(context)
550
565
  ret = {
551
- authentication: @auth_chain.describe(context),
552
- resources: {},
553
- meta: Metadata.describe,
554
- help: version_prefix(context.version)
566
+ authentication: @auth_chain.describe(context),
567
+ resources: {},
568
+ meta: Metadata.describe,
569
+ help: version_prefix(context.version)
555
570
  }
556
571
 
557
572
  #puts JSON.pretty_generate(@routes)
@@ -10,13 +10,13 @@ module HaveAPI::Spec
10
10
 
11
11
  def call(input, user: nil, &block)
12
12
  action = @action.new(nil, @v, input, nil, HaveAPI::Context.new(
13
- @server,
14
- version: @v,
15
- action: @action,
16
- path: @path,
17
- params: input,
18
- user: user,
19
- endpoint: true
13
+ @server,
14
+ version: @v,
15
+ action: @action,
16
+ path: @path,
17
+ params: input,
18
+ user: user,
19
+ endpoint: true
20
20
  ))
21
21
 
22
22
  unless action.authorized?(user)
@@ -35,23 +35,23 @@ module HaveAPI::Spec
35
35
  app
36
36
 
37
37
  action, path = find_action(
38
- (params && params[:version]) || @api.default_version,
39
- r_name, a_name
38
+ (params && params[:version]) || @api.default_version,
39
+ r_name, a_name
40
40
  )
41
41
 
42
42
  method(action.http_method).call(
43
- path,
44
- params && params.to_json,
45
- {'Content-Type' => 'application/json'}
43
+ path,
44
+ params && params.to_json,
45
+ {'Content-Type' => 'application/json'}
46
46
  )
47
47
 
48
48
  else
49
49
  http_method, path, params = args
50
50
 
51
51
  method(http_method).call(
52
- path,
53
- params && params.to_json,
54
- {'Content-Type' => 'application/json'}
52
+ path,
53
+ params && params.to_json,
54
+ {'Content-Type' => 'application/json'}
55
55
  )
56
56
  end
57
57
  end
@@ -5,8 +5,8 @@ def render_doc_file(src, dst)
5
5
 
6
6
  Proc.new do
7
7
  File.write(
8
- dst,
9
- ERB.new(File.read(src), 0).result(binding)
8
+ dst,
9
+ ERB.new(File.read(src), 0).result(binding)
10
10
  )
11
11
  end
12
12
  end