sweet_actions 0.1.5 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/README.md +216 -175
  4. data/lib/generators/sweet_actions/templates/base_action.rb +4 -0
  5. data/lib/generators/sweet_actions/templates/collect_action.rb +7 -8
  6. data/lib/generators/sweet_actions/templates/create_action.rb +10 -11
  7. data/lib/generators/sweet_actions/templates/destroy_action.rb +10 -11
  8. data/lib/generators/sweet_actions/templates/show_action.rb +7 -8
  9. data/lib/generators/sweet_actions/templates/update_action.rb +11 -12
  10. data/lib/sweet_actions/.DS_Store +0 -0
  11. data/lib/sweet_actions/action.rb +32 -0
  12. data/lib/sweet_actions/action_factory.rb +4 -2
  13. data/lib/sweet_actions/{authorization_concerns.rb → authorization.rb} +2 -2
  14. data/lib/sweet_actions/controller_concerns.rb +0 -1
  15. data/lib/sweet_actions/exceptions.rb +13 -0
  16. data/lib/sweet_actions/json/.DS_Store +0 -0
  17. data/lib/sweet_actions/json/base_action.rb +41 -0
  18. data/lib/sweet_actions/json/collect_action.rb +13 -0
  19. data/lib/sweet_actions/json/create_action.rb +18 -0
  20. data/lib/sweet_actions/json/destroy_action.rb +13 -0
  21. data/lib/sweet_actions/json/show_action.rb +13 -0
  22. data/lib/sweet_actions/json/update_action.rb +18 -0
  23. data/lib/sweet_actions/{rest_concerns.rb → resource.rb} +3 -5
  24. data/lib/sweet_actions/rest/.DS_Store +0 -0
  25. data/lib/sweet_actions/rest/base.rb +8 -0
  26. data/lib/sweet_actions/rest/collect.rb +14 -0
  27. data/lib/sweet_actions/rest/create.rb +14 -0
  28. data/lib/sweet_actions/rest/destroy.rb +25 -0
  29. data/lib/sweet_actions/rest/find.rb +13 -0
  30. data/lib/sweet_actions/rest/multiple.rb +9 -0
  31. data/lib/sweet_actions/rest/read.rb +19 -0
  32. data/lib/sweet_actions/rest/save.rb +54 -0
  33. data/lib/sweet_actions/rest/show.rb +8 -0
  34. data/lib/sweet_actions/rest/singular.rb +9 -0
  35. data/lib/sweet_actions/rest/update.rb +15 -0
  36. data/lib/sweet_actions/{rest_serializer_concerns.rb → serialize.rb} +12 -3
  37. data/lib/sweet_actions/version.rb +1 -1
  38. data/lib/sweet_actions.rb +27 -13
  39. data/sweet_actions.gemspec +3 -0
  40. metadata +55 -14
  41. data/lib/generators/rails/resource_override.rb +0 -10
  42. data/lib/sweet_actions/api_action.rb +0 -63
  43. data/lib/sweet_actions/collect_action.rb +0 -11
  44. data/lib/sweet_actions/create_action.rb +0 -10
  45. data/lib/sweet_actions/destroy_action.rb +0 -19
  46. data/lib/sweet_actions/read_concerns.rb +0 -14
  47. data/lib/sweet_actions/save_concerns.rb +0 -51
  48. data/lib/sweet_actions/show_action.rb +0 -9
  49. data/lib/sweet_actions/update_action.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1d876fa0a38c4362063893f5f103368cb66a0ef1
4
- data.tar.gz: 76d1bcd450c832d38e708c34892ed17615efb569
3
+ metadata.gz: a2460958332f8a5291485d228c95da9b3c6f38a9
4
+ data.tar.gz: 977ce5b64b0d74e726ebc212ab259bc3db1d26e5
5
5
  SHA512:
