skinny_controllers 0.7.4 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +180 -8
- data/lib/generators/operation/USAGE +5 -0
- data/lib/generators/operation/operation_generator.rb +16 -0
- data/lib/generators/operation/templates/operation.rb.erb +64 -0
- data/lib/generators/policy/USAGE +5 -0
- data/lib/generators/policy/policy_generator.rb +20 -0
- data/lib/generators/policy/templates/policy.rb.erb +40 -0
- data/lib/generators/skinny_controller/USAGE +5 -0
- data/lib/generators/skinny_controller/skinny_controller_generator.rb +28 -0
- data/lib/generators/skinny_controller/templates/skinny_controller.rb.erb +24 -0
- data/lib/skinny_controllers/operation/model_helpers.rb +2 -2
- data/lib/skinny_controllers/version.rb +1 -1
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5387b4c2379bd53d4a19a66243c529d8e764dfab
|
4
|
+
data.tar.gz: 5c8df4c666ae85e6f762bb68391f06c03c5e21e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6a9c45f2ba2def7309e979a060a87f984640c03e33340778547f6fae68c56289c9d8f089e5092fd24fed89fef70ca7752095853a2ce219dd1021438cd5ddafe
|
7
|
+
data.tar.gz: 93434e6dd65a1c1a650810acb9b95fae6382570dbe908e538f84206c9f22790da2cfd4c4644eedc510309d9f2cf60c9b0e15700efa09f528b2c0735b0fc0f724
|
data/README.md
CHANGED
@@ -22,6 +22,20 @@ or
|
|
22
22
|
|
23
23
|
`gem install skinny_controllers`
|
24
24
|
|
25
|
+
|
26
|
+
## Generators
|
27
|
+
|
28
|
+
```
|
29
|
+
rails g operation event_summary
|
30
|
+
# => create app/operations/event_summary_operations.rb
|
31
|
+
|
32
|
+
rails g policy event_summary
|
33
|
+
# create app/policies/event_summary_policy.rb
|
34
|
+
|
35
|
+
rails g skinny_controller event_summaries
|
36
|
+
# create app/controllers/event_summaries_controller.rb
|
37
|
+
```
|
38
|
+
|
25
39
|
# Usage
|
26
40
|
|
27
41
|
## In a controller:
|
@@ -46,6 +60,8 @@ The above does a multitude of assumptions to make sure that you can type the lea
|
|
46
60
|
5. If relying on the default / implicit operations for create and update, the params key for your model's changes much be formatted as `{ Model.name.underscore => { attributes }}``
|
47
61
|
6. If using strong parameters, SkinnyControllers will look for `{action}_{model}_params` then `{model}_params` and then `params`. See the `strong_parameters_spec.rb` test to see an example.
|
48
62
|
|
63
|
+
|
64
|
+
|
49
65
|
### Your model name might be different from your resource name
|
50
66
|
Lets say you have a JSON API resource that you'd like to render that has some additional/subset of data.
|
51
67
|
Maybe the model is an `Event`, and the resource an `EventSummary` (which could do some aggregation of `Event` data).
|
@@ -244,6 +260,158 @@ end
|
|
244
260
|
```
|
245
261
|
|
246
262
|
|
263
|
+
## More Advanced Usage
|
264
|
+
|
265
|
+
These are snippets taking from other projects.
|
266
|
+
|
267
|
+
### Finding a record when the id parameter isn't passed
|
268
|
+
|
269
|
+
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
module HostOperations
|
273
|
+
class Read < SkinnyControllers::Operation::Base
|
274
|
+
def run
|
275
|
+
model # always allowed, never restricted
|
276
|
+
end
|
277
|
+
|
278
|
+
# Needs to be overridden, because a 'host' can be either
|
279
|
+
# an Event or an Organization.
|
280
|
+
#
|
281
|
+
# the params to this method should include the subdomain
|
282
|
+
# e.g.: { subdomain: 'swingin2015' }
|
283
|
+
def model_from_params
|
284
|
+
subdomain = params[:subdomain]
|
285
|
+
# first check the events, since those are more commonly used
|
286
|
+
host = Event.find_by_domain(subdomain)
|
287
|
+
# if the event doesn't exist, see if we have an organization
|
288
|
+
host ||= Organization.find_by_domain(subdomain)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
### The built in model-finding methods can be completely ignored
|
295
|
+
|
296
|
+
The `model` method does not need to be overridden. `run` is what is called on the operation.
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
module MembershipRenewalOperations
|
300
|
+
# MembershipRenewalsController#index
|
301
|
+
class ReadAll < SkinnyControllers::Operation::Base
|
302
|
+
|
303
|
+
def run
|
304
|
+
# default 'model' functionality is avoided
|
305
|
+
latest_renewals
|
306
|
+
end
|
307
|
+
|
308
|
+
private
|
309
|
+
|
310
|
+
def organization
|
311
|
+
id = params[:organization_id]
|
312
|
+
Organization.find(id)
|
313
|
+
end
|
314
|
+
|
315
|
+
def renewals
|
316
|
+
options = organization.membership_options.includes(renewals: [:user, :membership_option])
|
317
|
+
options.map(&:renewals).flatten
|
318
|
+
end
|
319
|
+
|
320
|
+
def latest_renewals
|
321
|
+
sorted_renewals = renewals.sort_by{|r| [r.user_id,r.updated_at]}.reverse
|
322
|
+
|
323
|
+
# unique picks the first option.
|
324
|
+
# so, because the list is sorted by user id, then updated at,
|
325
|
+
# for each user, the first renewal will be chosen...
|
326
|
+
# and because it is descending, that means the most recent renewal
|
327
|
+
sorted_renewals.uniq{|r| r.user_id}
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
## Testing
|
335
|
+
|
336
|
+
The whole goal of this project is to minimize the complexity or existence of controller tests, and provide
|
337
|
+
a way to unit test business logic.
|
338
|
+
|
339
|
+
In the following examples, I'll be using RSpec -- but there isn't anything that would prevent you from using a different testing framework, if you so choose.
|
340
|
+
|
341
|
+
### Operations
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
describe HostOperations do
|
345
|
+
describe HostOperations::Read do
|
346
|
+
context 'model_from_params' do
|
347
|
+
let(:subdomain){ 'subdomain' }
|
348
|
+
# an operation takes a user, and a list of params
|
349
|
+
# there are optional parameters as well, but generally may not be required.
|
350
|
+
# see: `SkinnyControllers:::Operation::Base`
|
351
|
+
let(:operation){ HostOperations::Read.new(nil, { subdomain: subdomain }) }
|
352
|
+
|
353
|
+
it 'finds an event' do
|
354
|
+
event = create(:event, domain: subdomain)
|
355
|
+
model = operation.run
|
356
|
+
expect(model).to eq event
|
357
|
+
end
|
358
|
+
#...
|
359
|
+
```
|
360
|
+
|
361
|
+
### Policies
|
362
|
+
|
363
|
+
With policies, I like to test using Procs, because the setup is the same for most actions, and it's easier to set up different scenarios.
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
describe PackagePolicy do
|
367
|
+
# will test if the owner of this object can access it
|
368
|
+
let(:by_owner){
|
369
|
+
->(method){
|
370
|
+
package = create(:package)
|
371
|
+
# a policy takes a user and an object
|
372
|
+
policy = PackagePolicy.new(package.event.hosted_by, package)
|
373
|
+
policy.send(method)
|
374
|
+
}
|
375
|
+
}
|
376
|
+
|
377
|
+
# will test if the person registering with this package has permission
|
378
|
+
let(:by_registrant){
|
379
|
+
->(method){
|
380
|
+
event = create(:event)
|
381
|
+
package = create(:package, event: event)
|
382
|
+
attendance = create(:attendance, host: event, package: package)
|
383
|
+
# a policy takes a user and an object
|
384
|
+
policy = PackagePolicy.new(attendance.attendee, package)
|
385
|
+
policy.send(method)
|
386
|
+
}
|
387
|
+
}
|
388
|
+
|
389
|
+
context 'can be read?' do
|
390
|
+
it 'by the event owner' do
|
391
|
+
result = by_owner.call(:read?)
|
392
|
+
expect(result).to eq true
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'by a registrant' do
|
396
|
+
result = by_registrant.call(:read?)
|
397
|
+
expect(result).to eq true
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
context 'can be updated?' do
|
402
|
+
it 'by the event owner' do
|
403
|
+
result = by_owner.call(:update?)
|
404
|
+
expect(result).to eq true
|
405
|
+
end
|
406
|
+
|
407
|
+
it 'by a registrant' do
|
408
|
+
result = by_registrant.call(:update?)
|
409
|
+
expect(result).to eq false
|
410
|
+
end
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
|
247
415
|
## Globally Configurable Options
|
248
416
|
|
249
417
|
All of these can be set on `SkinnyControllers`,
|
@@ -266,17 +434,21 @@ The following options are available:
|
|
266
434
|
|`action_map`| see [skinny_controllers.rb](./lib/skinny_controllers.rb#L61)| |
|
267
435
|
|
268
436
|
|
437
|
+
|
438
|
+
|
439
|
+
|
440
|
+
|
269
441
|
-------------------------------------------------------
|
270
442
|
|
271
443
|
## How is this different from trailblazer?
|
272
444
|
|
273
445
|
This may not be horribly apparent, but here is a table overviewing some highlevel differences
|
274
446
|
|
275
|
-
| Feature | skinny_controllers | [trailblazer](https://github.com/apotonick/trailblazer) |
|
276
|
-
|
277
|
-
| Purpose
|
278
|
-
| Added Layers
|
279
|
-
| Validation
|
280
|
-
| Additional objects
|
281
|
-
| Rendering
|
282
|
-
| App Structure
|
447
|
+
| Feature | - | skinny_controllers | [trailblazer](https://github.com/apotonick/trailblazer) |
|
448
|
+
|----|----|----|----|
|
449
|
+
| Purpose |-| API - works very well with [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers)| General - additional featers for server-side rendered views |
|
450
|
+
| Added Layers |-| Operations, Policies | Operations, Policies, Forms |
|
451
|
+
| Validation |-| stay in models | moved to operations via contract block |
|
452
|
+
| Additional objects|-| none | contacts, representers, callbacks, cells |
|
453
|
+
| Rendering |-| done in the controller, and up to the dev to decide how that is done. `ActiveModel::Serializers` with JSON-API is highly recommended |-| representers provide a way to define serializers for json, xml, json-api, etc |
|
454
|
+
| App Structure |-| same as rails. `app/operations` and `app/policies` are added | encourages a new structure 'concepts', where cells, view templates, assets, operations, etc are all under `concepts/{model-name}` |
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class OperationGenerator < Rails::Generators::NamedBase
|
2
|
+
# gives us file_name
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def generate_layout
|
6
|
+
template 'operation.rb.erb', File.join('app/operations', class_path, "#{file_name}_operations.rb")
|
7
|
+
end
|
8
|
+
|
9
|
+
def operation_name
|
10
|
+
file_name.camelize
|
11
|
+
end
|
12
|
+
|
13
|
+
def controller_name
|
14
|
+
operation_name.pluralize + 'Controller'
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module <%= operation_name %>Operations
|
2
|
+
|
3
|
+
# Below are all the available actions an Operation can have.
|
4
|
+
# These directly correlate to the controller actions. None of
|
5
|
+
# the operations below are required. Default functionality is basic CRUD,
|
6
|
+
# and to always allow the model object to return.
|
7
|
+
# See: SkinnyControllers::Operation::Default#run
|
8
|
+
#
|
9
|
+
# NOTE: If each operation gets big enough, it may be desirable to
|
10
|
+
# move each operation in to its own file.
|
11
|
+
#
|
12
|
+
# In every operation, the following variables are available to you:
|
13
|
+
# - current_user
|
14
|
+
# - params
|
15
|
+
# - params_for_action - params specific to the action, if configured
|
16
|
+
# - action - current controller action
|
17
|
+
# - model_key - the underscored model_name
|
18
|
+
# - model_params - params based on the model_key in params
|
19
|
+
#
|
20
|
+
# Methods:
|
21
|
+
# - allowed? - calls allowed_for?(model)
|
22
|
+
# - allowed_for? - allows you to pass an object to the policy that corresponds
|
23
|
+
# to this operation
|
24
|
+
|
25
|
+
# # <%= controller_name %>#create
|
26
|
+
# class Create < SkinnyControllers::Operation::Base
|
27
|
+
# def run
|
28
|
+
# # @model is used here, because the `model` method is memoized using
|
29
|
+
# # the @model instance variable
|
30
|
+
# @model = model_class.new(model_params)
|
31
|
+
# @model.save
|
32
|
+
# @model
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # <%= controller_name %>#index
|
37
|
+
# class ReadAll < SkinnyControllers::Operation::Base
|
38
|
+
# def run
|
39
|
+
# # model is a list of model_class instances
|
40
|
+
# model if allowed?
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # <%= controller_name %>#show
|
45
|
+
# class Read < SkinnyControllers::Operation::Base
|
46
|
+
# def run
|
47
|
+
# model if allowed?
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # <%= controller_name %>#update
|
52
|
+
# class Update < SkinnyControllers::Operation::Base
|
53
|
+
# def run
|
54
|
+
# model.update(model_params) if allowed?
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# # <%= controller_name %>#destroy
|
59
|
+
# class Delete < SkinnyControllers::Operation::Base
|
60
|
+
# def run
|
61
|
+
# model.destroy if allowed?
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class PolicyGenerator < Rails::Generators::NamedBase
|
2
|
+
# gives us file_name
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def generate_layout
|
6
|
+
template 'policy.rb.erb', File.join('app/policies', class_path, "#{file_name}_policy.rb")
|
7
|
+
end
|
8
|
+
|
9
|
+
def policy_name
|
10
|
+
operation_name + 'Policy'
|
11
|
+
end
|
12
|
+
|
13
|
+
def operation_name
|
14
|
+
file_name.camelize
|
15
|
+
end
|
16
|
+
|
17
|
+
def controller_name
|
18
|
+
operation_name.pluralize + 'Controller'
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class <%= policy_name %> < SkinnyControllers::Policy::Base
|
2
|
+
|
3
|
+
# Below are all the available permissions. Each permission corresponds
|
4
|
+
# to an action in the controller.
|
5
|
+
# Default functionality is to return true (allow) -- so the methods
|
6
|
+
# below do not need to exist, unless you want to add custom logic
|
7
|
+
# to them.
|
8
|
+
#
|
9
|
+
# The following variables are available to you in each policy
|
10
|
+
# - object - the object the user is trying to access.
|
11
|
+
# - user - the current user
|
12
|
+
#
|
13
|
+
|
14
|
+
# <%= controller_name %>#index
|
15
|
+
def read_all?
|
16
|
+
default? # SkinnyControllers.allow_by_default # aka "true"
|
17
|
+
end
|
18
|
+
|
19
|
+
# <%= controller_name %>#show
|
20
|
+
def read?
|
21
|
+
default? # SkinnyControllers.allow_by_default # aka "true"
|
22
|
+
end
|
23
|
+
|
24
|
+
# <%= controller_name %>#create
|
25
|
+
def create?
|
26
|
+
default? # SkinnyControllers.allow_by_default # aka "true"
|
27
|
+
end
|
28
|
+
|
29
|
+
# <%= controller_name %>#update
|
30
|
+
def update?
|
31
|
+
default? # SkinnyControllers.allow_by_default # aka "true"
|
32
|
+
end
|
33
|
+
|
34
|
+
# <%= controller_name %>#destroy
|
35
|
+
def delete?
|
36
|
+
default? # SkinnyControllers.allow_by_default # aka "true"
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class SkinnyControllerGenerator < Rails::Generators::NamedBase
|
2
|
+
# gives us file_name
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def generate_layout
|
6
|
+
template 'skinny_controller.rb.erb', File.join('app/controllers', class_path, "#{file_name}_controller.rb")
|
7
|
+
end
|
8
|
+
|
9
|
+
def operation_name
|
10
|
+
file_name.camelize
|
11
|
+
end
|
12
|
+
|
13
|
+
def controller_name
|
14
|
+
operation_name.pluralize + 'Controller'
|
15
|
+
end
|
16
|
+
|
17
|
+
def parent_class
|
18
|
+
if defined?(::ApiController)
|
19
|
+
'ApiController'
|
20
|
+
elsif defined?(::APIController)
|
21
|
+
'APIController'
|
22
|
+
elsif defined?(::ApplicationController)
|
23
|
+
'ApplicationController'
|
24
|
+
else
|
25
|
+
'ActionController::Base'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class <%= controller_name %> < <%= parent_class %>
|
2
|
+
include SkinnyControllers::Diet
|
3
|
+
|
4
|
+
def index
|
5
|
+
render json: model
|
6
|
+
end
|
7
|
+
|
8
|
+
def create
|
9
|
+
render json: model
|
10
|
+
end
|
11
|
+
|
12
|
+
def show
|
13
|
+
render json: model
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
render json: model
|
18
|
+
end
|
19
|
+
|
20
|
+
def destroy
|
21
|
+
render json: model
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -50,7 +50,7 @@ module SkinnyControllers
|
|
50
50
|
|
51
51
|
def model_param_name
|
52
52
|
# model_key comes from Operation::Base
|
53
|
-
|
53
|
+
model_key || model_name.underscore
|
54
54
|
end
|
55
55
|
|
56
56
|
# @param [Hash] scoped_params
|
@@ -95,7 +95,7 @@ module SkinnyControllers
|
|
95
95
|
association = association_name_from_object
|
96
96
|
scoped.send(association)
|
97
97
|
else
|
98
|
-
|
98
|
+
raise "Parent object of type #{scope[:type]} not accessible"
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: skinny_controllers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- L. Preston Sego III
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-03-
|
11
|
+
date: 2016-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -186,6 +186,15 @@ extensions: []
|
|
186
186
|
extra_rdoc_files: []
|
187
187
|
files:
|
188
188
|
- README.md
|
189
|
+
- lib/generators/operation/USAGE
|
190
|
+
- lib/generators/operation/operation_generator.rb
|
191
|
+
- lib/generators/operation/templates/operation.rb.erb
|
192
|
+
- lib/generators/policy/USAGE
|
193
|
+
- lib/generators/policy/policy_generator.rb
|
194
|
+
- lib/generators/policy/templates/policy.rb.erb
|
195
|
+
- lib/generators/skinny_controller/USAGE
|
196
|
+
- lib/generators/skinny_controller/skinny_controller_generator.rb
|
197
|
+
- lib/generators/skinny_controller/templates/skinny_controller.rb.erb
|
189
198
|
- lib/skinny_controllers.rb
|
190
199
|
- lib/skinny_controllers/default_verbs.rb
|
191
200
|
- lib/skinny_controllers/diet.rb
|
@@ -225,5 +234,5 @@ rubyforge_project:
|
|
225
234
|
rubygems_version: 2.4.8
|
226
235
|
signing_key:
|
227
236
|
specification_version: 4
|
228
|
-
summary: SkinnyControllers-0.
|
237
|
+
summary: SkinnyControllers-0.8.0
|
229
238
|
test_files: []
|