snfoil-controller 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +201 -0
- data/README.md +537 -0
- data/lib/snfoil/controller/version.rb +21 -0
- data/lib/snfoil/controller.rb +153 -0
- data/lib/snfoil/deserializer/base.rb +128 -0
- data/lib/snfoil/deserializer/json.rb +98 -0
- data/lib/snfoil/deserializer/jsonapi.rb +116 -0
- data/snfoil-controller.gemspec +44 -0
- metadata +228 -0
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
|