garden_variety 1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +382 -0
- data/Rakefile +29 -0
- data/lib/garden_variety/actions.rb +89 -0
- data/lib/garden_variety/controller.rb +226 -0
- data/lib/garden_variety/current_user_stub.rb +8 -0
- data/lib/garden_variety/railtie.rb +29 -0
- data/lib/garden_variety/version.rb +3 -0
- data/lib/garden_variety.rb +2 -0
- data/lib/generators/garden/scaffold/scaffold_generator.rb +39 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9e0504f9a8b485b41628697e9b5b3061e697874d
|
4
|
+
data.tar.gz: 69d7067b8054ce75955a12cf47420901f4badf0b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3cb5e7ee8a146481586d7aa7efabdd48efa03e34617c3f9cd6e46bab3a8730210cc12d3ad9c1ec5d9d17daa675b17b8f08ae6481ac8545a8cc27f7a830b9fe40
|
7
|
+
data.tar.gz: 1e02647816ee226bf0d46dc7933e74aff5f28a465abbf75d4722d2e69c287581b7b2da56cfbc7fd7fe629705cd19d8beb6eb800b8a7d7e0ff786b276287d822e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,382 @@
|
|
1
|
+
# garden_variety
|
2
|
+
|
3
|
+
Delightfully boring Rails controllers. One of the superb advantages of
|
4
|
+
Ruby on Rails is convention over configuration. Opinionated default
|
5
|
+
behavior can decrease development time, and increase application
|
6
|
+
robustness (less custom code == less that can go wrong). In service of
|
7
|
+
this principle, *garden_variety* provides reasonable default controller
|
8
|
+
actions, with care to allow easy override.
|
9
|
+
|
10
|
+
*garden_variety* also uses the excellent
|
11
|
+
[Pundit](https://rubygems.org/gems/pundit) gem to isolate authorization
|
12
|
+
concerns. If you're unfamiliar with Pundit, see its documentation for
|
13
|
+
an explanation of policy objects and how they help controller actions
|
14
|
+
stay DRY and boring.
|
15
|
+
|
16
|
+
As an example, this controller using `garden_variety`...
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
class PostsController < ApplicationController
|
20
|
+
garden_variety
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
...expands to the following conventional implementation:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class PostsController < ApplicationController
|
28
|
+
|
29
|
+
def index
|
30
|
+
authorize(resource_class)
|
31
|
+
self.resources = policy_scope(list_resources)
|
32
|
+
end
|
33
|
+
|
34
|
+
def show
|
35
|
+
self.resource = find_resource
|
36
|
+
authorize(resource)
|
37
|
+
end
|
38
|
+
|
39
|
+
def new
|
40
|
+
self.resource = new_resource
|
41
|
+
authorize(resource)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create
|
45
|
+
self.resource = vest(new_resource)
|
46
|
+
if resource.save
|
47
|
+
redirect_to resource
|
48
|
+
else
|
49
|
+
render :new
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def edit
|
54
|
+
self.resource = find_resource
|
55
|
+
authorize(resource)
|
56
|
+
end
|
57
|
+
|
58
|
+
def update
|
59
|
+
self.resource = vest(find_resource)
|
60
|
+
if resource.save
|
61
|
+
redirect_to resource
|
62
|
+
else
|
63
|
+
render :edit
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def destroy
|
68
|
+
self.resource = find_resource
|
69
|
+
authorize(resource)
|
70
|
+
resource.destroy
|
71
|
+
redirect_to action: :index
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def resource_class
|
77
|
+
Post
|
78
|
+
end
|
79
|
+
|
80
|
+
def resources
|
81
|
+
@posts
|
82
|
+
end
|
83
|
+
|
84
|
+
def resources=(models)
|
85
|
+
@posts = models
|
86
|
+
end
|
87
|
+
|
88
|
+
def resource
|
89
|
+
@post
|
90
|
+
end
|
91
|
+
|
92
|
+
def resource=(model)
|
93
|
+
@post = model
|
94
|
+
end
|
95
|
+
|
96
|
+
def list_resources
|
97
|
+
resource_class.all
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_resource
|
101
|
+
resource_class.find(params[:id])
|
102
|
+
end
|
103
|
+
|
104
|
+
def new_resource
|
105
|
+
resource_class.new
|
106
|
+
end
|
107
|
+
|
108
|
+
def vest(model)
|
109
|
+
authorize(model)
|
110
|
+
model.assign_attributes(permitted_attributes(model))
|
111
|
+
model
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
The implementations of the `resource_class` and `resource` / `resources`
|
118
|
+
accessor methods are generated based on the controller name. They can
|
119
|
+
be altered with an optional argument to the `garden_variety` macro. The
|
120
|
+
rest of the methods can be overridden as normal, a la carte. For a
|
121
|
+
detailed description of method behavior, see the
|
122
|
+
[full documentation](http://www.rubydoc.info/gems/garden_variety/).
|
123
|
+
(Note that the `authorize`, `policy_scope`, and `permitted_attributes`
|
124
|
+
methods are provided by Pundit.)
|
125
|
+
|
126
|
+
|
127
|
+
## Scaffold generator
|
128
|
+
|
129
|
+
*garden_variety* includes a scaffold generator similar to the Rails
|
130
|
+
scaffold generator:
|
131
|
+
|
132
|
+
```
|
133
|
+
$ rails generate garden:scaffold post title:string body:text published:boolean
|
134
|
+
generate resource
|
135
|
+
invoke active_record
|
136
|
+
create db/migrate/19991231235959_create_posts.rb
|
137
|
+
create app/models/post.rb
|
138
|
+
invoke test_unit
|
139
|
+
create test/models/post_test.rb
|
140
|
+
create test/fixtures/posts.yml
|
141
|
+
invoke controller
|
142
|
+
create app/controllers/posts_controller.rb
|
143
|
+
invoke erb
|
144
|
+
create app/views/posts
|
145
|
+
invoke test_unit
|
146
|
+
create test/controllers/posts_controller_test.rb
|
147
|
+
invoke helper
|
148
|
+
create app/helpers/posts_helper.rb
|
149
|
+
invoke test_unit
|
150
|
+
invoke assets
|
151
|
+
invoke coffee
|
152
|
+
create app/assets/javascripts/posts.coffee
|
153
|
+
invoke scss
|
154
|
+
create app/assets/stylesheets/posts.scss
|
155
|
+
invoke resource_route
|
156
|
+
route resources :posts
|
157
|
+
generate erb:scaffold
|
158
|
+
exist app/views/posts
|
159
|
+
create app/views/posts/index.html.erb
|
160
|
+
create app/views/posts/edit.html.erb
|
161
|
+
create app/views/posts/show.html.erb
|
162
|
+
create app/views/posts/new.html.erb
|
163
|
+
create app/views/posts/_form.html.erb
|
164
|
+
insert app/controllers/posts_controller.rb
|
165
|
+
generate pundit:policy
|
166
|
+
create app/policies/post_policy.rb
|
167
|
+
invoke test_unit
|
168
|
+
create test/policies/post_policy_test.rb
|
169
|
+
```
|
170
|
+
|
171
|
+
The generated controller will contain only a call to the
|
172
|
+
`garden_variety` macro. Also, as you can see from the command output,
|
173
|
+
the *garden_variety* scaffold generator differs from the Rails scaffold
|
174
|
+
generator in a few small ways:
|
175
|
+
|
176
|
+
* No scaffold CSS (i.e. no "app/assets/stylesheets/scaffolds.scss").
|
177
|
+
* No jbuilder templates. Only HTML templates are generated.
|
178
|
+
* `rails generate pundit:policy` is invoked for the specified model.
|
179
|
+
|
180
|
+
|
181
|
+
## Beyond garden variety behavior
|
182
|
+
|
183
|
+
*garden_variety* is designed to reduce the amount of custom code
|
184
|
+
written, including in situations where custom code is unavoidable.
|
185
|
+
|
186
|
+
|
187
|
+
### Integrating with search
|
188
|
+
|
189
|
+
It is possible to integrate searching functionality by overriding the
|
190
|
+
`index` action. However, it can be simpler to override the
|
191
|
+
`list_resources` method instead:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class PostsController < ApplicationController
|
195
|
+
garden_variety
|
196
|
+
|
197
|
+
def list_resources
|
198
|
+
params[:author] ? super.where(author: params[:author]) : super
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
|
204
|
+
### Integrating with pagination
|
205
|
+
|
206
|
+
Your favorite pagination gem (*may I suggest
|
207
|
+
[foliate](https://rubygems.org/gems/foliate)?*) can also be integrated
|
208
|
+
by overriding the `list_resources` action:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class PostsController < ApplicationController
|
212
|
+
garden_variety
|
213
|
+
|
214
|
+
def list_resources
|
215
|
+
paginate(super)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
|
221
|
+
### Integrating with authentication
|
222
|
+
|
223
|
+
The details of integrating authentication will depend on your chosen
|
224
|
+
authentication library. [Devise](https://rubygems.org/gems/devise) is a
|
225
|
+
popular gem, but I prefer [Clearance](https://rubygems.org/gems/clearance)
|
226
|
+
for its simplicity and concision. Whatever library you choose, it is
|
227
|
+
likely to include a "before filter" which you must invoke in your
|
228
|
+
controller to enforce authentication. Something similar to the
|
229
|
+
following:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
class PostsController < ApplicationController
|
233
|
+
garden_variety
|
234
|
+
before_action :require_login
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
Your authentication library is also likely to provide a `current_user`
|
239
|
+
method, which will return an appropriate value when the user is
|
240
|
+
authenticated. Pundit automatically uses this method to enforce
|
241
|
+
authorization policies. See your authentication library's documentation
|
242
|
+
to verify that it provides this method, or see Pundit's documentation
|
243
|
+
for details on using a different method to identify the current user.
|
244
|
+
|
245
|
+
*garden_variety* also provides a stub implementation of `current_user`,
|
246
|
+
so that if no authentication library is chosen, `current_user` will be
|
247
|
+
defined to always return nil.
|
248
|
+
|
249
|
+
**Note about Clearance:** Clearance versions previous to 2.0 define a
|
250
|
+
deprecated `authorize` method which conflicts with Pundit. To avoid
|
251
|
+
this conflict, add the following line to your ApplicationController or
|
252
|
+
Clearance initializer:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
::Clearance::Controller.send(:remove_method, :authorize)
|
256
|
+
```
|
257
|
+
|
258
|
+
|
259
|
+
### Integrating with Form Objects
|
260
|
+
|
261
|
+
The Form Object pattern is used to mitigate the complexity of handling
|
262
|
+
forms which need special processing logic, such as context-dependent
|
263
|
+
validation, or forms which involve more than one model. A detailed
|
264
|
+
explanation of the pattern is beyond the scope of this document, but
|
265
|
+
consider the following minimal example:
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
class RegistrationForm
|
269
|
+
include ActiveModel::Model
|
270
|
+
|
271
|
+
attr_accessor :email, :password, :accept_terms_of_service
|
272
|
+
|
273
|
+
validates :accept_terms_of_service, presence: true, acceptance: true
|
274
|
+
|
275
|
+
def save
|
276
|
+
@user = User.new(email: email, password: password)
|
277
|
+
if [valid?, @user.valid?].all?
|
278
|
+
@user.save
|
279
|
+
else
|
280
|
+
@user.errors.each{|attr, message| errors.add(attr, message) }
|
281
|
+
false
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
class RegistrationFormsController < ApplicationController
|
288
|
+
garden_variety :new
|
289
|
+
|
290
|
+
def create
|
291
|
+
self.resource = vest(new_resource)
|
292
|
+
if resource.save
|
293
|
+
redirect_to root_path # redirect to front page instead of show
|
294
|
+
else
|
295
|
+
render :new
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
```
|
300
|
+
|
301
|
+
Only the `new` controller action is generated by the `garden_variety`
|
302
|
+
macro. The `create` action is implemented directly in order to redirect
|
303
|
+
to `root_path` rather than the resource itself, as would be
|
304
|
+
conventional. The *garden_variety* helper methods all work as expected
|
305
|
+
because `RegistrationForm` responds to `assign_attributes` and `save`,
|
306
|
+
and has a default (nullary) constructor.
|
307
|
+
|
308
|
+
|
309
|
+
### Non-REST actions
|
310
|
+
|
311
|
+
You may also define any non-REST controller actions you wish (i.e.
|
312
|
+
actions other than: `index`, `show`, `new`, `create`, `edit`, `update`,
|
313
|
+
and `destroy`). The helper methods *garden_variety* provides may be
|
314
|
+
useful when doing so.
|
315
|
+
|
316
|
+
However, before implementing a non-REST controller action, consider if
|
317
|
+
the behavior might be better implemented as a REST action in a new
|
318
|
+
controller. For example, instead of the following `published` action...
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
class PostsController < ApplicationController
|
322
|
+
garden_variety
|
323
|
+
|
324
|
+
def published
|
325
|
+
@posts = Post.where(published: true)
|
326
|
+
render :index
|
327
|
+
end
|
328
|
+
end
|
329
|
+
```
|
330
|
+
|
331
|
+
...consider a new controller:
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
class PublishedPostsController < ApplicationController
|
335
|
+
garden_variety :index, resources: :posts
|
336
|
+
|
337
|
+
def list_resources
|
338
|
+
super.where(published: true)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
```
|
342
|
+
|
343
|
+
Note the `resources:` argument to the `garden_variety` macro. The
|
344
|
+
resource class for `PublishedPostsController` will be overridden as
|
345
|
+
`Post` instead of derived as `PublishedPost`. Likewise, the `@posts`
|
346
|
+
instance variable will be used instead of `@published_posts`.
|
347
|
+
|
348
|
+
This example may be somewhat contrived, but there is an excellent talk
|
349
|
+
from RailsConf which delves deeper into the principle:
|
350
|
+
[In Relentless Pursuit of REST](https://www.youtube.com/watch?v=HctYHe-YjnE).
|
351
|
+
|
352
|
+
|
353
|
+
## Installation
|
354
|
+
|
355
|
+
Add this line to your application's Gemfile:
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
gem "garden_variety"
|
359
|
+
```
|
360
|
+
|
361
|
+
Then execute:
|
362
|
+
|
363
|
+
```bash
|
364
|
+
$ bundle
|
365
|
+
```
|
366
|
+
|
367
|
+
And finally, if you haven't already used and installed Pundit, run the
|
368
|
+
Pundit installation generator:
|
369
|
+
|
370
|
+
```bash
|
371
|
+
$ rails generate pundit:install
|
372
|
+
```
|
373
|
+
|
374
|
+
|
375
|
+
## Contributing
|
376
|
+
|
377
|
+
Run `rake test` to run the tests.
|
378
|
+
|
379
|
+
|
380
|
+
## License
|
381
|
+
|
382
|
+
[MIT License](https://opensource.org/licenses/MIT)
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'yard'
|
8
|
+
|
9
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
require 'bundler/gem_tasks'
|
19
|
+
|
20
|
+
require 'rake/testtask'
|
21
|
+
|
22
|
+
Rake::TestTask.new(:test) do |t|
|
23
|
+
t.libs << 'test'
|
24
|
+
t.pattern = 'test/**/*_test.rb'
|
25
|
+
t.verbose = false
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
task default: :test
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module GardenVariety
|
2
|
+
|
3
|
+
module IndexAction
|
4
|
+
# Garden variety controller +index+ action.
|
5
|
+
# @return [void]
|
6
|
+
def index
|
7
|
+
authorize(resource_class)
|
8
|
+
self.resources = policy_scope(list_resources)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ShowAction
|
13
|
+
# Garden variety controller +show+ action.
|
14
|
+
# @return [void]
|
15
|
+
def show
|
16
|
+
self.resource = find_resource
|
17
|
+
authorize(resource)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module NewAction
|
22
|
+
# Garden variety controller +new+ action.
|
23
|
+
# @return [void]
|
24
|
+
def new
|
25
|
+
self.resource = new_resource
|
26
|
+
authorize(resource)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module CreateAction
|
31
|
+
# Garden variety controller +create+ action.
|
32
|
+
# @return [void]
|
33
|
+
def create
|
34
|
+
self.resource = vest(new_resource)
|
35
|
+
if resource.save
|
36
|
+
redirect_to resource
|
37
|
+
else
|
38
|
+
render :new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module EditAction
|
44
|
+
# Garden variety controller +edit+ action.
|
45
|
+
# @return [void]
|
46
|
+
def edit
|
47
|
+
self.resource = find_resource
|
48
|
+
authorize(resource)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module UpdateAction
|
53
|
+
# Garden variety controller +update+ action.
|
54
|
+
# @return [void]
|
55
|
+
def update
|
56
|
+
self.resource = vest(find_resource)
|
57
|
+
if resource.save
|
58
|
+
redirect_to resource
|
59
|
+
else
|
60
|
+
render :edit
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module DestroyAction
|
66
|
+
# Garden variety controller +destroy+ action.
|
67
|
+
# @return [void]
|
68
|
+
def destroy
|
69
|
+
self.resource = find_resource
|
70
|
+
authorize(resource)
|
71
|
+
resource.destroy
|
72
|
+
redirect_to action: :index
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Map of controller action name to action module. Used by the
|
77
|
+
# {GardenVariety::Controller::ClassMethods#garden_variety} macro to
|
78
|
+
# include desired controller actions.
|
79
|
+
ACTION_MODULES = {
|
80
|
+
index: IndexAction,
|
81
|
+
show: ShowAction,
|
82
|
+
new: NewAction,
|
83
|
+
create: CreateAction,
|
84
|
+
edit: EditAction,
|
85
|
+
update: UpdateAction,
|
86
|
+
destroy: DestroyAction,
|
87
|
+
}
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require "pundit"
|
2
|
+
require "garden_variety/actions"
|
3
|
+
|
4
|
+
module GardenVariety
|
5
|
+
module Controller
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
include Pundit
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Macro to include garden variety implementations of specified
|
12
|
+
# actions in the controller. If no actions are specified, all
|
13
|
+
# typical REST actions (index, show, new, create, edit, update,
|
14
|
+
# destroy) are included.
|
15
|
+
#
|
16
|
+
# The optional +resources:+ parameter dictates which model class
|
17
|
+
# and instance variables these actions use. The parameter's
|
18
|
+
# default value derives from the controller name. The value must
|
19
|
+
# be a resource name in plural form.
|
20
|
+
#
|
21
|
+
# The macro also defines the following accessor methods for use in
|
22
|
+
# generic action and helper methods: +resources+, +resources=+,
|
23
|
+
# +resource+, and +resource=+. These accessors get and set the
|
24
|
+
# instance variables dictated by the +resources:+ parameter.
|
25
|
+
#
|
26
|
+
# @example default usage
|
27
|
+
# # This...
|
28
|
+
# class PostsController < ApplicationController
|
29
|
+
# garden_variety
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # ...is equivalent to:
|
33
|
+
# class PostsController < ApplicationController
|
34
|
+
# include GardenVariety::IndexAction
|
35
|
+
# include GardenVariety::ShowAction
|
36
|
+
# include GardenVariety::NewAction
|
37
|
+
# include GardenVariety::CreateAction
|
38
|
+
# include GardenVariety::EditAction
|
39
|
+
# include GardenVariety::UpdateAction
|
40
|
+
# include GardenVariety::DestroyAction
|
41
|
+
#
|
42
|
+
# private
|
43
|
+
#
|
44
|
+
# def resource_class
|
45
|
+
# Post
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def resources
|
49
|
+
# @posts
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# def resources=(models)
|
53
|
+
# @posts = models
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# def resource
|
57
|
+
# @post
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# def resource=(model)
|
61
|
+
# @post = model
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# @example custom usage
|
66
|
+
# # This...
|
67
|
+
# class CountriesController < ApplicationController
|
68
|
+
# garden_variety :index, resources: :locations
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# # ...is equivalent to:
|
72
|
+
# class CountriesController < ApplicationController
|
73
|
+
# include GardenVariety::IndexAction
|
74
|
+
#
|
75
|
+
# private
|
76
|
+
#
|
77
|
+
# def resource_class
|
78
|
+
# Location
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# def resources
|
82
|
+
# @locations
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# def resources=(models)
|
86
|
+
# @locations = models
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# def resource
|
90
|
+
# @location
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# def resource=(model)
|
94
|
+
# @location = model
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# @param actions [Array<:index, :show, :new, :create, :edit, :update, :destroy>]
|
99
|
+
# @param resources [Symbol, String]
|
100
|
+
# @return [void]
|
101
|
+
def garden_variety(*actions, resources: controller_name)
|
102
|
+
class_eval <<-CODE
|
103
|
+
private
|
104
|
+
|
105
|
+
def resource_class # optimized override
|
106
|
+
#{resources.to_s.classify}
|
107
|
+
end
|
108
|
+
|
109
|
+
def resources
|
110
|
+
@#{resources.to_s.underscore}
|
111
|
+
end
|
112
|
+
|
113
|
+
def resources=(models)
|
114
|
+
@#{resources.to_s.underscore} = models
|
115
|
+
end
|
116
|
+
|
117
|
+
def resource
|
118
|
+
@#{resources.to_s.underscore.singularize}
|
119
|
+
end
|
120
|
+
|
121
|
+
def resource=(model)
|
122
|
+
@#{resources.to_s.underscore.singularize} = model
|
123
|
+
end
|
124
|
+
CODE
|
125
|
+
|
126
|
+
action_modules = actions.empty? ?
|
127
|
+
::GardenVariety::ACTION_MODULES.values :
|
128
|
+
::GardenVariety::ACTION_MODULES.values_at(*actions)
|
129
|
+
|
130
|
+
action_modules.each{|m| include m }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
# @!visibility public
|
138
|
+
# Returns the class of the resource corresponding to the controller
|
139
|
+
# name.
|
140
|
+
#
|
141
|
+
# @example
|
142
|
+
# PostsController.new.resource_class # == Post (class)
|
143
|
+
#
|
144
|
+
# @return [Class]
|
145
|
+
def resource_class
|
146
|
+
@resource_class ||= controller_name.classify.constantize
|
147
|
+
end
|
148
|
+
|
149
|
+
# @!visibility public
|
150
|
+
# Returns an ActiveRecord::Relation representing resource instances
|
151
|
+
# corresponding to the controller. Designed for use in generic
|
152
|
+
# +index+ action methods.
|
153
|
+
#
|
154
|
+
# @example
|
155
|
+
# class PostsController < ApplicationController
|
156
|
+
# def index
|
157
|
+
# @posts = list_resources.where(status: "published")
|
158
|
+
# end
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# @return [ActiveRecord::Relation]
|
162
|
+
def list_resources
|
163
|
+
resource_class.all
|
164
|
+
end
|
165
|
+
|
166
|
+
# @!visibility public
|
167
|
+
# Returns a model instance corresponding to the controller and the
|
168
|
+
# id parameter of the current request (i.e. +params[:id]+).
|
169
|
+
# Designed for use in generic +show+, +edit+, +update+, and
|
170
|
+
# +destroy+ action methods.
|
171
|
+
#
|
172
|
+
# @example
|
173
|
+
# class PostsController < ApplicationController
|
174
|
+
# def show
|
175
|
+
# @post = find_resource
|
176
|
+
# end
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# @return [ActiveRecord::Base]
|
180
|
+
def find_resource
|
181
|
+
resource_class.find(params[:id])
|
182
|
+
end
|
183
|
+
|
184
|
+
# @!visibility public
|
185
|
+
# Returns a new model instance corresponding to the controller.
|
186
|
+
# Designed for use in generic +new+ and +create+ action methods.
|
187
|
+
#
|
188
|
+
# @example
|
189
|
+
# class PostsController < ApplicationController
|
190
|
+
# def new
|
191
|
+
# @post = new_resource
|
192
|
+
# end
|
193
|
+
# end
|
194
|
+
#
|
195
|
+
# @return [ActiveRecord::Base]
|
196
|
+
def new_resource
|
197
|
+
resource_class.new
|
198
|
+
end
|
199
|
+
|
200
|
+
# @!visibility public
|
201
|
+
# Authorizes the given model for the current action via the model
|
202
|
+
# Pundit policy, and populates the model attributes with the current
|
203
|
+
# request params permitted by the model policy. Returns the given
|
204
|
+
# model modified but not persisted.
|
205
|
+
#
|
206
|
+
# @example
|
207
|
+
# class PostsController < ApplicationController
|
208
|
+
# def create
|
209
|
+
# @post = vest(Post.new)
|
210
|
+
# if @post.save
|
211
|
+
# redirect_to @post
|
212
|
+
# else
|
213
|
+
# render :new
|
214
|
+
# end
|
215
|
+
# end
|
216
|
+
# end
|
217
|
+
#
|
218
|
+
# @param model [ActiveRecord::Base]
|
219
|
+
# @return [ActiveRecord::Base]
|
220
|
+
def vest(model)
|
221
|
+
authorize(model)
|
222
|
+
model.assign_attributes(permitted_attributes(model))
|
223
|
+
model
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "rails/railtie"
|
2
|
+
require "garden_variety/controller"
|
3
|
+
require "garden_variety/current_user_stub"
|
4
|
+
|
5
|
+
module GardenVariety
|
6
|
+
# @!visibility private
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
# Render 404 on Pundit::NotAuthorizedError in production. (Helpful
|
9
|
+
# error pages will still be shown in development.) Code 404 is used
|
10
|
+
# because it is more discreet than 403, because it is explicitly
|
11
|
+
# allowed by RFC7231 (https://tools.ietf.org/html/rfc7231#section-6.5.3),
|
12
|
+
# and because Rails includes a default 404 page, but not a 403 page.
|
13
|
+
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] ||= :not_found
|
14
|
+
|
15
|
+
initializer "garden_variety.stub_current_user" do |app|
|
16
|
+
ActiveSupport.on_load :action_controller do
|
17
|
+
unless ActionController::Base.instance_methods.include?(:current_user)
|
18
|
+
ActionController::Base.send :include, GardenVariety::CurrentUserStub
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
initializer "garden_variety.extend_action_controller" do |app|
|
24
|
+
ActiveSupport.on_load :action_controller do
|
25
|
+
ActionController::Base.send :include, GardenVariety::Controller
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "rails/generators/base"
|
2
|
+
|
3
|
+
# @!visibility private
|
4
|
+
module Garden
|
5
|
+
module Generators
|
6
|
+
class ScaffoldGenerator < Rails::Generators::Base
|
7
|
+
argument :resource, type: :string
|
8
|
+
|
9
|
+
# NOTE: an appropriate default value for template_engine (e.g.
|
10
|
+
# :erb, or with the proper gem installed, :slim or :haml) is
|
11
|
+
# inherited from Rails::Generators::Base
|
12
|
+
class_option :template_engine
|
13
|
+
|
14
|
+
# override +initialize+ because it is the only way to reliably
|
15
|
+
# capture the raw input arguments in order to pass them on to
|
16
|
+
# `rails generate resource` (Thor neglects to provide an accessor,
|
17
|
+
# and ARGV is not populated during unit tests)
|
18
|
+
def initialize(raw_args, raw_opts, config)
|
19
|
+
@argv = raw_args + raw_opts
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_scaffolding
|
24
|
+
generate("resource", *@argv)
|
25
|
+
generate("#{options[:template_engine]}:scaffold", *@argv)
|
26
|
+
end
|
27
|
+
|
28
|
+
def inject_garden_variety_into_controller
|
29
|
+
inject_into_class("app/controllers/#{resource.tableize}_controller.rb",
|
30
|
+
"#{resource.tableize.camelize}Controller",
|
31
|
+
" garden_variety\n")
|
32
|
+
end
|
33
|
+
|
34
|
+
def generate_pundit_policy
|
35
|
+
generate("pundit:policy", resource)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: garden_variety
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan Hefner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.1.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.1.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pundit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.9'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.9'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- jonathan.hefner@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- MIT-LICENSE
|
77
|
+
- README.md
|
78
|
+
- Rakefile
|
79
|
+
- lib/garden_variety.rb
|
80
|
+
- lib/garden_variety/actions.rb
|
81
|
+
- lib/garden_variety/controller.rb
|
82
|
+
- lib/garden_variety/current_user_stub.rb
|
83
|
+
- lib/garden_variety/railtie.rb
|
84
|
+
- lib/garden_variety/version.rb
|
85
|
+
- lib/generators/garden/scaffold/scaffold_generator.rb
|
86
|
+
homepage: https://github.com/jonathanhefner/garden_variety
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.6.13
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Delightfully boring Rails controllers
|
110
|
+
test_files: []
|