6
- metadata.gz: 549ae05b48cc2d281525898bfeaaa557a57399b41be1b42f6b944d29e948a2fdff43a1c1f32360740abbc112cc0ba05e0522433e52140cdba9c543b1b48fab59
7
- data.tar.gz: 70ef12a7a56ea0b1a9651cfa9e67f54984cfe35a12f86c6d9a7de1768f51b31ee7900c69f512f4ad8d1a1b3ce6a85643cf95fcc500be79becc18c644ed27f47a
6
+ metadata.gz: 2994cd3afa31f96da8f23a90af9399ff029da96c9c813fab812c34f2c07492a78b891e7b75ff02d4b36895ba6ae6c157ee2223e99817470a8c4e2d002a7006de
7
+ data.tar.gz: 568fc472e0f7ba8476ad86e359650f730a128f279fb01aca0e562fb5926e65cafe468f7f812a81894938b041bf63cf489f84cafb4f0bc072fc35b7155b007936
data/.DS_Store ADDED
Binary file
data/README.md CHANGED
@@ -1,6 +1,174 @@
1
1
  # Sweet Actions
2
2
 
3
- ## The Idea
3
+ ## Introduction
4
+ Controller actions (`events#create`) tend to have more in common with their cousins (`articles#create`) than their siblings (`events#show`). Because of this, we think actions should be classes instead of methods. This makes it possible for actions to take advantage of common Object Oriented principles like Inheritance and Composition.
5
+
6
+ The end result of this approach is that resource-specific controllers and actions often don't even need to exist - their logic is abstracted to parent actions.
7
+
8
+ Work smart not hard right?
9
+
10
+ Let's take a look at how that's possible.
11
+
12
+ ## Installation
13
+
14
+ ### 1. Install Gem
15
+
16
+ Gemfile:
17
+
18
+ ```ruby
19
+ gem 'sweet_actions'
20
+ gem 'active_model_serializers'
21
+ gem 'decanter'
22
+ ```
23
+
24
+ Terminal:
25
+
26
+ ```
27
+ bundle
28
+ bundle exec rails g sweet_actions:install
29
+ ```
30
+
31
+ This command generates a folder at `app/actions` with the following structure:
32
+
33
+ ```
34
+ - base_action.rb
35
+ - collect_action.rb
36
+ - create_action.rb
37
+ - destroy_action.rb
38
+ - show_action.rb
39
+ - update_action.rb
40
+ ```
41
+
42
+ ### 2. Generate Resource
43
+
44
+ ```
45
+ rails g model Event title:string start_date:date
46
+ bundle exec rake db:migrate
47
+ rails g decanter Event title:string start_date:date
48
+ rails g serializer Event title:string start_date:date
49
+ rails g actions Events
50
+ ```
51
+
52
+ This last command (`rails g actions events`) generates a folder at `app/actions/events` with the following structure:
53
+
54
+ ```
55
+ - events/
56
+ - collect.rb
57
+ - create.rb
58
+ - destroy.rb
59
+ - show.rb
60
+ - update.rb
61
+ ```
62
+
63
+ ### 3. Add Routes
64
+
65
+ ```ruby
66
+ Rails.application.routes.draw do
67
+ scope :api do
68
+ scope :v1 do
69
+ create_sweet_actions(:events)
70
+ end
71
+ end
72
+ end
73
+ ```
74
+
75
+ ### 4. Profit
76
+
77
+ ```
78
+ rails s
79
+ ```
80
+
81
+ Using Postman, submit the following request:
82
+
83
+ POST to localhost:3000/api/v1/events
84
+
85
+ ```json
86
+ {
87
+ "event": {
88
+ "title": "My sweet event",
89
+ "start_date": "01/18/2018"
90
+ }
91
+ }
92
+ ```
93
+
94
+ You should get a response like so:
95
+
96
+ ```json
97
+ {
98
+ "type": "event",
99
+ "attributes": {
100
+ "id": 1,
101
+ "title": "My sweet event",
102
+ "start_date": "2018-01-18"
103
+ }
104
+ }
105
+ ```
106
+
107
+ ## Default REST Actions
108
+
109
+ For a given resource, we provide five RESTful actions:
110
+
111
+ ```
112
+ Collect: GET '/events'
113
+ Create: POST '/events'
114
+ Show: GET '/events/:id'
115
+ Update: PUT '/events/:id'
116
+ Destroy: DELETE '/events/:id'
117
+ ```
118
+
119
+ Many of these actions have shared behavior, which we abstract for you:
120
+ - Authorization of resource (cancancan for example)
121
+ - Create and Update need to be able to properly respond with error information when save does not succeed
122
+ - Create and Update rely on decanted params
123
+ - Serialization of resource
124
+
125
+ ## Creating One-Off Actions
126
+
127
+ For actions that are not RESTful (i.e. not one of the five listed above), you can still use `sweet_actions`. For example, let's say you want to create the action `events#export`.
128
+
129
+ 1. Create a new file at app/actions/events/export.rb:
130
+ 2. Implement `action` method that responds with a response hash
131
+ 3. Create the route
132
+
133
+ ```ruby
134
+ # app/actions/events/export.rb:
135
+ module Events
136
+ class Export < SweetActions::JSON::BaseAction
137
+ def action
138
+ {
139
+ success: true
140
+ }
141
+ end
142
+ end
143
+ end
144
+ ```
145
+
146
+ ```ruby
147
+ # config/routes.rb
148
+ Rails.application.routes.draw do
149
+ scope :api
150
+ scope :v1
151
+ get '/events/export' => 'sweet_actions#export', resource_class: 'Event'
152
+ end
153
+ end
154
+ end
155
+ ```
156
+
157
+ Using a tool like Postman, submit the following request:
158
+
159
+ ```
160
+ GET to localhost:3000/api/v1/events/export
161
+ ```
162
+
163
+ You should get a response like so:
164
+
165
+ ```json
166
+ {
167
+ success: true
168
+ }
169
+ ```
170
+
171
+ ## The Idea Explained in Detail
4
172
 
