typical_situation 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 +405 -0
- data/Rakefile +14 -0
- data/lib/typical_situation/actions.rb +57 -0
- data/lib/typical_situation/identity.rb +64 -0
- data/lib/typical_situation/operations.rb +115 -0
- data/lib/typical_situation/permissions.rb +9 -0
- data/lib/typical_situation/responses.rb +156 -0
- data/lib/typical_situation/version.rb +5 -0
- data/lib/typical_situation.rb +67 -0
- metadata +220 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e4c1f0edb5145c0cbeab8bd20520a2be77459ff68280f2dfd99350e51c66c288
|
4
|
+
data.tar.gz: f72c7aba90b5984f87ddd5cba2982c512292e4ac1f44603a07eec230db4ccdee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cee57ac0c5eea896aea026f06a9889cb832ce98a303e95a6a18d9b91fe9fee9d54a5bcb19fbfa8f52279a73416aa818a2fbb88335113c29c04d2b299ebc51b8e
|
7
|
+
data.tar.gz: de2e3f522926679ae805b952c06ca33409d953d0d4ed3a1482081337bc1d6b87ab20bc7bbc89727037deb5fca5cef51732972c158b13c60d593d1a4a774e1835
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 YOURNAME
|
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,405 @@
|
|
1
|
+
# Typical Situation [](https://github.com/apsislabs/typical_situation/actions)
|
2
|
+
|
3
|
+
The missing Ruby on Rails ActionController REST API mixin.
|
4
|
+
|
5
|
+
A Ruby mixin (module) providing the seven standard resource actions & responses for an ActiveRecord :model_type & :collection.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Tested in:
|
10
|
+
|
11
|
+
- Rails 7.0
|
12
|
+
- Rails 7.1
|
13
|
+
- Rails 8.0
|
14
|
+
|
15
|
+
Against Ruby versions:
|
16
|
+
|
17
|
+
- 3.2
|
18
|
+
- 3.3
|
19
|
+
- 3.4
|
20
|
+
|
21
|
+
Add to your **Gemfile**:
|
22
|
+
|
23
|
+
gem 'typical_situation'
|
24
|
+
|
25
|
+
**Legacy Versions**: For Rails 4.x/5.x/6.x support, see older versions of this gem. Ruby 3.0+ is required.
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Define your model and methods
|
30
|
+
|
31
|
+
Basic usage is to declare the `typical_situation`, and then two required helper methods. Everything else is handled automatically.
|
32
|
+
|
33
|
+
```rb
|
34
|
+
class PostsController < ApplicationController
|
35
|
+
include TypicalSituation
|
36
|
+
|
37
|
+
# Symbolized, underscored version of the model to use as the resource.
|
38
|
+
typical_situation :post # => maps to the Post model
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# The collection of model instances.
|
43
|
+
def collection
|
44
|
+
current_user.posts
|
45
|
+
end
|
46
|
+
|
47
|
+
# Find a model instance by ID.
|
48
|
+
def find_in_collection(id)
|
49
|
+
collection.find_by_id(id)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
There are two alternative helper methods:
|
55
|
+
|
56
|
+
#### Typical REST
|
57
|
+
|
58
|
+
The typical REST helper is an alias for `typical_situation`, and defines the 7 standard REST endpoints: `index`, `show`, `new`, `create`, `edit`, `update`, `destroy`.
|
59
|
+
|
60
|
+
```rb
|
61
|
+
class PostsController < ApplicationController
|
62
|
+
include TypicalSituation
|
63
|
+
|
64
|
+
typical_rest :post
|
65
|
+
|
66
|
+
...
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
#### Typical CRUD
|
71
|
+
|
72
|
+
Sometimes you don't need all seven endpoints, and just need standard CRUD. The typical CRUD helper defines the 4 standard CRUD endpoints: `create`, `show`, `update`, `destroy`.
|
73
|
+
|
74
|
+
```rb
|
75
|
+
class PostsController < ApplicationController
|
76
|
+
include TypicalSituation
|
77
|
+
|
78
|
+
typical_crud :post
|
79
|
+
|
80
|
+
...
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
#### Customizing defined endpoints
|
85
|
+
|
86
|
+
You can also define only the endpoints you want by passing an `only` flag to `typical_situation`:
|
87
|
+
|
88
|
+
```rb
|
89
|
+
class PostsController < ApplicationController
|
90
|
+
include TypicalSituation
|
91
|
+
|
92
|
+
typical_situation :post, only: [:index, :show]
|
93
|
+
|
94
|
+
...
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
### Customize by overriding highly composable methods
|
99
|
+
|
100
|
+
`TypicalSituation` is composed of a library of common functionality, which can all be overridden in individual controllers. Express what is _different_ & _special_ about each controller, instead of repeating boilerplate.
|
101
|
+
|
102
|
+
The library is split into modules:
|
103
|
+
|
104
|
+
- [identity](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/identity.rb) - **required definitions** of the model & how to find it
|
105
|
+
- [actions](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/actions.rb) - high-level controller actions
|
106
|
+
- [operations](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/operations.rb) - loading, changing, & persisting the model
|
107
|
+
- [permissions](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/permissions.rb) - handling authorization to records and actions
|
108
|
+
- [responses](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/responses.rb) - HTTP responses & redirects
|
109
|
+
|
110
|
+
#### Common Customization Hooks
|
111
|
+
|
112
|
+
**Scoped Collections** - Filter the collection based on user permissions or other criteria:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
def scoped_resource
|
116
|
+
if current_user.admin?
|
117
|
+
collection
|
118
|
+
else
|
119
|
+
collection.where(published: true)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
**Custom Lookup** - Use different attributes for finding resources:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
def find_resource(param)
|
128
|
+
collection.find_by!(slug: param)
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
**Custom Redirects** - Control where users go after actions:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
def after_resource_created_path(resource)
|
136
|
+
{ action: :index }
|
137
|
+
end
|
138
|
+
|
139
|
+
def after_resource_updated_path(resource)
|
140
|
+
edit_resource_path(resource)
|
141
|
+
end
|
142
|
+
|
143
|
+
def after_resource_destroyed_path(resource)
|
144
|
+
{ action: :index }
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
**Sorting** - Set default sorting for index pages:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
def default_sorting_attribute
|
152
|
+
:created_at
|
153
|
+
end
|
154
|
+
|
155
|
+
def default_sorting_direction
|
156
|
+
:desc
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
**Pagination** - Bring your own pagination solution:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
# Kaminari
|
164
|
+
def paginate_resources(resources)
|
165
|
+
resources.page(params[:page]).per(params[:per_page] || 25)
|
166
|
+
end
|
167
|
+
|
168
|
+
# will_paginate
|
169
|
+
def paginate_resources(resources)
|
170
|
+
resources.paginate(page: params[:page], per_page: params[:per_page] || 25)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Custom pagination
|
174
|
+
def paginate_resources(resources)
|
175
|
+
resources.limit(20).offset((params[:page].to_i - 1) * 20)
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
**Strong Parameters** - Control which parameters are allowed for create and update operations:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class PostsController < ApplicationController
|
183
|
+
include TypicalSituation
|
184
|
+
typical_situation :post
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
# Only allow title and content for new posts
|
189
|
+
def permitted_create_params
|
190
|
+
[:title, :content]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Allow title, content, and published for updates
|
194
|
+
def permitted_update_params
|
195
|
+
[:title, :content, :published]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
By default, `TypicalSituation` permits all parameters (`permit!`) when these methods return `nil` or an empty array. Override them to restrict parameters for security.
|
201
|
+
|
202
|
+
#### Authorization
|
203
|
+
|
204
|
+
Control access to resources by overriding the `authorized?` method:
|
205
|
+
|
206
|
+
```rb
|
207
|
+
class PostsController < ApplicationController
|
208
|
+
include TypicalSituation
|
209
|
+
typical_situation :post
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def authorized?(action, resource = nil)
|
214
|
+
case action
|
215
|
+
when :destroy, :update, :edit
|
216
|
+
resource&.user == current_user || current_user&.admin?
|
217
|
+
when :show
|
218
|
+
resource&.published? || resource&.user == current_user
|
219
|
+
else
|
220
|
+
true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
You can also customize the response when authorization is denied:
|
227
|
+
|
228
|
+
```rb
|
229
|
+
def respond_as_forbidden
|
230
|
+
redirect_to login_path, alert: "Access denied"
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
##### CanCanCan
|
235
|
+
|
236
|
+
```rb
|
237
|
+
def authorized?(action, resource = nil)
|
238
|
+
can?(action, resource || model_class)
|
239
|
+
end
|
240
|
+
```
|
241
|
+
|
242
|
+
##### Pundit
|
243
|
+
|
244
|
+
```rb
|
245
|
+
def authorized?(action, resource = nil)
|
246
|
+
policy(resource || model_class).public_send("#{action}?")
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
250
|
+
#### Serialization
|
251
|
+
|
252
|
+
Under the hood `TypicalSituation` calls `to_json` on your `ActiveRecord` models. This isn't always the optimal way to serialize resources, though, and so `TypicalSituation` offers a simple means of overriding the base Serialization --- either on an individual controller, or for your entire application.
|
253
|
+
|
254
|
+
##### Alba
|
255
|
+
|
256
|
+
```rb
|
257
|
+
class MockApplePieResource
|
258
|
+
include Alba::Resource
|
259
|
+
|
260
|
+
attributes :id, :ingredients
|
261
|
+
|
262
|
+
association :grandma, resource: GrandmaResource
|
263
|
+
end
|
264
|
+
|
265
|
+
class MockApplePiesController < ApplicationController
|
266
|
+
include TypicalSituation
|
267
|
+
typical_situation :mock_apple_pie
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
def serializable_resource(resource)
|
272
|
+
MockApplePieResource.new(resource).serialize
|
273
|
+
end
|
274
|
+
|
275
|
+
def collection
|
276
|
+
current_user.mock_apple_pies
|
277
|
+
end
|
278
|
+
|
279
|
+
def find_in_collection(id)
|
280
|
+
collection.find_by_id(id)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
##### ActiveModelSerializers
|
286
|
+
|
287
|
+
```rb
|
288
|
+
class MockApplePieIndexSerializer < ActiveModel::Serializer
|
289
|
+
attributes :id, :ingredients
|
290
|
+
end
|
291
|
+
|
292
|
+
module TypicalSituation
|
293
|
+
module Operations
|
294
|
+
def serializable_resource(resource)
|
295
|
+
if action_name == "index"
|
296
|
+
ActiveModelSerializers::SerializableResource.new(
|
297
|
+
resource,
|
298
|
+
each_serializer: MockApplePieIndexSerializer
|
299
|
+
)
|
300
|
+
else
|
301
|
+
ActiveModelSerializers::SerializableResource.new(resource)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
```
|
307
|
+
|
308
|
+
###### Fast JSON API
|
309
|
+
|
310
|
+
```rb
|
311
|
+
class MockApplePieSerializer
|
312
|
+
include FastJsonapi::ObjectSerializer
|
313
|
+
attributes :ingredients
|
314
|
+
belongs_to :grandma
|
315
|
+
end
|
316
|
+
|
317
|
+
class MockApplePiesController < ApplicationController
|
318
|
+
include TypicalSituation
|
319
|
+
|
320
|
+
def serializable_resource(resource)
|
321
|
+
MockApplePieSerializer.new(resource).serializable_hash
|
322
|
+
end
|
323
|
+
end
|
324
|
+
```
|
325
|
+
|
326
|
+
## Development
|
327
|
+
|
328
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
329
|
+
|
330
|
+
### Local Setup
|
331
|
+
|
332
|
+
1. Clone the repository
|
333
|
+
2. Install dependencies:
|
334
|
+
```bash
|
335
|
+
bundle install
|
336
|
+
```
|
337
|
+
3. Install appraisal gemfiles for testing across Rails versions:
|
338
|
+
```bash
|
339
|
+
bundle exec appraisal install
|
340
|
+
```
|
341
|
+
|
342
|
+
### Running Tests
|
343
|
+
|
344
|
+
Tests are written using [RSpec](https://rspec.info/) and are setup to use [Appraisal](https://github.com/thoughtbot/appraisal) to run tests over multiple Rails versions.
|
345
|
+
|
346
|
+
Run all tests across all supported Rails versions:
|
347
|
+
```bash
|
348
|
+
bundle exec appraisal rspec
|
349
|
+
```
|
350
|
+
|
351
|
+
Run tests for a specific Rails version:
|
352
|
+
```bash
|
353
|
+
bundle exec appraisal rails_7.0 rspec
|
354
|
+
bundle exec appraisal rails_7.1 rspec
|
355
|
+
bundle exec appraisal rails_8.0 rspec
|
356
|
+
```
|
357
|
+
|
358
|
+
Run specific test files:
|
359
|
+
```bash
|
360
|
+
bundle exec rspec spec/path/to/spec.rb
|
361
|
+
bundle exec appraisal rails_7.0 rspec spec/path/to/spec.rb
|
362
|
+
```
|
363
|
+
|
364
|
+
### Linting and Formatting
|
365
|
+
|
366
|
+
This project uses [Standard Ruby](https://github.com/testdouble/standard) for code formatting and linting.
|
367
|
+
|
368
|
+
Check for style violations:
|
369
|
+
```bash
|
370
|
+
bundle exec standardrb
|
371
|
+
```
|
372
|
+
|
373
|
+
Automatically fix style violations:
|
374
|
+
```bash
|
375
|
+
bundle exec standardrb --fix
|
376
|
+
```
|
377
|
+
|
378
|
+
Run both linting and tests (the default rake task):
|
379
|
+
```bash
|
380
|
+
bundle exec rake
|
381
|
+
```
|
382
|
+
|
383
|
+
### Console
|
384
|
+
|
385
|
+
Start an interactive console to experiment with the gem:
|
386
|
+
```bash
|
387
|
+
bundle exec irb -r typical_situation
|
388
|
+
```
|
389
|
+
|
390
|
+
## Contributing
|
391
|
+
|
392
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/typical_situation.
|
393
|
+
|
394
|
+
## License
|
395
|
+
|
396
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
397
|
+
|
398
|
+
|
399
|
+
---
|
400
|
+
|
401
|
+
# Built by Apsis
|
402
|
+
|
403
|
+
[](https://www.apsis.io)
|
404
|
+
|
405
|
+
`typical_situation` was built by Apsis Labs. We love sharing what we build! Check out our [other libraries on Github](https://github.com/apsislabs), and if you like our work you can [hire us](https://www.apsis.io) to build your vision.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
begin
|
9
|
+
require "standard/rake"
|
10
|
+
rescue LoadError
|
11
|
+
# Standard not available
|
12
|
+
end
|
13
|
+
|
14
|
+
task default: [:standard, :spec]
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypicalSituation
|
4
|
+
# Standard REST/CRUD actions.
|
5
|
+
module Actions
|
6
|
+
def index
|
7
|
+
raise TypicalSituation::ActionForbidden unless authorized?(:index)
|
8
|
+
|
9
|
+
get_resources
|
10
|
+
respond_with_resources
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
get_resource
|
15
|
+
raise TypicalSituation::ActionForbidden unless authorized?(:show, @resource)
|
16
|
+
|
17
|
+
respond_with_resource
|
18
|
+
end
|
19
|
+
|
20
|
+
def edit
|
21
|
+
get_resource
|
22
|
+
raise TypicalSituation::ActionForbidden unless authorized?(:edit, @resource)
|
23
|
+
|
24
|
+
respond_with_resource
|
25
|
+
end
|
26
|
+
|
27
|
+
def new
|
28
|
+
raise TypicalSituation::ActionForbidden unless authorized?(:new)
|
29
|
+
|
30
|
+
new_resource
|
31
|
+
respond_with_resource
|
32
|
+
end
|
33
|
+
|
34
|
+
def update
|
35
|
+
get_resource
|
36
|
+
raise TypicalSituation::ActionForbidden unless authorized?(:update, @resource)
|
37
|
+
|
38
|
+
update_resource(@resource, update_params)
|
39
|
+
respond_as_changed
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroy
|
43
|
+
get_resource
|
44
|
+
raise TypicalSituation::ActionForbidden unless authorized?(:destroy, @resource)
|
45
|
+
|
46
|
+
destroy_resource(@resource)
|
47
|
+
respond_as_gone
|
48
|
+
end
|
49
|
+
|
50
|
+
def create
|
51
|
+
raise TypicalSituation::ActionForbidden unless authorized?(:create)
|
52
|
+
|
53
|
+
@resource = create_resource(create_params)
|
54
|
+
respond_as_created
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypicalSituation
|
4
|
+
# These Identity methods must be defined for each implementation.
|
5
|
+
module Identity
|
6
|
+
# Symbolized, underscored version of the model (class) to use.
|
7
|
+
def model_type
|
8
|
+
raise(NotImplementedError, "#model_type must be defined in the TypicalSituation implementation.")
|
9
|
+
end
|
10
|
+
|
11
|
+
def model_params
|
12
|
+
params.require(model_type.to_sym)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_params
|
16
|
+
if permitted_create_params.nil? || permitted_create_params.empty?
|
17
|
+
return model_params.permit!
|
18
|
+
end
|
19
|
+
|
20
|
+
model_params.permit(permitted_create_params)
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_params
|
24
|
+
if permitted_update_params.nil? || permitted_update_params.empty?
|
25
|
+
return model_params.permit!
|
26
|
+
end
|
27
|
+
|
28
|
+
model_params.permit(permitted_update_params)
|
29
|
+
end
|
30
|
+
|
31
|
+
def permitted_create_params
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def permitted_update_params
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# The collection of model instances.
|
40
|
+
def collection
|
41
|
+
raise(NotImplementedError, "#collection must be defined in the TypicalSituation implementation.")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Find a model instance by ID.
|
45
|
+
def find_in_collection(_id)
|
46
|
+
raise(NotImplementedError, "#find_in_collection must be defined in the TypicalSituation implementation.")
|
47
|
+
end
|
48
|
+
|
49
|
+
def include_root?
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
def plural_model_type
|
54
|
+
model_type.to_s.pluralize.intern
|
55
|
+
end
|
56
|
+
|
57
|
+
def location_url
|
58
|
+
return if @resource.nil? || @resource.new_record?
|
59
|
+
|
60
|
+
@resource.respond_to?(:to_url) ?
|
61
|
+
@resource.to_url : polymorphic_url(@resource)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypicalSituation
|
4
|
+
# Model operations.
|
5
|
+
# Assume that we're working w/ an ActiveRecord association collection.
|
6
|
+
module Operations
|
7
|
+
def scoped_resource
|
8
|
+
collection
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_resource(param)
|
12
|
+
find_in_collection(param)
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_sorting_attribute
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_sorting_direction
|
20
|
+
:asc
|
21
|
+
end
|
22
|
+
|
23
|
+
def paginate_resources(resources)
|
24
|
+
resources
|
25
|
+
end
|
26
|
+
|
27
|
+
def pagination_params
|
28
|
+
params.permit(:page, :per_page)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_resource
|
32
|
+
if (@resource = find_resource(params[:id]))
|
33
|
+
set_single_instance
|
34
|
+
@resource
|
35
|
+
else
|
36
|
+
raise ActiveRecord::RecordNotFound, "Could not find #{model_class}( id:#{params[:id].inspect} )"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_errors?
|
41
|
+
!@resource.errors.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_resources
|
45
|
+
@resources = paginate_resources(apply_sorting(scoped_resource))
|
46
|
+
set_collection_instance
|
47
|
+
@resources
|
48
|
+
end
|
49
|
+
|
50
|
+
def new_resource
|
51
|
+
@resource = collection.build
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_resource(resource, attrs)
|
55
|
+
resource.update(attrs)
|
56
|
+
end
|
57
|
+
|
58
|
+
def assign_resource(resource, attrs)
|
59
|
+
resource.assign_attributes(attrs)
|
60
|
+
end
|
61
|
+
|
62
|
+
def destroy_resource(resource)
|
63
|
+
resource.destroy
|
64
|
+
collection.reload if resource.errors.empty?
|
65
|
+
resource
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_resource(attrs)
|
69
|
+
@resource = collection.create(attrs)
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_resource(attrs)
|
73
|
+
@resource = collection.build(attrs)
|
74
|
+
end
|
75
|
+
|
76
|
+
def model_class
|
77
|
+
@model_class ||= model_type.to_s.camelize.constantize
|
78
|
+
end
|
79
|
+
|
80
|
+
def serialize_resource(resource, options = {})
|
81
|
+
serializable_resource(resource).to_json(options.merge(root: include_root?))
|
82
|
+
end
|
83
|
+
|
84
|
+
def serialize_resources(resources)
|
85
|
+
if include_root?
|
86
|
+
return {plural_model_type => serializable_resource(resources)}
|
87
|
+
end
|
88
|
+
|
89
|
+
serializable_resource(resources).to_json(root: false)
|
90
|
+
end
|
91
|
+
|
92
|
+
def serializable_resource(resource)
|
93
|
+
resource
|
94
|
+
end
|
95
|
+
|
96
|
+
# Set the singular instance variable named after the model. Modules are delimited with "_".
|
97
|
+
# Example: a MockApplePie resource is set to ivar @mock_apple_pie.
|
98
|
+
def set_single_instance
|
99
|
+
instance_variable_set(:"@#{model_type.to_s.gsub("/", "__")}", @resource)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set the plural instance variable named after the model. Modules are delimited with "_".
|
103
|
+
# Example: a MockApplePie resource collection is set to ivar @mock_apple_pies.
|
104
|
+
def set_collection_instance
|
105
|
+
instance_variable_set(:"@#{model_type.to_s.gsub("/", "__").pluralize}", @resources)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def apply_sorting(resources)
|
111
|
+
return resources unless default_sorting_attribute
|
112
|
+
resources.order(default_sorting_attribute => default_sorting_direction)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypicalSituation
|
4
|
+
# Rails MIME responses.
|
5
|
+
module Responses
|
6
|
+
# Return the collection as HTML or JSON
|
7
|
+
#
|
8
|
+
def respond_with_resources
|
9
|
+
respond_to do |format|
|
10
|
+
yield(format) if block_given?
|
11
|
+
|
12
|
+
format.html do
|
13
|
+
set_collection_instance
|
14
|
+
render
|
15
|
+
end
|
16
|
+
format.json do
|
17
|
+
render json: serialize_resources(@resources)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return the resource as HTML or JSON
|
23
|
+
#
|
24
|
+
# A provided block is passed the #respond_to format to further define responses.
|
25
|
+
#
|
26
|
+
def respond_with_resource
|
27
|
+
respond_to do |format|
|
28
|
+
yield(format) if block_given?
|
29
|
+
|
30
|
+
format.html do
|
31
|
+
set_single_instance
|
32
|
+
render
|
33
|
+
end
|
34
|
+
format.json do
|
35
|
+
render json: serialize_resource(@resource)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_as_changed
|
41
|
+
if has_errors?
|
42
|
+
respond_as_error
|
43
|
+
else
|
44
|
+
respond_to do |format|
|
45
|
+
yield(format) if block_given?
|
46
|
+
|
47
|
+
format.html do
|
48
|
+
set_single_instance
|
49
|
+
changed_so_redirect || render
|
50
|
+
end
|
51
|
+
format.json do
|
52
|
+
render json: serialize_resource(@resource)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond_as_created
|
59
|
+
if has_errors?
|
60
|
+
respond_as_error
|
61
|
+
else
|
62
|
+
respond_to do |format|
|
63
|
+
yield(format) if block_given?
|
64
|
+
|
65
|
+
format.html do
|
66
|
+
set_single_instance
|
67
|
+
changed_so_redirect || render
|
68
|
+
end
|
69
|
+
format.json do
|
70
|
+
render json: serialize_resource(@resource),
|
71
|
+
location: location_url,
|
72
|
+
status: :created
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def respond_as_error
|
79
|
+
respond_to do |format|
|
80
|
+
yield(format) if block_given?
|
81
|
+
|
82
|
+
format.html do
|
83
|
+
set_single_instance
|
84
|
+
render action: (@resource.new_record? ? :new : :edit),
|
85
|
+
status: :unprocessable_entity
|
86
|
+
end
|
87
|
+
format.json do
|
88
|
+
render json: serialize_resource(@resource, methods: [:errors]),
|
89
|
+
status: :unprocessable_entity
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def respond_as_gone
|
95
|
+
if has_errors?
|
96
|
+
respond_as_error
|
97
|
+
else
|
98
|
+
respond_to do |format|
|
99
|
+
yield(format) if block_given?
|
100
|
+
|
101
|
+
format.html do
|
102
|
+
set_single_instance
|
103
|
+
gone_so_redirect || render
|
104
|
+
end
|
105
|
+
format.json do
|
106
|
+
head :no_content
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def respond_as_not_found
|
113
|
+
respond_to do |format|
|
114
|
+
yield(format) if block_given?
|
115
|
+
|
116
|
+
format.html do
|
117
|
+
raise ActionController::RoutingError, "Not Found"
|
118
|
+
end
|
119
|
+
format.json do
|
120
|
+
head :not_found
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def respond_as_forbidden
|
126
|
+
respond_to do |format|
|
127
|
+
format.html { render plain: "Forbidden", status: :forbidden }
|
128
|
+
format.json { head :forbidden }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def after_resource_created_path(resource)
|
133
|
+
{action: :show, id: resource.id}
|
134
|
+
end
|
135
|
+
|
136
|
+
def after_resource_updated_path(resource)
|
137
|
+
{action: :show, id: resource.id}
|
138
|
+
end
|
139
|
+
|
140
|
+
def after_resource_destroyed_path(_resource)
|
141
|
+
{action: :index}
|
142
|
+
end
|
143
|
+
|
144
|
+
# HTML response when @resource saved or updated.
|
145
|
+
def changed_so_redirect
|
146
|
+
redirect_to after_resource_updated_path(@resource)
|
147
|
+
true
|
148
|
+
end
|
149
|
+
|
150
|
+
# HTML response when @resource deleted.
|
151
|
+
def gone_so_redirect
|
152
|
+
redirect_to after_resource_destroyed_path(@resource)
|
153
|
+
true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "typical_situation/identity"
|
4
|
+
require "typical_situation/permissions"
|
5
|
+
require "typical_situation/actions"
|
6
|
+
require "typical_situation/operations"
|
7
|
+
require "typical_situation/responses"
|
8
|
+
|
9
|
+
module TypicalSituation
|
10
|
+
class Error < StandardError; end
|
11
|
+
class ActionForbidden < Error; end
|
12
|
+
|
13
|
+
include Identity
|
14
|
+
include Permissions
|
15
|
+
include Operations
|
16
|
+
include Responses
|
17
|
+
|
18
|
+
def self.included(base)
|
19
|
+
add_rescues(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
# Syntactic sugar for defining model_type
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
# class PostsController < ApplicationController
|
28
|
+
# include TypicalSituation
|
29
|
+
# typical_situation :post
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# This is equivalent to:
|
33
|
+
# def model_type
|
34
|
+
# :post
|
35
|
+
# end
|
36
|
+
def typical_situation(model_type_symbol, only: nil)
|
37
|
+
define_method :model_type do
|
38
|
+
model_type_symbol
|
39
|
+
end
|
40
|
+
|
41
|
+
if only
|
42
|
+
only.each do |action|
|
43
|
+
if TypicalSituation::Actions.method_defined?(action)
|
44
|
+
define_method(action, TypicalSituation::Actions.instance_method(action))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
else
|
48
|
+
include TypicalSituation::Actions
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def typical_rest(model_type_symbol)
|
53
|
+
typical_situation(model_type_symbol, only: nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
def typical_crud(model_type_symbol)
|
57
|
+
typical_situation(model_type_symbol, only: %i[create show update destroy])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.add_rescues(action_controller)
|
62
|
+
action_controller.class_eval do
|
63
|
+
rescue_from ActiveRecord::RecordNotFound, with: :respond_as_not_found
|
64
|
+
rescue_from TypicalSituation::ActionForbidden, with: :respond_as_forbidden
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typical_situation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mars Hall
|
8
|
+
- Wyatt Kirby
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 1980-01-02 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: 7.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: appraisal
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.2.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.2.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: combustion
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: coveralls
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: factory_bot_rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rails-controller-testing
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rspec-rails
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '6.0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '6.0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: sqlite3
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '1.4'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '1.4'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: standard
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
description: A module providing the seven standard resource actions & responses for
|
182
|
+
an ActiveRecord :model_type & :collection.
|
183
|
+
email:
|
184
|
+
- m@marsorange.com
|
185
|
+
- wyatt@apsis.io
|
186
|
+
executables: []
|
187
|
+
extensions: []
|
188
|
+
extra_rdoc_files: []
|
189
|
+
files:
|
190
|
+
- MIT-LICENSE
|
191
|
+
- README.md
|
192
|
+
- Rakefile
|
193
|
+
- lib/typical_situation.rb
|
194
|
+
- lib/typical_situation/actions.rb
|
195
|
+
- lib/typical_situation/identity.rb
|
196
|
+
- lib/typical_situation/operations.rb
|
197
|
+
- lib/typical_situation/permissions.rb
|
198
|
+
- lib/typical_situation/responses.rb
|
199
|
+
- lib/typical_situation/version.rb
|
200
|
+
homepage: https://github.com/apsislabs/typical_situation
|
201
|
+
licenses: []
|
202
|
+
metadata: {}
|
203
|
+
rdoc_options: []
|
204
|
+
require_paths:
|
205
|
+
- lib
|
206
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
207
|
+
requirements:
|
208
|
+
- - ">="
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: 3.0.0
|
211
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
requirements: []
|
217
|
+
rubygems_version: 3.6.9
|
218
|
+
specification_version: 4
|
219
|
+
summary: The missing Rails ActionController REST API mixin.
|
220
|
+
test_files: []
|