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 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 [![Spec CI](https://github.com/apsislabs/typical_situation/workflows/Spec%20CI/badge.svg)](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
+ [![apsis](https://s3-us-west-2.amazonaws.com/apsiscdn/apsis.png)](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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSituation
4
+ module Permissions
5
+ def authorized?(_action, _resource = nil)
6
+ true
7
+ end
8
+ end
9
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSituation
4
+ VERSION = "1.0.0"
5
+ 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: []