5
173
  In a RESTful context, controller actions tend to have more in common with the same actions belonging to other resources than other actions belonging to the same resource. For example, let's say we have two resources in our app: Events and Articles.
6
174
 
@@ -16,7 +184,7 @@ We would argue that #2 has more in common than #1. Both events#create and articl
16
184
  3. Persist the new record
17
185
  4. Respond with new record (if successful) or error information (if unsuccessful) in the JSON
18
186
 
19
- By organizing our actions as methods inside a resource based controller like below, we don't have the opportunity to take advantage of basic Object Oriented programming concepts like Inheritance and Modules.
187
+ Rails pushes us to organize our actions as methods inside a resource based controller like below. With this approach we cannot take advantage of basic Object Oriented programming concepts like Inheritance and Modules as it relates to specific actions.
20
188
 
21
189
  ```ruby
22
190
  class EventsController < ApplicationController
@@ -61,33 +229,22 @@ class ArticlesController < ApplicationController
61
229
  end
62
230
  ```
63
231
 
64
- Instead, we propose a strategy that looks more like the following:
232
+ Instead, we propose a strategy where the actions themselves are classes. This would allow us have multiple layers of abstraction like so:
65
233
 
66
- ```ruby
67
- # generic logic for create (sweet_actions gem)
68
- module SweetActions
69
- class CreateAction < ApiAction
70
- def action
71
- @resource = set_resource
72
- authorize
73
- validate_and_save ? success : failure
74
- end
234
+ 1. **Generic Logic** (logic that applies to all apps that use SweetActions):
235
+ - `class SweetActions::JSON::CreateAction`
75
236
 
76
- # ...
77
- end
78
- end
237
+ 2. **Application Logic**: logic that applies to all create actions in your app:
238
+ - `class CreateAction < SweetActions::JSON::CreateAction`
79
239
 
80
- # app logic for create (app/actions/create_action.rb)
81
- class CreateAction < SweetActions::CreateAction
82
- def set_resource
83
- resource_class.new(resource_params)
84
- end
240
+ 3. **Resource Logic**: logic that applies to a specific resource (e.g. Events) in your app
241
+ - `class Events::Create < CreateAction`
85
242
 
86
- def authorized?
87
- can?(:create, resource)
88
- end
89
- end
243
+ With this approach, we often won't even need to implement resource specific actions. This is because by default our `Events::Create` action will inherit all the functionality it needs. Only when there are deviances from the norm do we implement resource specific classes and in those cases, we need only override the methods that correspond with the deviance.
244
+
245
+ For example, let's say we want to send an email when an event is created. It's as easy as overriding the `after_save` hook:
90
246
 
247
+ ```ruby
91
248
  # resource logic for create (app/actions/events/create.rb)
