sweet_actions 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/PITCHME.md +224 -0
- data/PITCHME.yaml +1 -0
- data/README.md +257 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/.DS_Store +0 -0
- data/lib/generators/.DS_Store +0 -0
- data/lib/generators/rails/.DS_Store +0 -0
- data/lib/generators/rails/actions_generator.rb +21 -0
- data/lib/generators/rails/resource_override.rb +10 -0
- data/lib/generators/rails/templates/.DS_Store +0 -0
- data/lib/generators/rails/templates/collect.rb.erb +11 -0
- data/lib/generators/rails/templates/create.rb.erb +15 -0
- data/lib/generators/rails/templates/destroy.rb.erb +15 -0
- data/lib/generators/rails/templates/show.rb.erb +11 -0
- data/lib/generators/rails/templates/update.rb.erb +11 -0
- data/lib/generators/sweet_actions/install_generator.rb +18 -0
- data/lib/generators/sweet_actions/templates/collect_action.rb +10 -0
- data/lib/generators/sweet_actions/templates/create_action.rb +14 -0
- data/lib/generators/sweet_actions/templates/destroy_action.rb +14 -0
- data/lib/generators/sweet_actions/templates/initializer.rb +9 -0
- data/lib/generators/sweet_actions/templates/show_action.rb +10 -0
- data/lib/generators/sweet_actions/templates/update_action.rb +10 -0
- data/lib/sweet_actions.rb +42 -0
- data/lib/sweet_actions/.DS_Store +0 -0
- data/lib/sweet_actions/action_factory.rb +64 -0
- data/lib/sweet_actions/api_action.rb +63 -0
- data/lib/sweet_actions/authorization_concerns.rb +23 -0
- data/lib/sweet_actions/collect_action.rb +11 -0
- data/lib/sweet_actions/configuration.rb +9 -0
- data/lib/sweet_actions/controller_concerns.rb +10 -0
- data/lib/sweet_actions/create_action.rb +6 -0
- data/lib/sweet_actions/destroy_action.rb +19 -0
- data/lib/sweet_actions/exceptions.rb +5 -0
- data/lib/sweet_actions/railtie.rb +8 -0
- data/lib/sweet_actions/read_concerns.rb +14 -0
- data/lib/sweet_actions/rest_concerns.rb +47 -0
- data/lib/sweet_actions/rest_serializer_concerns.rb +54 -0
- data/lib/sweet_actions/routes_helpers.rb +40 -0
- data/lib/sweet_actions/save_concerns.rb +51 -0
- data/lib/sweet_actions/show_action.rb +9 -0
- data/lib/sweet_actions/update_action.rb +6 -0
- data/lib/sweet_actions/version.rb +3 -0
- data/sweet_actions-0.1.0.gem +0 -0
- data/sweet_actions.gemspec +36 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3904d237f789cd3db0a4dc78e4f9adecc8bf3b9a
|
4
|
+
data.tar.gz: 4231ace1119c75b32e0c57dd8f7ac33e1e02051a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: abc3087589ceed04b51f4e60b7fcb2485c4f96049bc7c029d3b9feaca16f1449d7518c328631f9c4a3dc8420d78c84f8701d48085e4361ccbfaa991450ff54af
|
7
|
+
data.tar.gz: 57c3d381618502950e5e1301ede3de0d048890cb86a18280924632f661d9dd504c5ae50e7381918363cb76bb170747f79267e21f0c7f43ab202c796f46d361aa
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Ryan Francis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/PITCHME.md
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
---
|
2
|
+
|
3
|
+
# Sweet Actions
|
4
|
+
#### a Ruby on Rails Gem
|
5
|
+
|
6
|
+
---
|
7
|
+
|
8
|
+
## Let's REST not STRESS
|
9
|
+
|
10
|
+
Implementing a REST JSON API should be:
|
11
|
+
|
12
|
+
- easy
|
13
|
+
- consistent
|
14
|
+
- secure
|
15
|
+
- extendable
|
16
|
+
|
17
|
+
---
|
18
|
+
|
19
|
+
## Job to be Done
|
20
|
+
|
21
|
+
What is the Job that REST is solving?
|
22
|
+
|
23
|
+
- Users need to interact with our database
|
24
|
+
- They can do so by either creating a new record or reading, updating, or destroying an existing record (CRUD)
|
25
|
+
|
26
|
+
---
|
27
|
+
|
28
|
+
For example, given an **Events** resource:
|
29
|
+
|
30
|
+
- See an event
|
31
|
+
- See a list of events
|
32
|
+
- Create an event
|
33
|
+
- Update an event
|
34
|
+
- Delete an event
|
35
|
+
|
36
|
+
---
|
37
|
+
|
38
|
+
## The Big 5
|
39
|
+
|
40
|
+
- show
|
41
|
+
- index
|
42
|
+
- create
|
43
|
+
- update
|
44
|
+
- destroy
|
45
|
+
|
46
|
+
---
|
47
|
+
|
48
|
+
## Good News
|
49
|
+
|
50
|
+
Consistency = easy to program
|
51
|
+
|
52
|
+
...or at least it should be
|
53
|
+
|
54
|
+
---
|
55
|
+
|
56
|
+
## Current Process
|
57
|
+
|
58
|
+
We are focused on the **resource** instead of the **action**
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class EventsController
|
62
|
+
def index; end
|
63
|
+
def show; end
|
64
|
+
def create; end
|
65
|
+
def update; end
|
66
|
+
def destroy; end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
**resource = class, actions = methods**
|
71
|
+
|
72
|
+
---
|
73
|
+
|
74
|
+
## But wait...
|
75
|
+
|
76
|
+
Which do you think has more in common?
|
77
|
+
|
78
|
+
1. events#create <=> events#index
|
79
|
+
2. events#create <=> articles#create
|
80
|
+
|
81
|
+
---
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class EventsController
|
85
|
+
def create
|
86
|
+
event = Event.new(event_params)
|
87
|
+
raise NotAuthorized unless can?(:create, event)
|
88
|
+
|
89
|
+
if event.save
|
90
|
+
# success
|
91
|
+
else
|
92
|
+
# failure
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def index
|
97
|
+
events = Event.where(date: >= Date.today)
|
98
|
+
serialize(events)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
---
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class EventsController
|
107
|
+
def create
|
108
|
+
event = Event.new(event_params)
|
109
|
+
raise NotAuthorized unless can?(:create, event)
|
110
|
+
|
111
|
+
if event.save
|
112
|
+
# success
|
113
|
+
else
|
114
|
+
# failure
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class ArticlesController
|
120
|
+
def create
|
121
|
+
article = Article.new(article_params)
|
122
|
+
raise NotAuthorized unless can?(:create, article)
|
123
|
+
|
124
|
+
if article.save
|
125
|
+
# success
|
126
|
+
else
|
127
|
+
# failure
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
---
|
134
|
+
|
135
|
+
## Answer: #2
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
'events#create' == 'articles#create'
|
139
|
+
'events#create' != 'events#index'
|
140
|
+
```
|
141
|
+
|
142
|
+
---
|
143
|
+
|
144
|
+
### Actions as First Class Citizens
|
145
|
+
|
146
|
+
Since the actions have more in common with each other than they do with the resources to which they belong, it is the actions that should be objects instead of the resource.
|
147
|
+
|
148
|
+
---
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class CreateAction < SweetActions::CreateAction
|
152
|
+
def action
|
153
|
+
resource = set_resource
|
154
|
+
resource.save ? success : failure
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def set_resource
|
160
|
+
resource_class.new(resource_params)
|
161
|
+
end
|
162
|
+
|
163
|
+
def resource_params
|
164
|
+
decanter.new(params[resource_name])
|
165
|
+
end
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
---
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
module Events
|
173
|
+
class Create < CreateAction
|
174
|
+
def after_save
|
175
|
+
UserMailer.send_event_confirmation.deliver_later
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
---
|
182
|
+
|
183
|
+
## Demo
|
184
|
+
|
185
|
+
1. Install gem
|
186
|
+
2. Generate resource (model, decanter, serializer, actions)
|
187
|
+
3. Add routes
|
188
|
+
|
189
|
+
---
|
190
|
+
|
191
|
+
## 1. Install Gem
|
192
|
+
|
193
|
+
Gemfile:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
gem 'sweet_actions'
|
197
|
+
```
|
198
|
+
|
199
|
+
Terminal:
|
200
|
+
|
201
|
+
```
|
202
|
+
bundle
|
203
|
+
```
|
204
|
+
|
205
|
+
---
|
206
|
+
|
207
|
+
## 2. Generate Resource
|
208
|
+
|
209
|
+
```
|
210
|
+
rails g model Event title:name start_date:date
|
211
|
+
rails g decanter Event title:name start_date:date
|
212
|
+
rails g serializer Event title:name start_date:date
|
213
|
+
rails g actions Events
|
214
|
+
```
|
215
|
+
|
216
|
+
---
|
217
|
+
|
218
|
+
## 3. Add Routes
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
Rails.application.routes.draw do
|
222
|
+
create_sweet_actions(:events)
|
223
|
+
end
|
224
|
+
```
|
data/PITCHME.yaml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# theme : black
|
data/README.md
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
# Sweet Actions
|
2
|
+
|
3
|
+
## The Idea
|
4
|
+
|
5
|
+
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
|
+
|
7
|
+
Which do you think has more in common in terms of programming logic?
|
8
|
+
|
9
|
+
1. events#create <=> events#index
|
10
|
+
2. events#create <=> articles#create
|
11
|
+
|
12
|
+
We would argue that #2 has more in common than #1. Both events#create and articles#create need to do the following:
|
13
|
+
|
14
|
+
1. Authorize the transaction
|
15
|
+
2. Validate the data
|
16
|
+
3. Persist the new record
|
17
|
+
4. Respond with new record (if successful) or error information (if unsuccessful) in the JSON
|
18
|
+
|
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.
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class EventsController < ApplicationController
|
23
|
+
# more similar to articles#create than events#index
|
24
|
+
def create
|
25
|
+
event = Event.new(event_params)
|
26
|
+
raise NotAuthorized unless can?(:create, event)
|
27
|
+
|
28
|
+
if event.save
|
29
|
+
UserMailer.new_event_confirmation(event).deliver_later
|
30
|
+
serialize(event)
|
31
|
+
else
|
32
|
+
serialize_errors(event)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# more similar to articles#index than events#create
|
37
|
+
def index
|
38
|
+
events = Event.where(date: >= Date.today)
|
39
|
+
serialize(events)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ArticlesController < ApplicationController
|
44
|
+
# more similar to events#create than articles#index
|
45
|
+
def create
|
46
|
+
article = Article.new(article_params)
|
47
|
+
raise NotAuthorized unless can?(:create, article)
|
48
|
+
|
49
|
+
if article.save
|
50
|
+
serialize(article)
|
51
|
+
else
|
52
|
+
serialize_errors(article)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# more similar to events#index than articles#create
|
57
|
+
def index
|
58
|
+
articles = Article.where(published: true)
|
59
|
+
serialize(articles)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
Instead, we propose a strategy that looks more like the following:
|
65
|
+
|
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
|
75
|
+
|
76
|
+
# ...
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
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
|
85
|
+
|
86
|
+
def authorized?
|
87
|
+
can?(:create, resource)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# resource logic for create (app/actions/events/create.rb)
|
92
|
+
module Events
|
93
|
+
class Create < CreateAction
|
94
|
+
def after_save
|
95
|
+
UserMailer.new_event_confirmation(resource).deliver_later
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
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
|
156
|
+
|
157
|
+
def authorized?
|
158
|
+
can?(:read, resource)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
app/sweet_actions/events/create.rb
|
165
|
+
|
166
|
+
```
|
167
|
+
module Events
|
168
|
+
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
|
179
|
+
end
|
180
|
+
|
181
|
+
def after_save
|
182
|
+
SiteMailer.notify_user
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
app/sweet_actions/events/show.rb
|
189
|
+
|
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)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
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
|
216
|
+
|
217
|
+
def save
|
218
|
+
resource.update(resource_params)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
app/sweet_actions/events/destroy.rb
|
225
|
+
|
226
|
+
```
|
227
|
+
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
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
## Installation
|
245
|
+
|
246
|
+
Gemfile:
|
247
|
+
|
248
|
+
```
|
249
|
+
gem 'sweet_actions'
|
250
|
+
```
|
251
|
+
|
252
|
+
Terminal:
|
253
|
+
|
254
|
+
```
|
255
|
+
bundle
|
256
|
+
bundle exec rails g sweet_actions:install
|
257
|
+
```
|