power_api 1.0.0 → 2.0.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +93 -90
  5. data/README.md +329 -75
  6. data/app/helpers/power_api/application_helper.rb +57 -0
  7. data/bin/clean_test_app +2 -0
  8. data/lib/generators/power_api/controller/controller_generator.rb +27 -15
  9. data/lib/generators/power_api/exposed_api_config/USAGE +5 -0
  10. data/lib/generators/power_api/exposed_api_config/exposed_api_config_generator.rb +58 -0
  11. data/lib/generators/power_api/install/install_generator.rb +2 -44
  12. data/lib/generators/power_api/internal_api_config/USAGE +5 -0
  13. data/lib/generators/power_api/internal_api_config/internal_api_config_generator.rb +31 -0
  14. data/lib/generators/power_api/version/version_generator.rb +2 -2
  15. data/lib/power_api/engine.rb +8 -1
  16. data/lib/power_api/errors.rb +2 -0
  17. data/lib/power_api/generator_helper/active_record_resource.rb +10 -6
  18. data/lib/power_api/generator_helper/ams_helper.rb +5 -11
  19. data/lib/power_api/generator_helper/api_helper.rb +61 -0
  20. data/lib/power_api/generator_helper/controller_helper.rb +45 -15
  21. data/lib/power_api/generator_helper/routes_helper.rb +22 -7
  22. data/lib/power_api/generator_helper/rspec_controller_helper.rb +306 -0
  23. data/lib/power_api/generator_helper/swagger_helper.rb +14 -24
  24. data/lib/power_api/generator_helpers.rb +2 -1
  25. data/lib/power_api/version.rb +1 -1
  26. data/spec/dummy/app/controllers/api/base_controller.rb +2 -0
  27. data/spec/dummy/app/controllers/api/internal/base_controller.rb +5 -0
  28. data/spec/dummy/app/controllers/api/internal/blogs_controller.rb +36 -0
  29. data/spec/dummy/app/serializers/api/internal/blog_serializer.rb +12 -0
  30. data/spec/dummy/config/initializers/active_model_serializers.rb +1 -0
  31. data/spec/dummy/config/initializers/api_pagination.rb +32 -0
  32. data/spec/dummy/config/routes.rb +2 -7
  33. data/spec/dummy/spec/helpers/power_api/application_helper_spec.rb +171 -0
  34. data/spec/dummy/spec/lib/power_api/generator_helper/ams_helper_spec.rb +50 -12
  35. data/spec/dummy/spec/lib/power_api/generator_helper/api_helper_spec.rb +115 -0
  36. data/spec/dummy/spec/lib/power_api/generator_helper/controller_helper_spec.rb +126 -34
  37. data/spec/dummy/spec/lib/power_api/generator_helper/routes_helper_spec.rb +29 -5
  38. data/spec/dummy/spec/lib/power_api/generator_helper/rspec_controller_helper_spec.rb +559 -0
  39. data/spec/dummy/spec/lib/power_api/generator_helper/swagger_helper_spec.rb +10 -20
  40. data/spec/dummy/spec/support/shared_examples/active_record_resource_atrributes.rb +22 -3
  41. metadata +27 -5
  42. data/lib/power_api/generator_helper/version_helper.rb +0 -16
  43. data/spec/dummy/spec/lib/power_api/generator_helper/version_helper_spec.rb +0 -55
@@ -9,14 +9,21 @@ RSpec.describe PowerApi::GeneratorHelper::RoutesHelper, type: :generator do
9
9
  it { expect(perform).to eq(expected_path) }
10
10
  end
11
11
 
12
- describe "#api_version_routes_line_regex" do
13
- let(:expected_regex) { /Api::V1[^\n]*/ }
12
+ describe "#api_current_route_namespace_line_regex" do
13
+ let(:expected_regex) { /Api::Exposed::V1[^\n]*/ }
14
14
 
15
15
  def perform
16
- generators_helper.api_version_routes_line_regex
16
+ generators_helper.api_current_route_namespace_line_regex
17
17
  end
18
18
 
19
19
  it { expect(perform).to eq(expected_regex) }