92
249
  module Events
93
250
  class Create < CreateAction
@@ -98,182 +255,66 @@ module Events
98
255
  end
99
256
  ```
100
257
 
101
- With this structure, we essentially have three levels of abstraction:
102
-
103
- - Generic create logic: SweetActions::CreateActions
104
- - App create logic: CreateAction
105
- - Resource create logic: Events::Create
106
-
107
-
108
- As you can see, we can abstract most of the `create` logic to be shared across resources, which means you **only need to write the code that is unique about this create action vs. other create actions**.
109
-
110
- ## Default REST Actions
111
-
112
- For a given resource...
113
- - Collect: list items
114
- - Create: create new item
115
- - Show: show item
116
- - Update: update item
117
- - Destroy: delete item
118
-
119
- Many of these actions have shared behavior, which we abstract for you:
120
- - All require serialization of the resource
121
- - Create and Update need to be able to properly respond with error information when save does not succeed
122
- - Create and Update rely on decanted params
123
- - All require authorization (cancancan)
124
-
125
- ## Automatic REST API
126
-
127
- Given an Event model, one can do the following and get a basic RESTful API (assuming we have [decanter](https://github.com/launchpadlab/decanter) and [AMS](https://github.com/rails-api/active_model_serializers):
128
-
129
- - rails g model Event name:string start_date:date
130
- - rails g decanter Event name:string start_date:date
131
- - rails g serializer Event name:string start_date:date
132
- - add the new resource in routes
133
-
134
- With that, you have the following at your disposal:
135
-
136
- Collect: get '/events'
137
- Create: post '/events'
138
- Show: get '/events/:id'
139
- Update: put '/events/:id'
140
- Destroy: delete '/events/:id'
141
-
142
- Each of these will respond with a consistent JSON format, including when saves don't succeed.
143
-
144
- ## Overriding Default Actions
145
-
146
- Should you choose to override the default behavior (defined in app/sweet_actions/defaults/, ), you need only create your own action like so:
147
-
148
- app/sweet_actions/events/collect.rb
149
-
150
- ```
151
- module Events
152
- class Collect < SweetActions::CollectAction
153
- def set_resource
154
- Event.all.limit(10)
155
- end
258
+ If we wanted to override all action behavior, we could just implement the `action` method itself:
156
259
 
157
- def authorized?
158
- can?(:read, resource)
159
- end
160
- end
161
- end
162
- ```
163
-
164
- app/sweet_actions/events/create.rb
165
-
166
- ```
260
+ ```ruby
167
261
  module Events
168
262
  class Create < CreateAction
169
- def set_resource
170
- Event.new(resource_params)
171
- end
172
-
173
- def authorized?
174
- can?(:create, resource)
175
- end
176
-
177
- def save
178
- resource.save
263
+ def action
264
+ event = Event.new(resource_params)
265
+ event.save ? success(event) : failure
179
266
  end
180
267
 
181
- def after_save
182
- SiteMailer.notify_user
268
+ def success(event)
269
+ UserMailer.new_event_confirmation(resource).deliver_later
270
+ { success: true, data: { event: event } }
183
271
  end
184
272
  end
185
273
  end
186
274
  ```
187
275
 
188
- app/sweet_actions/events/show.rb
276
+ Under the hood, this is made possible by a structure that looks like the following:
189
277
 
190
- ```
191
- module Events
192
- class Show < ShowAction
193
- def set_resource
194
- Event.find(params[:id])
195
- end
196
-
197
- def authorized?
198
- can?(:read, resource)
278
+ ```ruby
279
+ # generic logic for create (sweet_actions gem)
280
+ module SweetActions
281
+ module JSON
282
+ class CreateAction < BaseAction
283
+ def action
284
+ @resource = set_resource
285
+ authorize
286
+ validate_and_save ? success : failure
287
+ end
288
+
289
+ # ...
199
290
  end
200
291
  end
201
292
  end
