haveapi 0.18.1 → 0.19.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 (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