skinny_controllers 0.10.6 → 0.10.7
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.
- checksums.yaml +4 -4
- data/README.md +11 -433
- data/lib/skinny_controllers/diet.rb +21 -9
- data/lib/skinny_controllers/policy/base.rb +0 -4
- data/lib/skinny_controllers/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec8abdfb85c06f6eb9502eed56d1c9aaefe44dae
|
4
|
+
data.tar.gz: 1cb87906f028c8e73e8c6cffb0d1c4e4ede94b4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93867fac825426ab77574c718b2a8b4027dc3c32a2c0e72dd7764757c49e2a612e87aa1445f217e1a160534cb7f72859e72c4e771e3a5e56a3859f60f3300eab
|
7
|
+
data.tar.gz: 89bc46531b3fa9fe7a0c1dac5cc7f877181814a0a457460d88fe8477ce145d782ecd466d7009c8ec546889a76a3fde7706f6fd6d01ab723de6462ad59639ef7a
|
data/README.md
CHANGED
@@ -38,20 +38,6 @@ or
|
|
38
38
|
gem install skinny_controllers
|
39
39
|
```
|
40
40
|
|
41
|
-
|
42
|
-
## Generators
|
43
|
-
|
44
|
-
```bash
|
45
|
-
rails g operation event_summary
|
46
|
-
# => create app/operations/event_summary_operations.rb
|
47
|
-
|
48
|
-
rails g policy event_summary
|
49
|
-
# create app/policies/event_summary_policy.rb
|
50
|
-
|
51
|
-
rails g skinny_controller event_summaries
|
52
|
-
# create app/controllers/event_summaries_controller.rb
|
53
|
-
```
|
54
|
-
|
55
41
|
# Usage
|
56
42
|
|
57
43
|
## In a controller:
|
@@ -65,117 +51,6 @@ render json: model
|
|
65
51
|
|
66
52
|
and that's it!
|
67
53
|
|
68
|
-
The above does a multitude of assumptions to make sure that you can type the least amount code possible.
|
69
|
-
|
70
|
-
1. Your controller name is the name of your _resource_.
|
71
|
-
2. Any defined policies or operations follow the formats (though they don't have to exist):
|
72
|
-
- `class #{resource_name}Policy`
|
73
|
-
- `module #{resource_name}Operations`
|
74
|
-
3. Your model responds to `find`, and `where`
|
75
|
-
4. 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 }}`
|
76
|
-
5. 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.
|
77
|
-
|
78
|
-
### Per Controller Configuration
|
79
|
-
|
80
|
-
```ruby
|
81
|
-
skinny_controllers_config model_class: AClass,
|
82
|
-
parent_class: ParentClass,
|
83
|
-
asociation_name: :association_aclasses,
|
84
|
-
model_params_key: :aclass
|
85
|
-
```
|
86
|
-
|
87
|
-
#### model_class
|
88
|
-
Lets say you have a JSON API resource that you'd like to render that has some additional/subset of data.
|
89
|
-
Maybe the model is an `Event`, and the resource an `EventSummary` (which could do some aggregation of `Event` data).
|
90
|
-
|
91
|
-
The naming of all the objects should be as follows:
|
92
|
-
- `EventSummariesController`
|
93
|
-
- `EventSummaryOperations::*`
|
94
|
-
- `EventSummaryPolicy`
|
95
|
-
- and the model is still `Event`
|
96
|
-
|
97
|
-
In `EventSummariesController`, you would make the following additions:
|
98
|
-
```ruby
|
99
|
-
class EventSummariesController < ApiController # or whatever your superclass is
|
100
|
-
include SkinnyControllers::Diet
|
101
|
-
|
102
|
-
skinny_controllers_config model_class: Event
|
103
|
-
|
104
|
-
def index
|
105
|
-
render json: model, each_serializer: EventSummariesSerializer
|
106
|
-
end
|
107
|
-
|
108
|
-
def show
|
109
|
-
render json: model, serializer: EventSummariesSerializer
|
110
|
-
end
|
111
|
-
end
|
112
|
-
```
|
113
|
-
|
114
|
-
Note that `each_serializer` and `serializer` is not part of `SkinnyControllers`, and is part of [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers).
|
115
|
-
|
116
|
-
Also note that setting `model_class` may be required if your model is namespaced.
|
117
|
-
|
118
|
-
#### parent_class and association_name
|
119
|
-
|
120
|
-
If you want to scope the finding of a resource to a parent object, `parent_class` must be set
|
121
|
-
|
122
|
-
```ruby
|
123
|
-
skinny_controllers_config parent_class: ParentClass,
|
124
|
-
assaciation_name: :children,
|
125
|
-
model_class: Child
|
126
|
-
```
|
127
|
-
|
128
|
-
Given the above configuration in a controller, and a request with the params:
|
129
|
-
|
130
|
-
```
|
131
|
-
{
|
132
|
-
id: 2,
|
133
|
-
parent_id: 78
|
134
|
-
}
|
135
|
-
```
|
136
|
-
|
137
|
-
The following query will be made:
|
138
|
-
|
139
|
-
```ruby
|
140
|
-
Parent.find(78).children.find(2)
|
141
|
-
```
|
142
|
-
|
143
|
-
#### model_params_key
|
144
|
-
|
145
|
-
Date stored another a different key in `params`?
|
146
|
-
|
147
|
-
```ruby
|
148
|
-
skinny_controllers_config model_class: Child,
|
149
|
-
model_params_key: :progeny
|
150
|
-
```
|
151
|
-
|
152
|
-
Given the above configuration in a controller, and a request with the params:
|
153
|
-
|
154
|
-
```
|
155
|
-
{
|
156
|
-
progeny: {
|
157
|
-
attribute1: 'value'
|
158
|
-
}
|
159
|
-
}
|
160
|
-
```
|
161
|
-
|
162
|
-
The attributes inside the `progeny` sub hash will be used instead of the default, `child`.
|
163
|
-
|
164
|
-
### What if your model is namespaced?
|
165
|
-
|
166
|
-
All you have to do is set the `model_class`, and `model_key`.
|
167
|
-
|
168
|
-
```ruby
|
169
|
-
class ItemsController < ApiController # or whatever your superclass is
|
170
|
-
include SkinnyControllers::Diet
|
171
|
-
|
172
|
-
skinny_controllers_config model_class: NameSpace::Item
|
173
|
-
model_params_key: :item
|
174
|
-
end
|
175
|
-
```
|
176
|
-
`model_key` specifies the key to look for params when creating / updating the model.
|
177
|
-
|
178
|
-
Note that while `model_key` doesn't *have* to be specified, it would default to name_space/item. So, just keep that in mind.
|
179
54
|
|
180
55
|
### What if you want to call your own operations?
|
181
56
|
|
@@ -227,314 +102,6 @@ end
|
|
227
102
|
|
228
103
|
Note that we don't need the id under the data hash, because in a RESTful api, the id will be available to us through the top level params hash.
|
229
104
|
|
230
|
-
|
231
|
-
## Defining Operations
|
232
|
-
|
233
|
-
Operations should be placed in `app/operations` of your rails app.
|
234
|
-
|
235
|
-
For operations concerning an `Event`, they should be under `app/operations/event_operations/`.
|
236
|
-
|
237
|
-
Using the example from the specs:
|
238
|
-
```ruby
|
239
|
-
module EventOperations
|
240
|
-
class Read < SkinnyControllers::Operation::Base
|
241
|
-
def run
|
242
|
-
model if allowed?
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
```
|
247
|
-
|
248
|
-
alternatively, all operation verbs can be stored in the same file under (for example) `app/operations/user_operations.rb`
|
249
|
-
|
250
|
-
```ruby
|
251
|
-
module UserOperations
|
252
|
-
class Read < SkinnyControllers::Operation::Base
|
253
|
-
def run
|
254
|
-
model if allowed?
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
class ReadAll < SkinnyControllers::Operation::Base
|
259
|
-
def run
|
260
|
-
model if allowed?
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
|
-
```
|
265
|
-
|
266
|
-
### Creating
|
267
|
-
|
268
|
-
To achieve default functionality, this operation *may* be defined -- though, it is implicitly assumed to function this way if not defined.
|
269
|
-
```ruby
|
270
|
-
module UserOperations
|
271
|
-
class Create < SkinnyControllers::Operation::Base
|
272
|
-
def run
|
273
|
-
@model = User.new(model_params)
|
274
|
-
|
275
|
-
# raising an exception here allows the corresponding resource controller to
|
276
|
-
# `rescue_from SkinnyControllers::DeniedByPolicy` and have a uniform error
|
277
|
-
# returned to the frontend
|
278
|
-
raise SkinnyControllers::DeniedByPolicy.new('Something Horrible') unless allowed?
|
279
|
-
|
280
|
-
@model.save
|
281
|
-
return @model # or just `model`
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
285
|
-
```
|
286
|
-
|
287
|
-
### Updating
|
288
|
-
```ruby
|
289
|
-
module UserOperations
|
290
|
-
class Update < SkinnyControllers::Operation::Base
|
291
|
-
def run
|
292
|
-
# this throws a DeniedByPolicy exception if `allowed?` returns false
|
293
|
-
check_allowed!
|
294
|
-
|
295
|
-
model.update(model_params)
|
296
|
-
model
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
300
|
-
```
|
301
|
-
|
302
|
-
### Deleting
|
303
|
-
|
304
|
-
Goal: Users should only be able to delete themselves
|
305
|
-
|
306
|
-
To achieve default functionality, this operation *may* be defined -- though, it is implicitly assumed to function this way if not defined.
|
307
|
-
```ruby
|
308
|
-
module UserOperations
|
309
|
-
class Delete < SkinnyControllers::Operation::Base
|
310
|
-
def run
|
311
|
-
model.destroy if allowed?
|
312
|
-
end
|
313
|
-
end
|
314
|
-
end
|
315
|
-
```
|
316
|
-
|
317
|
-
NOTE: `allowed?` is `true` by default via the `SkinnyControllers.allow_by_default` option.
|
318
|
-
|
319
|
-
## Defining Policies
|
320
|
-
|
321
|
-
Policies should be placed in `app/policies` of your rails app.
|
322
|
-
These are where you define your access logic, and how to decide if a user has access to the `object`
|
323
|
-
|
324
|
-
```ruby
|
325
|
-
class EventPolicy < SkinnyControllers::Policy::Base
|
326
|
-
def read?(event = object)
|
327
|
-
event.user == user
|
328
|
-
end
|
329
|
-
end
|
330
|
-
```
|
331
|
-
|
332
|
-
|
333
|
-
## More Advanced Usage
|
334
|
-
|
335
|
-
These are snippets taking from other projects.
|
336
|
-
|
337
|
-
### Using ransack
|
338
|
-
|
339
|
-
```ruby
|
340
|
-
# config/initializers/skinny_controllers.rb
|
341
|
-
SkinnyControllers.search_proc = lambda do |relation|
|
342
|
-
relation.ransack(params[:q]).result
|
343
|
-
end
|
344
|
-
```
|
345
|
-
|
346
|
-
### Finding a record when the id parameter isn't passed
|
347
|
-
|
348
|
-
```ruby
|
349
|
-
module HostOperations
|
350
|
-
class Read < SkinnyControllers::Operation::Base
|
351
|
-
def run
|
352
|
-
# always allowed, never restricted
|
353
|
-
# (because there is now call to allowed?)
|
354
|
-
model
|
355
|
-
end
|
356
|
-
|
357
|
-
# the params to this method should include the subdomain
|
358
|
-
# e.g.: { subdomain: 'swingin2015' }
|
359
|
-
def model_from_params
|
360
|
-
subdomain = params[:subdomain]
|
361
|
-
host = Host.find_by_subdomain(subdomain)
|
362
|
-
end
|
363
|
-
end
|
364
|
-
end
|
365
|
-
```
|
366
|
-
|
367
|
-
### The built in model-finding methods can be completely ignored
|
368
|
-
|
369
|
-
The `model` method does not need to be overridden. `run` is what is called on the operation.
|
370
|
-
|
371
|
-
```ruby
|
372
|
-
module MembershipRenewalOperations
|
373
|
-
# MembershipRenewalsController#index
|
374
|
-
class ReadAll < SkinnyControllers::Operation::Base
|
375
|
-
|
376
|
-
def run
|
377
|
-
# default 'model' functionality is avoided
|
378
|
-
latest_renewals
|
379
|
-
end
|
380
|
-
|
381
|
-
private
|
382
|
-
|
383
|
-
def organization
|
384
|
-
id = params[:organization_id]
|
385
|
-
Organization.find(id)
|
386
|
-
end
|
387
|
-
|
388
|
-
def renewals
|
389
|
-
options = organization.membership_options.includes(renewals: [:user, :membership_option])
|
390
|
-
options.map(&:renewals).flatten
|
391
|
-
end
|
392
|
-
|
393
|
-
def latest_renewals
|
394
|
-
sorted_renewals = renewals.sort_by{|r| [r.user_id,r.updated_at]}.reverse
|
395
|
-
|
396
|
-
# unique picks the first option.
|
397
|
-
# so, because the list is sorted by user id, then updated at,
|
398
|
-
# for each user, the first renewal will be chosen...
|
399
|
-
# and because it is descending, that means the most recent renewal
|
400
|
-
sorted_renewals.uniq { |r| r.user_id }
|
401
|
-
end
|
402
|
-
end
|
403
|
-
|
404
|
-
end
|
405
|
-
```
|
406
|
-
|
407
|
-
### Updating / Deleting the current_user
|
408
|
-
|
409
|
-
This is something you could do if you always know your model ahead of time.
|
410
|
-
|
411
|
-
```ruby
|
412
|
-
module UserOperations
|
413
|
-
class Update < SkinnyControllers::Operation::Base
|
414
|
-
def run
|
415
|
-
return unless allowed_for?(current_user)
|
416
|
-
# update with password provided by Devise
|
417
|
-
current_user.update_with_password(model_params)
|
418
|
-
current_user
|
419
|
-
end
|
420
|
-
end
|
421
|
-
|
422
|
-
class Delete < SkinnyControllers::Operation::Base
|
423
|
-
def run
|
424
|
-
if allowed_for?(current_user)
|
425
|
-
if current_user.upcoming_events.count > 0
|
426
|
-
current_user.errors.add(
|
427
|
-
:base,
|
428
|
-
"You cannot delete your account when you are about to attend an event."
|
429
|
-
)
|
430
|
-
else
|
431
|
-
current_user.destroy
|
432
|
-
end
|
433
|
-
|
434
|
-
current_user
|
435
|
-
end
|
436
|
-
end
|
437
|
-
end
|
438
|
-
|
439
|
-
end
|
440
|
-
```
|
441
|
-
|
442
|
-
## Testing
|
443
|
-
|
444
|
-
The whole goal of this project is to minimize the complexity or existence of controller tests, and provide
|
445
|
-
a way to unit test business logic.
|
446
|
-
|
447
|
-
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.
|
448
|
-
|
449
|
-
### Operations
|
450
|
-
|
451
|
-
```ruby
|
452
|
-
describe HostOperations do
|
453
|
-
describe HostOperations::Read do
|
454
|
-
context 'model_from_params' do
|
455
|
-
let(:subdomain){ 'subdomain' }
|
456
|
-
# an operation takes a user, and a list of params
|
457
|
-
# there are optional parameters as well, but generally may not be required.
|
458
|
-
# see: `SkinnyControllers:::Operation::Base`
|
459
|
-
let(:operation){ HostOperations::Read.new(nil, { subdomain: subdomain }) }
|
460
|
-
|
461
|
-
it 'finds an event' do
|
462
|
-
host = create(:host, domain: subdomain)
|
463
|
-
model = operation.run
|
464
|
-
expect(model).to eq host
|
465
|
-
end
|
466
|
-
#...
|
467
|
-
```
|
468
|
-
|
469
|
-
### Policies
|
470
|
-
|
471
|
-
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.
|
472
|
-
|
473
|
-
```ruby
|
474
|
-
describe PackagePolicy do
|
475
|
-
# will test if the owner of this object can access it
|
476
|
-
let(:by_owner){
|
477
|
-
->(method){
|
478
|
-
package = create(:package)
|
479
|
-
# a policy takes a user and an object
|
480
|
-
policy = PackagePolicy.new(package.event.hosted_by, package)
|
481
|
-
policy.send(method)
|
482
|
-
}
|
483
|
-
}
|
484
|
-
|
485
|
-
# will test if the person registering with this package has permission
|
486
|
-
let(:by_registrant){
|
487
|
-
->(method){
|
488
|
-
event = create(:event)
|
489
|
-
package = create(:package, event: event)
|
490
|
-
attendance = create(:attendance, host: event, package: package)
|
491
|
-
# a policy takes a user and an object
|
492
|
-
policy = PackagePolicy.new(attendance.attendee, package)
|
493
|
-
policy.send(method)
|
494
|
-
}
|
495
|
-
}
|
496
|
-
|
497
|
-
context 'can be read?' do
|
498
|
-
it 'by the event owner' do
|
499
|
-
result = by_owner.call(:read?)
|
500
|
-
expect(result).to eq true
|
501
|
-
end
|
502
|
-
|
503
|
-
it 'by a registrant' do
|
504
|
-
result = by_registrant.call(:read?)
|
505
|
-
expect(result).to eq true
|
506
|
-
end
|
507
|
-
end
|
508
|
-
|
509
|
-
context 'can be updated?' do
|
510
|
-
it 'by the event owner' do
|
511
|
-
result = by_owner.call(:update?)
|
512
|
-
expect(result).to eq true
|
513
|
-
end
|
514
|
-
|
515
|
-
it 'by a registrant' do
|
516
|
-
result = by_registrant.call(:update?)
|
517
|
-
expect(result).to eq false
|
518
|
-
end
|
519
|
-
end
|
520
|
-
```
|
521
|
-
|
522
|
-
|
523
|
-
## Globally Configurable Options
|
524
|
-
|
525
|
-
The following options are available:
|
526
|
-
|
527
|
-
|Option|Default|Note|
|
528
|
-
|------|-------|----|
|
529
|
-
|`operations_namespace` | '' | Optional namespace to put all the operations in. |
|
530
|
-
|`operations_suffix`|`'Operations'` | Default suffix for the operations namespaces. |
|
531
|
-
|`policy_suffix`|`'Policy'` | Default suffix for policies classes. |
|
532
|
-
|`controller_namespace`|`''`| Global Namespace for all controllers (e.g.: `'API'`) |
|
533
|
-
|`allow_by_default`| `true` | Default permission |
|
534
|
-
|`action_map`| see [skinny_controllers.rb](./lib/skinny_controllers.rb#L61)| |
|
535
|
-
| `search_proc`| passthrough | can be used to filter results, such as with using ransack |
|
536
|
-
|
537
|
-
|
538
105
|
-------------------------------------------------------
|
539
106
|
|
540
107
|
## How is this different from trailblazer?
|
@@ -549,3 +116,14 @@ This may not be horribly apparent, but here is a table overviewing some highleve
|
|
549
116
|
| Additional objects|-| none | contacts, representers, callbacks, cells |
|
550
117
|
| 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 |
|
551
118
|
| 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}` |
|
119
|
+
|
120
|
+
|
121
|
+
# Contributing
|
122
|
+
|
123
|
+
Please refer to each project's style guidelines and guidelines for submitting patches and additions. In general, we follow the "fork-and-pull" Git workflow.
|
124
|
+
|
125
|
+
1. **Fork** the repo on GitHub
|
126
|
+
2. **Clone** the project to your own machine
|
127
|
+
3. **Commit** changes to your own branch
|
128
|
+
4. **Push** your work back up to your fork
|
129
|
+
5. Submit a **Pull request** so that we can review your changes
|
@@ -77,7 +77,9 @@ module SkinnyControllers
|
|
77
77
|
|
78
78
|
# In order of most specific, to least specific:
|
79
79
|
# - {action}_{model_name}_params
|
80
|
-
# - {
|
80
|
+
# - {action}_params
|
81
|
+
# - {model_key}_params
|
82
|
+
# - resource_params
|
81
83
|
# - params
|
82
84
|
#
|
83
85
|
# It's recommended to use whitelisted strong parameters on
|
@@ -100,16 +102,26 @@ module SkinnyControllers
|
|
100
102
|
_lookup.model_name.underscore
|
101
103
|
end
|
102
104
|
|
103
|
-
|
104
|
-
|
105
|
+
params_lookups = [
|
106
|
+
# e.g.: create_post_params
|
107
|
+
"#{action_name}_#{model_key}_params",
|
108
|
+
# generic for action
|
109
|
+
"#{action_name}_params",
|
110
|
+
# e.g.: post_params
|
111
|
+
"#{model_key}_params",
|
112
|
+
# most generic
|
113
|
+
'resource_params'
|
114
|
+
]
|
115
|
+
|
116
|
+
lookup_params_for_action(params_lookups)
|
117
|
+
end
|
105
118
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
send(model_params_method)
|
110
|
-
else
|
111
|
-
params
|
119
|
+
def lookup_params_for_action(lookups)
|
120
|
+
lookups.each do |method_name|
|
121
|
+
return send(method_name) if respond_to?(method_name, true)
|
112
122
|
end
|
123
|
+
|
124
|
+
params
|
113
125
|
end
|
114
126
|
|
115
127
|
# action name is inherited from ActionController::Base
|
@@ -64,10 +64,6 @@ module SkinnyControllers
|
|
64
64
|
# TODO: think of a way to override the authorized_via_parent functionality
|
65
65
|
def read_all?
|
66
66
|
return true if authorized_via_parent
|
67
|
-
|
68
|
-
# Might be deceptive...
|
69
|
-
return true if object.nil? || object.empty?
|
70
|
-
|
71
67
|
# This is expensive, so try to avoid it
|
72
68
|
# TODO: look in to creating a cache for
|
73
69
|
# these look ups that's invalidated upon
|
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.10.
|
4
|
+
version: 0.10.7
|
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: 2017-07-
|
11
|
+
date: 2017-07-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -235,8 +235,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
235
235
|
version: '0'
|
236
236
|
requirements: []
|
237
237
|
rubyforge_project:
|
238
|
-
rubygems_version: 2.6.
|
238
|
+
rubygems_version: 2.6.8
|
239
239
|
signing_key:
|
240
240
|
specification_version: 4
|
241
|
-
summary: SkinnyControllers-0.10.
|
241
|
+
summary: SkinnyControllers-0.10.7
|
242
242
|
test_files: []
|