202
293
  ```
203
294
 
204
- app/sweet_actions/events/update.rb
205
-
206
- ```
207
- module Events
208
- class Update < UpdateAction
209
- def set_resource
210
- Event.find(params[:id])
211
- end
212
-
213
- def authorized?
214
- can?(:update, resource)
215
- end
295
+ ```ruby
296
+ # app logic for create (app/actions/create_action.rb)
297
+ class CreateAction < SweetActions::JSON::CreateAction
298
+ def set_resource
299
+ resource_class.new(resource_params)
300
+ end
216
301
 
217
- def save
218
- resource.update(resource_params)
219
- end
302
+ def authorized?
303
+ can?(:create, resource)
220
304
  end
221
305
  end
222
306
  ```
223
307
 
224
- app/sweet_actions/events/destroy.rb
225
-
226
- ```
308
+ ```ruby
309
+ # resource logic for create (app/actions/events/create.rb)
227
310
  module Events
228
- class Destroy < DestroyAction
229
- def set_resource
230
- Event.find(params[:id])
231
- end
232
-
233
- def authorized?
234
- can?(:destroy, resource)
235
- end
236
-
237
- def destroy
238
- resource.destroy
311
+ class Create < CreateAction
312
+ def after_save
313
+ UserMailer.new_event_confirmation(resource).deliver_later
239
314
  end
240
315
  end
241
316
  end