20
+
21
+ context "without version" do
22
+ let(:version_number) { "" }
23
+ let(:expected_regex) { /namespace :internal[^\n]*/ }
24
+
25
+ it { expect(perform).to eq(expected_regex) }
26
+ end
20
27
  end
21
28
 
22
29
  describe "#parent_resource_routes_line_regex" do
@@ -87,7 +94,7 @@ RSpec.describe PowerApi::GeneratorHelper::RoutesHelper, type: :generator do
87
94
  let(:expected_tpl) do
88
95
  <<~ROUTE
89
96
  scope path: '/api' do
90
- api_version(module: 'Api::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
97
+ api_version(module: 'Api::Exposed::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
91
98
  end
92
99
  end
93
100
  ROUTE
@@ -104,7 +111,7 @@ RSpec.describe PowerApi::GeneratorHelper::RoutesHelper, type: :generator do
104
111
 
105
112
  let(:expected_tpl) do
106
113
  <<~ROUTE
107
- api_version(module: 'Api::V2', path: { value: 'v2' }, defaults: { format: 'json' }) do
114
+ api_version(module: 'Api::Exposed::V2', path: { value: 'v2' }, defaults: { format: 'json' }) do
108
115
  end
109
116
  ROUTE
110
117
  end
@@ -113,6 +120,23 @@ RSpec.describe PowerApi::GeneratorHelper::RoutesHelper, type: :generator do
113
120
  end
114
121
  end
115
122
 
123
+ describe "#internal_route_tpl" do
124
+ let(:expected_tpl) do
125
+ <<~ROUTE
126
+ namespace :api, defaults: { format: :json } do
127
+ namespace :internal do
128
+ end
129
+ end
130
+ ROUTE
131
+ end
132
+
133
+ def perform
134
+ generators_helper.internal_route_tpl
135
+ end
136
+
137
+ it { expect(perform).to eq(expected_tpl) }
138
+ end
139
+
116
140
  describe "#parent_route_exist?" do
117
141
  let(:parent_resource_name) { "user" }
118
142
  let(:line) { nil }
@@ -0,0 +1,559 @@
1
+ describe PowerApi::GeneratorHelper::RspecControllerHelper, type: :generator do
2
+ describe "#resource_spec_path" do
3
+ let(:expected_path) { "spec/requests/api/exposed/v1/blogs_spec.rb" }
4
+
5
+ def perform
6
+ generators_helper.resource_spec_path
7
+ end
8
+
9
+ it { expect(perform).to eq(expected_path) }
10
+
11
+ context "when nil version" do
12
+ let(:version_number) { nil }
13
+ let(:expected_path) { "spec/requests/api/internal/blogs_spec.rb" }
14
+
15
+ it { expect(perform).to eq(expected_path) }
16
+ end
17
+ end
18
+
19
+ describe "#resource_spec_tpl" do
20
+ let(:template) do
21
+ <<~SPEC
22
+ require 'rails_helper'
23
+
24
+ RSpec.describe 'Api::Exposed::V1::BlogsControllers', type: :request do
25
+ describe 'GET /index' do
26
+ let!(:blogs) { create_list(:blog, 5) }
27
+ let(:collection) { JSON.parse(response.body)['blogs'] }
28
+ let(:params) { {} }
29
+
30
+ def perform
31
+ get '/api/v1/blogs', params: params
32
+ end
33
+
34
+ before do
35
+ perform
36
+ end
37
+
38
+ it { expect(collection.count).to eq(5) }
39
+ it { expect(response.status).to eq(200) }
40
+ end
41
+
42
+ describe 'POST /create' do
43
+ let(:params) do
44
+ {
45
+ blog: {
46
+ title: 'Some title',
47
+ body: 'Some body'
48
+ }
49
+ }
50
+ end
51
+
52
+ let(:attributes) do
53
+ JSON.parse(response.body)['blog'].symbolize_keys
54
+ end
55
+ def perform
56
+ post '/api/v1/blogs', params: params
57
+ end
58
+
59
+ before do
60
+ perform
61
+ end
62
+
63
+ it { expect(attributes).to include(params[:blog]) }
64
+ it { expect(response.status).to eq(201) }
65
+ context 'with invalid attributes' do
66
+ let(:params) do
67
+ {
68
+ blog: {
69
+ title: nil}
70
+ }
71
+ end
72
+
73
+ it { expect(response.status).to eq(400) }
74
+ end
75
+
76
+ end
77
+
78
+ describe 'GET /show' do
79
+ let(:blog) { create(:blog) }
80
+ let(:blog_id) { blog.id.to_s }
81
+
82
+ let(:attributes) do
83
+ JSON.parse(response.body)['blog'].symbolize_keys
84
+ end
85
+ def perform
86
+ get '/api/v1/blogs/' + blog_id
87
+ end
88
+
89
+ before do
90
+ perform
91
+ end
92
+
93
+ it { expect(response.status).to eq(200) }
94
+ context 'with resource not found' do
95
+ let(:blog_id) { '666' }
96
+ it { expect(response.status).to eq(404) }
97
+ end
98
+ end
99
+
100
+ describe 'PUT /update' do
101
+ let(:blog) { create(:blog) }
102
+ let(:blog_id) { blog.id.to_s }
103
+
104
+ let(:params) do
105
+ {
106
+ blog: {
107
+ title: 'Some title',
108
+ body: 'Some body'
109
+ }
110
+ }
111
+ end
112
+
113
+ let(:attributes) do
114
+ JSON.parse(response.body)['blog'].symbolize_keys
115
+ end
116
+ def perform
117
+ put '/api/v1/blogs/' + blog_id, params: params
118
+ end
119
+
120
+ before do
121
+ perform
122
+ end
123
+
124
+ it { expect(attributes).to include(params[:blog]) }
125
+ it { expect(response.status).to eq(200) }
126
+ context 'with invalid attributes' do
127
+ let(:params) do
128
+ {
129
+ blog: {
130
+ title: nil}
131
+ }
132
+ end
133
+
134
+ it { expect(response.status).to eq(400) }
135
+ end
136
+
137
+ context 'with resource not found' do
138
+ let(:blog_id) { '666' }
139
+ it { expect(response.status).to eq(404) }
140
+ end
141
+ end
142
+
143
+ describe 'DELETE /destroy' do
144
+ let(:blog) { create(:blog) }
145
+ let(:blog_id) { blog.id.to_s }
146
+
147
+ def perform
148
+ get '/api/v1/blogs/' + blog_id
149
+ end
150
+
151
+ before do
152
+ perform
153
+ end
154
+
155
+ it { expect(response.status).to eq(200) }
156
+ context 'with resource not found' do
157
+ let(:blog_id) { '666' }
158
+ it { expect(response.status).to eq(404) }
159
+ end
160
+ end
161
+
162
+ end
163
+ SPEC
164
+ end
165
+
166
+ def perform
167
+ generators_helper.resource_spec_tpl
168
+ end
169
+
170
+ it { expect(perform).to eq(template) }
171
+
172
+ context "when nil version" do
173
+ let(:version_number) { nil }
174
+
175
+ it { expect(perform).to include("RSpec.describe 'Api::Internal::BlogsControllers'") }
176
+ it { expect(perform).to include("get '/api/internal/blogs', params: params") }
177
+ end
178
+
179
+ context "with authenticated_resource option" do
180
+ let(:authenticated_resource) { "user" }
181
+
182
+ let(:template) do
183
+ <<~SPEC
184
+ require 'rails_helper'
185
+
186
+ RSpec.describe 'Api::Exposed::V1::BlogsControllers', type: :request do
187
+ let(:user) { create(:user) }
188
+ describe 'GET /index' do
189
+ let!(:blogs) { create_list(:blog, 5) }
190
+ let(:collection) { JSON.parse(response.body)['blogs'] }
191
+ let(:params) { {} }
192
+
193
+ def perform
194
+ get '/api/v1/blogs', params: params
195
+ end
196
+
197
+ context 'with authorized user' do
198
+ before do
199
+ sign_in(user)
200
+ perform
201
+ end
202
+
203
+ it { expect(collection.count).to eq(5) }
204
+ it { expect(response.status).to eq(200) }
205
+ end
206
+
207
+ context 'with unauthenticated user' do
208
+ before { perform }
209
+
210
+ it { expect(response.status).to eq(401) }
211
+ end
212
+
213
+ end
214
+
215
+ describe 'POST /create' do
216
+ let(:params) do
217
+ {
218
+ blog: {
219
+ title: 'Some title',
220
+ body: 'Some body'
221
+ }
222
+ }
223
+ end
224
+
225
+ let(:attributes) do
226
+ JSON.parse(response.body)['blog'].symbolize_keys
227
+ end
228
+ def perform
229
+ post '/api/v1/blogs', params: params
230
+ end
231
+
232
+ context 'with authorized user' do
233
+ before do
234
+ sign_in(user)
235
+ perform
236
+ end
237
+
238
+ it { expect(attributes).to include(params[:blog]) }
239
+ it { expect(response.status).to eq(201) }
240
+ context 'with invalid attributes' do
241
+ let(:params) do
242
+ {
243
+ blog: {
244
+ title: nil}
245
+ }
246
+ end
247
+
248
+ it { expect(response.status).to eq(400) }
249
+ end
250
+
251
+ end
252
+
253
+ context 'with unauthenticated user' do
254
+ before { perform }
255
+
256
+ it { expect(response.status).to eq(401) }
257
+ end
258
+
259
+ end
260
+
261
+ describe 'GET /show' do
262
+ let(:blog) { create(:blog) }
263
+ let(:blog_id) { blog.id.to_s }
264
+
265
+ let(:attributes) do
266
+ JSON.parse(response.body)['blog'].symbolize_keys
267
+ end
268
+ def perform
269
+ get '/api/v1/blogs/' + blog_id
270
+ end
271
+
272
+ context 'with authorized user' do
273
+ before do
274
+ sign_in(user)
275
+ perform
276
+ end
277
+
278
+ it { expect(response.status).to eq(200) }
279
+ context 'with resource not found' do
280
+ let(:blog_id) { '666' }
281
+ it { expect(response.status).to eq(404) }
282
+ end
283
+ end
284
+
285
+ context 'with unauthenticated user' do
286
+ before { perform }
287
+
288
+ it { expect(response.status).to eq(401) }
289
+ end
290
+
291
+ end
292
+
293
+ describe 'PUT /update' do
294
+ let(:blog) { create(:blog) }
295
+ let(:blog_id) { blog.id.to_s }
296
+
297
+ let(:params) do
298
+ {
299
+ blog: {
300
+ title: 'Some title',
301
+ body: 'Some body'
302
+ }
303
+ }
304
+ end
305
+
306
+ let(:attributes) do
307
+ JSON.parse(response.body)['blog'].symbolize_keys
308
+ end
309
+ def perform
310
+ put '/api/v1/blogs/' + blog_id, params: params
311
+ end
312
+
313
+ context 'with authorized user' do
314
+ before do
315
+ sign_in(user)
316
+ perform
317
+ end
318
+
319
+ it { expect(attributes).to include(params[:blog]) }
320
+ it { expect(response.status).to eq(200) }
321
+ context 'with invalid attributes' do
322
+ let(:params) do
323
+ {
324
+ blog: {
325
+ title: nil}
326
+ }
327
+ end
328
+
329
+ it { expect(response.status).to eq(400) }
330
+ end
331
+
332
+ context 'with resource not found' do
333
+ let(:blog_id) { '666' }
334
+ it { expect(response.status).to eq(404) }
335
+ end
336
+ end
337
+
338
+ context 'with unauthenticated user' do
339
+ before { perform }
340
+
341
+ it { expect(response.status).to eq(401) }
342
+ end
343
+
344
+ end
345
+
346
+ describe 'DELETE /destroy' do
347
+ let(:blog) { create(:blog) }
348
+ let(:blog_id) { blog.id.to_s }
349
+
350
+ def perform
351
+ get '/api/v1/blogs/' + blog_id
352
+ end
353
+
354
+ context 'with authorized user' do
355
+ before do
356
+ sign_in(user)
357
+ perform
358
+ end
359
+
360
+ it { expect(response.status).to eq(200) }
361
+ context 'with resource not found' do
362
+ let(:blog_id) { '666' }
363
+ it { expect(response.status).to eq(404) }
364
+ end
365
+ end
366
+
367
+ context 'with unauthenticated user' do
368
+ before { perform }
369
+
370
+ it { expect(response.status).to eq(401) }
371
+ end
372
+
373
+ end
374
+
375
+ end
376
+ SPEC
377
+ end
378
+
379
+ it { expect(perform).to eq(template) }
380
+
381
+ context "with owned_by_authenticated_resource option" do
382
+ let(:authenticated_resource) { "user" }
383
+ let(:owned_by_authenticated_resource) { true }
384
+
385
+ it { expect(perform).to include("create_list(:blog, 5, user: user)") }
386
+ end
387
+ end
388
+
389
+ context "with parent_resource option" do
390
+ let(:parent_resource_name) { "portfolio" }
391
+
392
+ let(:template) do
393
+ <<~SPEC
394
+ require 'rails_helper'
395
+
396
+ RSpec.describe 'Api::Exposed::V1::BlogsControllers', type: :request do
397
+ let(:portfolio) { create(:portfolio) }
398
+ let(:portfolio_id) { portfolio.id }
399
+
400
+ describe 'GET /index' do
401
+ let!(:blogs) { create_list(:blog, 5, portfolio: portfolio) }
402
+ let(:collection) { JSON.parse(response.body)['blogs'] }
403
+ let(:params) { {} }
404
+
405
+ def perform
406
+ get '/api/v1/portfolios/' + portfolio.id.to_s + '/blogs', params: params
407
+ end
408
+
409
+ before do
410
+ perform
411
+ end
412
+
413
+ it { expect(collection.count).to eq(5) }
414
+ it { expect(response.status).to eq(200) }
415
+ end
416
+
417
+ describe 'POST /create' do
418
+ let(:params) do
419
+ {
420
+ blog: {
421
+ title: 'Some title',
422
+ body: 'Some body'
423
+ }
424
+ }
425
+ end
426
+
427
+ let(:attributes) do
428
+ JSON.parse(response.body)['blog'].symbolize_keys
429
+ end
430
+ def perform
431
+ post '/api/v1/portfolios/' + portfolio.id.to_s + '/blogs', params: params
432
+ end
433
+
434
+ before do
435
+ perform
436
+ end
437
+
438
+ it { expect(attributes).to include(params[:blog]) }
439
+ it { expect(response.status).to eq(201) }
440
+ context 'with invalid attributes' do
441
+ let(:params) do
442
+ {
443
+ blog: {
444
+ title: nil}
445
+ }
446
+ end
447
+
448
+ it { expect(response.status).to eq(400) }
449
+ end
450
+
451
+ end
452
+
453
+ describe 'GET /show' do
454
+ let(:blog) { create(:blog, portfolio: portfolio) }
455
+ let(:blog_id) { blog.id.to_s }
456
+
457
+ let(:attributes) do
458
+ JSON.parse(response.body)['blog'].symbolize_keys
459
+ end
460
+ def perform
461
+ get '/api/v1/blogs/' + blog_id
462
+ end
463
+
464
+ before do
465
+ perform
466
+ end
467
+
468
+ it { expect(response.status).to eq(200) }
469
+ context 'with resource not found' do
470
+ let(:blog_id) { '666' }
471
+ it { expect(response.status).to eq(404) }
472
+ end
473
+ end
474
+
475
+ describe 'PUT /update' do
476
+ let(:blog) { create(:blog, portfolio: portfolio) }
477
+ let(:blog_id) { blog.id.to_s }
478
+
479
+ let(:params) do
480
+ {
481
+ blog: {
482
+ title: 'Some title',
483
+ body: 'Some body'
484
+ }
485
+ }
486
+ end
487
+
488
+ let(:attributes) do
489
+ JSON.parse(response.body)['blog'].symbolize_keys
490
+ end
491
+ def perform
492
+ put '/api/v1/blogs/' + blog_id, params: params
493
+ end
494
+
495
+ before do
496
+ perform
497
+ end
498
+
499
+ it { expect(attributes).to include(params[:blog]) }
500
+ it { expect(response.status).to eq(200) }
501
+ context 'with invalid attributes' do
502
+ let(:params) do
503
+ {
504
+ blog: {
505
+ title: nil}
506
+ }
507
+ end
508
+
509
+ it { expect(response.status).to eq(400) }
510
+ end
511
+
512
+ context 'with resource not found' do
513
+ let(:blog_id) { '666' }
514
+ it { expect(response.status).to eq(404) }
515
+ end
516
+ end
517
+
518
+ describe 'DELETE /destroy' do
519
+ let(:blog) { create(:blog, portfolio: portfolio) }
520
+ let(:blog_id) { blog.id.to_s }
521
+
522
+ def perform
523
+ get '/api/v1/blogs/' + blog_id
524
+ end
525
+
526
+ before do
527
+ perform
528
+ end
529
+
530
+ it { expect(response.status).to eq(200) }
531
+ context 'with resource not found' do
532
+ let(:blog_id) { '666' }
533
+ it { expect(response.status).to eq(404) }
534
+ end
535
+ end
536
+
537
+ end
538
+ SPEC
539
+ end
540
+
541
+ it { expect(perform).to eq(template) }
542
+ end
543
+
544
+ context 'with only some actions (show and create)' do
545
+ let(:controller_actions) do
546
+ [
547
+ "show",
548
+ "create"
549
+ ]
550
+ end
551
+
552
+ it { expect(perform).to include("describe 'GET /show'") }
553
+ it { expect(perform).to include("describe 'POST /create'") }
554
+ it { expect(perform).not_to include("describe 'DELETE /destroy'") }
555
+ it { expect(perform).not_to include("describe 'PUT /update'") }
556
+ it { expect(perform).not_to include("describe 'GET /index'") }
557
+ end
558
+ end
559
+ end
@@ -1,4 +1,4 @@
1
- RSpec.describe PowerApi::GeneratorHelper::SwaggerHelper, type: :generator do
1
+ describe PowerApi::GeneratorHelper::SwaggerHelper, type: :generator do
2
2
  describe "#swagger_helper_path" do
3
3
  let(:expected_path) { "spec/swagger_helper.rb" }
4
4
 
@@ -206,50 +206,40 @@ RSpec.describe PowerApi::GeneratorHelper::SwaggerHelper, type: :generator do
206
206
  BLOG_SCHEMA = {
207
207
  type: :object,
208
208
  properties: {
209
- id: { type: :string, example: '1' },
210
- type: { type: :string, example: 'blog' },
211
- attributes: {
212
- type: :object,
213
- properties: {
209
+ id: { type: :integer, example: 666 },
214
210
  title: { type: :string, example: 'Some title' },
215
211
  body: { type: :string, example: 'Some body' },
216
212
  created_at: { type: :string, example: '1984-06-04 09:00', 'x-nullable': true },
217
213
  updated_at: { type: :string, example: '1984-06-04 09:00', 'x-nullable': true },
218
214
  portfolio_id: { type: :integer, example: 666, 'x-nullable': true }
219
- },
220
- required: [
221
- :title,
222
- :body
223
- ]
224
- }
225
215
  },
226
216
  required: [
227
- :id,
228
- :type,
229
- :attributes
217
+ :id,
218
+ :title,
219
+ :body
230
220
  ]
231
221
  }
232
222
 
233
223
  BLOGS_COLLECTION_SCHEMA = {
234
224
  type: "object",
235
225
  properties: {
236
- data: {
226
+ blogs: {
237
227
  type: "array",
238
228
  items: { "$ref" => "#/definitions/blog" }
239
229
  }
240
230
  },
241
231
  required: [
242
- :data
232
+ :blogs
243
233
  ]
244
234
  }
245
235
 
246
236
  BLOG_RESOURCE_SCHEMA = {
247
237
  type: "object",
248
238
  properties: {
249
- data: { "$ref" => "#/definitions/blog" }
239
+ blog: { "$ref" => "#/definitions/blog" }
250
240
  },
251
241
  required: [
252
- :data
242
+ :blog
253
243
  ]
254
244
  }
255
245
  SCHEMA
@@ -281,7 +271,7 @@ RSpec.describe PowerApi::GeneratorHelper::SwaggerHelper, type: :generator do
281
271
  schema('$ref' => '#/definitions/blogs_collection')
282
272
 
283
273
  run_test! do |response|
284
- expect(JSON.parse(response.body)['data'].count).to eq(expected_collection_count)
274
+ expect(JSON.parse(response.body)['blogs'].count).to eq(expected_collection_count)
285
275
  end
286
276
  end
287
277