snfoil-controller 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,537 @@
1
+ # SnFoil::Controller
2
+
3
+ ![build](https://github.com/limited-effort/snfoil-controller/actions/workflows/main.yml/badge.svg) [![maintainability](https://api.codeclimate.com/v1/badges/10885d7b7231f3e9b0b7/maintainability)](https://codeclimate.com/github/limited-effort/snfoil-controller/maintainability)
4
+
5
+ SnFoil Controllers help seperate your business logic from your api layer.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'snfoil-controller'
13
+ ```
14
+
15
+ ## Usage
16
+ Ultimately SnFoil Controllers are just SnFoil Contexts, but they setup their workflow a little differently. `endpoint` creates `setup_*` and `process_*` intervals to handle your data, and the method or block provided renders it.
17
+
18
+ ### Quickstart Example
19
+
20
+ ```ruby
21
+ # app/controllers/people_controller.rb
22
+
23
+ class PeopleController < ActionController::API
24
+ include SnFoil::Controller
25
+
26
+ context PeopleContext
27
+ serializer PeopleSerializer
28
+ context PeopleDeserializer
29
+
30
+ endpoint :create, do |object:, **options|
31
+ if object.errors
32
+ render json: object.errors, status: :unprocessable_entity
33
+ else
34
+ render json: serialize(object, **options), status: :created
35
+ end
36
+ end
37
+
38
+ endpoint :update, do |object:, **options|
39
+ if object.errors
40
+ render json: object.errors, status: :unprocessable_entity
41
+ else
42
+ render json: serialize(object, **options), status: :ok
43
+ end
44
+ end
45
+
46
+ endpoint :show, do |object:, **options|
47
+ render json: serialize(object, **options), status: :created
48
+ end
49
+
50
+ endpoint :delete, do |object:, **options|
51
+ if object.errors
52
+ render json: object.errors, status: :unprocessable_entity
53
+ else
54
+ render json: {}, status: :no_content
55
+ end
56
+ end
57
+
58
+ setup_create { |**options| options[:params] = deserialize(params, **options) }
59
+ setup_update { |**options| options[:params] = deserialize(params, **options) }
60
+
61
+ process_create { |**options| run_context(**options) }
62
+ process_update { |**options| run_context(**options) }
63
+ process_show { |**options| run_context(**options) }
64
+ process_delete { |**options| run_context(**options) }
65
+ end
66
+ ```
67
+
68
+ ### Controller
69
+
70
+ A controller is a combination of a Context, Serializer, Deserializer, and a some Endpoints. See the Quickstart exaple above and the description of functions below for more details.
71
+
72
+ ##### Ussing SSR
73
+
74
+ You don't need a serializer. You can just use a standard render in the endpoint's function.
75
+
76
+ ```ruby
77
+ # taken from https://guides.rubyonrails.org/layouts_and_rendering.html
78
+
79
+ class BooksController < ApplicationController
80
+ def index
81
+ @books = Book.all
82
+ end
83
+ end
84
+
85
+ # becomes
86
+
87
+ class BooksController < ApplicationController
88
+ include SnFoil::Controller
89
+
90
+ endpoint(:index) { |**options| @books = Book.all }
91
+ end
92
+
93
+ ```
94
+
95
+ #### Endpoint
96
+
97
+ Endpoint creates a workflow with two intervals and a primary function for rendering.
98
+
99
+
100
+ ```ruby
101
+ class PeopleController < ActionController::API
102
+ include SnFoil::Controller
103
+
104
+ endpoint(:create) { |**options| render json: options[:object] }
105
+ end
106
+ ```
107
+
108
+ In this exmaple the `setup_create` and `process_create` intervals are defined for you and the method finally returns the block. If you don't want to provide a block you can instead pass in a method name
109
+
110
+ ```ruby
111
+ class PeopleController < ActionController::API
112
+ include SnFoil::Controller
113
+
114
+ endpoint(:create, with: :render_create)
115
+
116
+ def render_create(**options)
117
+ render json: options[:object]
118
+ end
119
+ end
120
+ ```
121
+
122
+ Any options passed in as arguements to endpoint will be passed to the intervals and flow through just like a Context (becuase it is a Context under the hood).
123
+
124
+ ```ruby
125
+ class PeopleController < ActionController::API
126
+ include SnFoil::Controller
127
+
128
+ endpoint(:create, with: :render_create, interesting: 'key you have there')
129
+
130
+ setup_create do |**options|
131
+ puts options[:interesting] # => 'key you have there'
132
+ ...
133
+ end
134
+ end
135
+ ```
136
+ ##### Arguments
137
+
138
+ <table>
139
+ <thead>
140
+ <th>name</th>
141
+ <th>type</th>
142
+ <th>description</th>
143
+ <th>required</th>
144
+ </thead>
145
+
146
+ <tbody>
147
+ <tr>
148
+ <td>name</td>
149
+ <td>string|symbol</td>
150
+ <td>The name of the method to be defined on the controller and the intervals</td>
151
+ <td>true</td>
152
+ </tr>
153
+ <tr>
154
+ <td>options</td>
155
+ <td>keyword arguments</td>
156
+ <td>The options you want passed down the chain of intervals and to the context</td>
157
+ <td>false</td>
158
+ </tr>
159
+ <tr>
160
+ <td>block</td>
161
+ <td>proc</td>
162
+ <td>The function you want to render your controller action</td>
163
+ <td>conditionally based on if you don't provide a `:with` in the only</td>
164
+ </tr>
165
+ </tbody>
166
+ </table>
167
+
168
+ There are a few reserved keyword arguements that cause different functionlity/configuration for options:
169
+
170
+ * `with` - The method name to use if a block is not provided to the endpoint
171
+ * `context` - The context to use for this endpoint. Overrides the one configured using #self.context
172
+ * `context_action` - The method name to call on the context. Defaults to the endpoint name.
173
+ * `serializer` - The serializer to use for this endpoint. Overrides the one configured using #self.serializer
174
+ * `serialize` - The block used to process the serializer. Overrides the one configured using #self.serializer
175
+ * `serialize_with` - The method used to process the serializer. Overrides the one configured using #self.serializer
176
+ * `deserializer` - The deserializer to use for this endpoint. Overrides the one configured using #self.deserializer
177
+ * `deserialize` - The block used to process the deserializer. Overrides the one configured using #self.deserializer
178
+ * `deserialize_with` - The method used to process the deserializer. Overrides the one configured using #self.deserializer
179
+
180
+ #### Context
181
+
182
+ The main context intended to be called by the Controller.
183
+
184
+ ```ruby
185
+ class PeopleController < ActionController::API
186
+ include SnFoil::Controller
187
+
188
+ context PeopleContext
189
+ end
190
+ ```
191
+ ##### Arguments
192
+
193
+ <table>
194
+ <thead>
195
+ <th>name</th>
196
+ <th>type</th>
197
+ <th>description</th>
198
+ <th>required</th>
199
+ </thead>
200
+
201
+ <tbody>
202
+ <tr>
203
+ <td>name</td>
204
+ <td>class</td>
205
+ <td>The context class for the controller</td>
206
+ <td>true</td>
207
+ </tr>
208
+ </tbody>
209
+ </table>
210
+
211
+ #### Serializer
212
+
213
+ The main serializer intended to be called by the Controller. Also the default serializer and block used by the '#serialize` method.
214
+
215
+ ```ruby
216
+ class PeopleController < ActionController::API
217
+ include SnFoil::Controller
218
+
219
+ serializer PeopleSerializer
220
+ end
221
+ ```
222
+ ##### Arguments
223
+
224
+ <table>
225
+ <thead>
226
+ <th>name</th>
227
+ <th>type</th>
228
+ <th>description</th>
229
+ <th>required</th>
230
+ </thead>
231
+
232
+ <tbody>
233
+ <tr>
234
+ <td>name</td>
235
+ <td>class</td>
236
+ <td>The serializer class for the controller</td>
237
+ <td>false</td>
238
+ </tr>
239
+ <tr>
240
+ <td>block</td>
241
+ <td>proc</td>
242
+ <td>The block to be called to serialize the data</td>
243
+ <td>false</td>
244
+ </tr>
245
+ </tbody>
246
+ </table>
247
+
248
+ ##### Default Call
249
+
250
+ If no block or method is provided, `#serialize` will try to new up the Serializer class with arguments `object` and `options` and call `#to_hash`.
251
+
252
+ ```ruby
253
+ Serializer.new(object, **options).to_hash
254
+ ```
255
+
256
+ ##### Passing in a Block
257
+
258
+ If you provide a block to the `#self.serializer` method you can define how you want the serializer to be called.
259
+
260
+ ```ruby
261
+ class PeopleController < ActionController::API
262
+ include SnFoil::Controller
263
+
264
+ serializer(PeopleSerializer) { |object, serializer, **_options| serializer.new(object).serialize }
265
+ end
266
+ ```
267
+
268
+ #### Deserializer
269
+
270
+ The main deserializer intended to be called by the Controller. Also the default deserializer and block used by the '#deserialize` method.
271
+
272
+ ```ruby
273
+ class PeopleController < ActionController::API
274
+ include SnFoil::Controller
275
+
276
+ deserializer PeopleDeserializer
277
+ end
278
+ ```
279
+ ##### Arguments
280
+
281
+ <table>
282
+ <thead>
283
+ <th>name</th>
284
+ <th>type</th>
285
+ <th>description</th>
286
+ <th>required</th>
287
+ </thead>
288
+
289
+ <tbody>
290
+ <tr>
291
+ <td>name</td>
292
+ <td>class</td>
293
+ <td>The deserializer class for the controller</td>
294
+ <td>false</td>
295
+ </tr>
296
+ <tr>
297
+ <td>block</td>
298
+ <td>proc</td>
299
+ <td>The block to be called to deserialize the data</td>
300
+ <td>false</td>
301
+ </tr>
302
+ </tbody>
303
+ </table>
304
+
305
+ ##### Default Call
306
+
307
+ If no block or method is provided, `#deserialize` will try to new up the Deserializer class with arguments `object` and `options` and call `#to_hash`.
308
+
309
+ ```ruby
310
+ Deserializer.new(object, **options).to_hash
311
+ ```
312
+
313
+ ##### Passing in a Block
314
+
315
+ If you provide a block to the `#self.deserializer` method you can define how you want the deserializer to be called.
316
+
317
+ ```ruby
318
+ class PeopleController < ActionController::API
319
+ include SnFoil::Controller
320
+
321
+ deserializer(PeopleDeserializer) { |object, deserializer, **_options| deserializer.new(object).deserialize }
322
+ end
323
+ ```
324
+
325
+ ### Serializers and Deserializers
326
+
327
+ Since Serializers seem so abundant SnFoil Controllers does not ship with any. We recommend the awesome [jsonapi-serializer](https://github.com/jsonapi-serializer/jsonapi-serializer).
328
+
329
+ Deserializers haven't come so far - so we've setup two:
330
+
331
+ * SnFoil::Deserializer::JSON
332
+ * SnFoil::Deserializer::JSONAPI
333
+
334
+ These allow you to allow-list and format any incoming data into a standard more usable by your business logic.
335
+
336
+ ##### Usage
337
+
338
+ ```ruby
339
+ class PeopleDeserializer
340
+ include SnFoil::Deserializer::JSON
341
+
342
+ key_transform :underscore
343
+
344
+ attributes :first_name, :middle_name, :last_name
345
+ attributes :line1, :line2, :city, :state, :zip, prefix: :address_
346
+
347
+ has_many :books, deserializer: BookDeserializer
348
+ end
349
+ ```
350
+
351
+ Both these deserializers share some common functions
352
+
353
+ ##### key_transform
354
+
355
+ How you want to format the keys in the incoming payload. SnFoil::Deserializers will always `:to_sym` all of the keys and will by default `:underscore` them. You can pass in most active_support inflections or you can run some custom logic on them.
356
+
357
+ ###### Arguments
358
+
359
+ <table>
360
+ <thead>
361
+ <th>name</th>
362
+ <th>type</th>
363
+ <th>description</th>
364
+ <th>required</th>
365
+ </thead>
366
+ <tbody>
367
+ <tr>
368
+ <td>tranform</td>
369
+ <td>symbol</td>
370
+ <td>The inflection you want called on the key value. ex: `underscore`, `camelcase`</td>
371
+ <td>false</td>
372
+ </tr>
373
+ <tr>
374
+ <td>block</td>
375
+ <td>proc</td>
376
+ <td>A custom proc passed the input request and the key the return value will be stored under.</td>
377
+ <td>false</td>
378
+ </tr>
379
+ </tbody>
380
+ </table>
381
+
382
+ ##### attribute
383
+
384
+ An attribute to be taken from the input payload.
385
+
386
+ ```ruby
387
+ attribute :first_name
388
+ attribute :last_name
389
+ attribute :line1, :prefix: :addr_
390
+ attribute :line2, :prefix: :addr_
391
+ attribute :city, :prefix: :addr_
392
+ attribute :state, :prefix: :addr_
393
+ attribute :zip_code, key: :addr_postal_code
394
+ ```
395
+
396
+ ###### Arguments
397
+
398
+ <table>
399
+ <thead>
400
+ <th>name</th>
401
+ <th>type</th>
402
+ <th>description</th>
403
+ <th>required</th>
404
+ </thead>
405
+ <tbody>
406
+ <tr>
407
+ <td>name</td>
408
+ <td>symbol</td>
409
+ <td>The name of the key to be output in the final hash</td>
410
+ <td>true</td>
411
+ </tr>
412
+ <tr>
413
+ <td>options</td>
414
+ <td>keyword arguments</td>
415
+ <td>The options you want passed down the chain of intervals and to the context</td>
416
+ <td>false</td>
417
+ </tr>
418
+ <tr>
419
+ <td>block</td>
420
+ <td>proc</td>
421
+ <td></td>
422
+ <td>false</td>
423
+ </tr>
424
+ </tbody>
425
+ </table>
426
+
427
+ If you are using a block or the `:with` argument it will be passed the input, the key, and any options for the deserializer. The return of the block or method is what will be used as the value instead of looking up the key directly in the input.
428
+
429
+ example:
430
+
431
+ ```ruby
432
+ attribute(:test) { |request, key, **options| request[:data][key] }
433
+ ```
434
+
435
+ There are a few reserved keyword arguements that cause different functionlity/configuration for options:
436
+
437
+ * `key` the name of the key from the original input payload. If not provided this defaults to the name of the attribute.
438
+ * `prefix` a prefix for the key you are looking for. ex `attribute(:line1, prefix: :addr_)` will look for a key labeled `:addr_line1`
439
+ * `with` the method name you want to call to lookup/parse an attribute
440
+
441
+ ##### attributes
442
+
443
+ The same as attribute except you can pass in multiple keys.
444
+
445
+
446
+ ```ruby
447
+ attributes :first_name, :last_name
448
+ attributes :line1, :line2, :city, :state :prefix: :addr_
449
+ ```
450
+
451
+ ##### belongs_to
452
+
453
+ A standard belongs_to relationship. Instead of grabbing a single key from the payload, expects to grab a hash.
454
+
455
+ ```ruby
456
+ belongs_to :team
457
+ ```
458
+
459
+ ###### Arguments
460
+
461
+ <table>
462
+ <thead>
463
+ <th>name</th>
464
+ <th>type</th>
465
+ <th>description</th>
466
+ <th>required</th>
467
+ </thead>
468
+ <tbody>
469
+ <tr>
470
+ <td>tranform</td>
471
+ <td>symbol</td>
472
+ <td>The inflection you want called on the key value. ex: `underscore`, `camelcase`</td>
473
+ <td>false</td>
474
+ </tr>
475
+ <tr>
476
+ <td>block</td>
477
+ <td>proc</td>
478
+ <td>A custom proc passed the input request and the key the return value will be stored under.</td>
479
+ <td>false</td>
480
+ </tr>
481
+ </tbody>
482
+ </table>
483
+
484
+ ##### has_one
485
+
486
+ Just an alias for `#belongs_to`
487
+
488
+ ##### has_many
489
+
490
+ A standard has_many relationship. Instead of grabbing a single key from the payload, expects to grab an array.
491
+
492
+ ```ruby
493
+ has_many :pets
494
+ ```
495
+
496
+ ###### Arguments
497
+
498
+ <table>
499
+ <thead>
500
+ <th>name</th>
501
+ <th>type</th>
502
+ <th>description</th>
503
+ <th>required</th>
504
+ </thead>
505
+ <tbody>
506
+ <tr>
507
+ <td>tranform</td>
508
+ <td>symbol</td>
509
+ <td>The inflection you want called on the key value. ex: `underscore`, `camelcase`</td>
510
+ <td>false</td>
511
+ </tr>
512
+ <tr>
513
+ <td>block</td>
514
+ <td>proc</td>
515
+ <td>A custom proc passed the input request and the key the return value will be stored under.</td>
516
+ <td>false</td>
517
+ </tr>
518
+ </tbody>
519
+ </table>
520
+
521
+ ## Development
522
+
523
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
524
+
525
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
526
+
527
+ ## Contributing
528
+
529
+ Bug reports and pull requests are welcome on GitHub at https://github.com/limited-effort/snfoil-controller. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/limited-effort/snfoil-controller/blob/main/CODE_OF_CONDUCT.md).
530
+
531
+ ## License
532
+
533
+ The gem is available as open source under the terms of the [Apache 2 License](https://opensource.org/licenses/Apache-2.0).
534
+
535
+ ## Code of Conduct
536
+
537
+ Everyone interacting in the Snfoil::Controller project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/limited-effort/snfoil-controller/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Matthew Howes
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module SnFoil
18
+ module Controller
19
+ VERSION = '1.0.0'
20
+ end
21
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Matthew Howes, Cliff Campbell
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'active_support/concern'
18
+ require 'snfoil/context'
19
+
20
+ module SnFoil
21
+ # ActiveSupport::Concern for Controller functionality
22
+ # A SnFoil::Controller is essentially a context but instead of using #action uses a more simplified workflow
23
+ # called #endpoint. The method or block passed to endpoint is ultimately what renders.
24
+ # #endpoint creates the following intervals:
25
+ # * setup_*
26
+ # * process_*
27
+ #
28
+ # This concern also adds the following class methods
29
+ # * context - The context associated with the controller to process the business logic
30
+ # * deserializer - the deserializer associated with the controller to allow list incoming params
31
+ # * endpoint - helper function to build endpoint methods
32
+ # * serializer - The serializer associated to render the context's output
33
+ #
34
+ # @author Matthew Howes
35
+ #
36
+ # @since 0.1.0
37
+ module Controller
38
+ extend ActiveSupport::Concern
39
+
40
+ class Error < RuntimeError; end
41
+
42
+ included do
43
+ include SnFoil::Context
44
+
45
+ module_eval do
46
+ def entity
47
+ @entity ||= if defined? current_entity
48
+ current_entity
49
+ elsif defined? current_user
50
+ current_user
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ class_methods do
57
+ attr_reader :snfoil_endpoints, :snfoil_context,
58
+ :snfoil_serializer, :snfoil_serializer_block,
59
+ :snfoil_deserializer, :snfoil_deserializer_block
60
+
61
+ def context(klass = nil)
62
+ raise SnFoil::Controller::Error, "context already defined for #{self}" if @snfoil_context
63
+
64
+ @snfoil_context = klass
65
+ end
66
+
67
+ def serializer(klass = nil, &block)
68
+ raise SnFoil::Controller::Error, "serializer already defined for #{self}" if @snfoil_serializer || @snfoil_serializer_block
69
+
70
+ @snfoil_serializer = klass
71
+ @snfoil_serializer_block = block
72
+ end
73
+
74
+ def deserializer(klass = nil, &block)
75
+ raise SnFoil::Controller::Error, "deserializer already defined for #{self}" if @snfoil_deserializer || @snfoil_deserializer_block
76
+
77
+ @snfoil_deserializer = klass
78
+ @snfoil_deserializer_block = block
79
+ end
80
+
81
+ def endpoint(name, **options, &block)
82
+ (@snfoil_endpoints ||= {})[name] =
83
+ options.merge(controller_action: name, method: options[:with], block: block)
84
+
85
+ interval "setup_#{name}"
86
+ interval "process_#{name}"
87
+
88
+ define_endpoint_method(name)
89
+ end
90
+ end
91
+
92
+ def serialize(object, **options)
93
+ serializer = options.fetch(:serializer) { self.class.snfoil_serializer }
94
+ return object unless serializer
95
+
96
+ exec_serialize(serializer, object, **options)
97
+ end
98
+
99
+ def deserialize(params, **options)
100
+ deserializer = options.fetch(:deserializer) { self.class.snfoil_deserializer }
101
+ return params unless deserializer
102
+
103
+ exec_deserialize(deserializer, params, **options)
104
+ end
105
+
106
+ def run_context(context: nil, context_action: nil, controller_action: nil, **_options)
107
+ (context || self.class.snfoil_context).new(entity).send(context_action || controller_action)
108
+ end
109
+
110
+ protected
111
+
112
+ class_methods do
113
+ def define_endpoint_method(name)
114
+ define_method(name) do |**options|
115
+ options = options.merge self.class.snfoil_endpoints[name]
116
+ options = run_interval("setup_#{name}", **options)
117
+ options = run_interval("process_#{name}", **options)
118
+ exec_render(**options)
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def exec_render(method: nil, block: nil, **options)
126
+ return send(method, **options) if method
127
+
128
+ instance_exec(**options, &block)
129
+ end
130
+
131
+ def exec_serialize(serializer, object, **options)
132
+ serializer_block = options.fetch(:serialize) { self.class.snfoil_serializer_block }
133
+ if options[:serialize_with]
134
+ send(options[:serialize_with], object, serializer, **options, current_entity: entity)
135
+ elsif serializer_block
136
+ instance_exec(object, serializer, **options, current_entity: entity, &serializer_block)
137
+ else
138
+ serializer.new(object, **options, current_entity: entity).to_hash
139
+ end
140
+ end
141
+
142
+ def exec_deserialize(deserializer, params, **options)
143
+ deserializer_block = options.fetch(:deserialize) { self.class.snfoil_deserializer_block }
144
+ if options[:deserialize_with]
145
+ send(options[:deserialize_with], params, deserializer, **options, current_entity: entity)
146
+ elsif deserializer_block
147
+ instance_exec(params, deserializer, **options, current_entity: entity, &deserializer_block)
148
+ else
149
+ deserializer.new(params, **options, current_entity: entity).to_hash
150
+ end
151
+ end
152
+ end
153
+ end