242
317
  ```
243
318
 
244
- ## Installation
245
-
246
- ### 1. Install Gem
247
-
248
- Gemfile:
249
-
250
- ```ruby
251
- gem 'sweet_actions'
252
- gem 'active_model_serializers'
253
- gem 'decanter'
254
- ```
255
-
256
- Terminal:
257
-
258
- ```
259
- bundle
260
- bundle exec rails g sweet_actions:install
261
- ```
262
-
263
- ### 2. Generate Resource
264
-
265
- ```
266
- rails g model Event title:name start_date:date
267
- bundle exec rake db:migrate
268
- rails g decanter Event title:name start_date:date
269
- rails g serializer Event title:name start_date:date
270
- rails g actions Events
271
- ```
272
-
273
- ### 3. Add Routes
319
+ As you can see, we can abstract most of the `create` logic to be shared across resources, which means you **only need to write the code that is unique about this create action vs. other create actions**.
274
320
 
275
- ```ruby
276
- Rails.application.routes.draw do
277
- create_sweet_actions(:events)
278
- end
279
- ```
@@ -0,0 +1,4 @@
1
+ class CreateAction < SweetActions::JSON::BaseAction
2
+ include SweetActions::REST::Base
3
+ include SweetActions::REST::Serialize
4
+ end
@@ -1,10 +1,9 @@
1
- class CollectAction < SweetActions::CollectAction
2
- def set_resource
3
- resource_class.all
4
- end
1
+ class CollectAction < SweetActions::JSON::CollectAction
2
+ # def set_resource
3
+ # resource_class.all
4
+ # end
5
5
 
6
- def authorized?
7
- # can?(:read, resource)
8
- false
9
- end
6
+ # def authorized?
7
+ # false
8
+ # end
10
9
  end
@@ -1,14 +1,13 @@
1
- class CreateAction < SweetActions::CreateAction
2
- def set_resource
3
- resource_class.new(resource_params)
4
- end
1
+ class CreateAction < SweetActions::JSON::CreateAction
2
+ # def set_resource
3
+ # resource_class.new(resource_params)
4
+ # end
5
5
 
6
- def authorized?
7
- # can?(:create, resource)
8
- false
9
- end
6
+ # def authorized?
7
+ # can?(:create, resource)
8
+ # end
10
9
 
11
- def save
12
- resource.save
13
- end
10
+ # def save
11
+ # resource.save
12
+ # end
14
13
  end
@@ -1,14 +1,13 @@
1
- class DestroyAction < SweetActions::DestroyAction
2
- def set_resource
3
- resource_class.find(params[:id])
4
- end
1
+ class DestroyAction < SweetActions::JSON::DestroyAction
2
+ # def set_resource
3
+ # resource_class.find(params[:id])
4
+ # end
5
5
 
6
- def authorized?
7
- # can?(:destroy, resource)
8
- false
9
- end
6
+ # def authorized?
7
+ # can?(:destroy, resource)
8
+ # end
10
9
 
11
- def destroy
12
- resource.destroy
13
- end
10
+ # def destroy
11
+ # resource.destroy
12
+ # end
14
13
  end
@@ -1,10 +1,9 @@
1
- class ShowAction < SweetActions::ShowAction
2
- def set_resource
3
- resource_class.find(params[:id])
4
- end
1
+ class ShowAction < SweetActions::JSON::ShowAction
2
+ # def set_resource
3
+ # resource_class.find(params[:id])
4
+ # end
5
5
 
6
- def authorized?
7
- # can?(:read, resource)
8
- false
9
- end
6
+ # def authorized?
7
+ # can?(:read, resource)
8
+ # end
10
9
  end
@@ -1,15 +1,14 @@
1
- class UpdateAction < SweetActions::UpdateAction
2
- def set_resource
3
- resource_class.find(params[:id])
4
- end
1
+ class UpdateAction < SweetActions::JSON::UpdateAction
2
+ # def set_resource
3
+ # resource_class.find(params[:id])
4
+ # end
5
5
 
6
- def authorized?
7
- # can?(:update, resource)
8
- false
9
- end
6
+ # def authorized?
7
+ # can?(:update, resource)
8
+ # end
10
9
 
11
- def save
12
- resource.attributes = resource_params
13
- resource.save
14
- end
10
+ # def save
11
+ # resource.attributes = resource_params
12
+ # resource.save
13
+ # end
15
14
  end
Binary file
@@ -0,0 +1,32 @@
1
+ module SweetActions
2
+ class Action
3
+ attr_reader :controller
4
+
5
+ def initialize(controller, options = {})
6
+ @controller = controller
7
+ after_init(options)
8
+ end
9
+
10
+ def perform_action
11
+ action
12
+ end
13
+
14
+ private
15
+
16
+ delegate :request, :params, to: :controller
17
+
18
+ def after_init(options); end
19
+
20
+ def action
21
+ raise "action method is required for #{self.class.name} because it inherits from SweetActions::Action"
22
+ end
23
+
24
+ def env
25
+ request.env
26
+ end
27
+
28
+ def path_parameters
29
+ @path_parameters ||= env['action_dispatch.request.path_parameters']
30
+ end
31
+ end
32
+ end
@@ -21,9 +21,11 @@ module SweetActions
21
21
  end
22
22
 
23
23
  def action_class
24
- klass_name = [namespace, resource_module, action_class_name].compact.join('::')
24
+ parts = [namespace, resource_module, action_class_name].compact
25
+ klass_name = parts.join('::')
25
26
  return klass_name.constantize if klass_defined?(klass_name)
26
- default_action
27
+ path = parts.map(&:downcase).join('/')
28
+ raise SweetActions::Exceptions::ActionNotFound, path: path, class_name: klass_name
27
29
  end
28
30
 
29
31
  def resource_module
@@ -1,5 +1,5 @@
1
1
  module SweetActions
2
- module AuthorizationConcerns
2
+ module Authorization
3
3
  private
4
4
 
5
5
  def authorize?
@@ -13,7 +13,7 @@ module SweetActions
13
13
  end
14
14
 
15
15
  def authorized?
16
- raise "authorized? method is required for the #{self.class.name} action"
16
+ false # lock it down by default
17
17
  end
18
18
 
19
19
  def unauthorized
@@ -4,7 +4,6 @@ module SweetActions
4
4
  factory = ActionFactory.new(self, action_name)
5
5
  action = factory.build_action
6
6
  action.perform_action
7
- render status: action.response_code, json: action.response_data
8
7
  end
9
8
  end
10
9
  end
@@ -1,5 +1,18 @@
1
1
  module SweetActions
2
2
  module Exceptions
3
3
  class NotAuthorized < StandardError; end
4
+
5
+ class ActionNotFound < StandardError
6
+ attr_reader :path, :class_name
7
+
8
+ def initialize(args = {})
9
+ @path = args.fetch(:path, '')
10
+ @class_name = args.fetch(:class_name, '')
11
+ end
12
+
13
+ def message
14
+ "Action class not found. Please make sure #{class_name} exists at app/actions/#{path}."
15
+ end
16
+ end
4
17
  end
5
18
  end
Binary file