restful_json 3.0.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +313 -52
- data/lib/restful_json/config.rb +3 -1
- data/lib/restful_json/controller.rb +67 -22
- data/lib/restful_json/version.rb +1 -1
- data/lib/twinturbo/application_permitter.rb +93 -0
- metadata +11 -42
data/README.md
CHANGED
@@ -5,7 +5,13 @@ Develop declarative, featureful JSON RESTful-ish service controllers to use with
|
|
5
5
|
|
6
6
|
Should work with Rails 3.1+ and Rails 4. Feel free to submit issues and/or do a pull requests if you run into anything.
|
7
7
|
|
8
|
-
|
8
|
+
For the JSON response, you can either use [active_model_serializers][active_model_serializers], JBuilder, and perhaps others we haven't tested for control over the JSON response format. For active_model_serializers, it also provides the serialize_action class method in the controller to specify custom serializers (assuming you are using a later version of active_model_serializers that works with respond_with). For JBuilder, you can set render_enabled in the config to false.
|
9
|
+
|
10
|
+
For the incoming JSON, it uses Adam Hawkin's [permitter][permitter] code which uses [strong_parameters][strong_parameters] and [cancan][cancan]. Permitters are an object-oriented way of defining what is permitted in the incoming JSON.
|
11
|
+
|
12
|
+
An example app using restful_json with AngularJS is [employee-training-tracker][employee-training-tracker], featured in [Built with AngularJS][built_with_angularjs].
|
13
|
+
|
14
|
+
The goal of restful_json is to reduce service controller code in an intuitive way, not to be a be-all DSL. The intent is for there to be no "restful_json way". It is currently opinionated about use of Cancan, strong_parameters, etc. because twinturbo [permitter][permitter] strategy uses those, but you can cut the ties via a pull request if you'd like.
|
9
15
|
|
10
16
|
### Installation
|
11
17
|
|
@@ -17,7 +23,26 @@ and:
|
|
17
23
|
|
18
24
|
bundle install
|
19
25
|
|
20
|
-
|
26
|
+
### Dependencies
|
27
|
+
|
28
|
+
#### Rails
|
29
|
+
|
30
|
+
Need at least Rails 3.1.0, but supports 3.1.x, 3.2.x, and 4+.
|
31
|
+
|
32
|
+
Go with whatever you have in your Gemfile, e.g.:
|
33
|
+
|
34
|
+
gem 'rails', '3.2.11'
|
35
|
+
|
36
|
+
If you don't want all of Rails (probably not the case), you at least need actionpack and activerecord:
|
37
|
+
|
38
|
+
gem 'actionpack', '> 3.1.0'
|
39
|
+
gem 'activerecord', '> 3.1.0'
|
40
|
+
|
41
|
+
#### Cancan
|
42
|
+
|
43
|
+
To use [cancan][cancan], in your Gemfile, add:
|
44
|
+
|
45
|
+
gem 'cancan', '~> 1.6.7'
|
21
46
|
|
22
47
|
In your `app/controllers/application_controller.rb` or in your service controllers, make sure `current_user` is set:
|
23
48
|
|
@@ -41,31 +66,68 @@ In `app/models/ability.rb`, setup a basic cancan ability. Just for testing we'll
|
|
41
66
|
end
|
42
67
|
end
|
43
68
|
|
44
|
-
|
69
|
+
#### Strong Parameters
|
70
|
+
|
71
|
+
To use [strong_parameters][strong_parameters], in your Gemfile, add:
|
45
72
|
|
46
|
-
|
73
|
+
gem 'strong_parameters', '~> 0.1.3'
|
47
74
|
|
48
|
-
|
75
|
+
If you're using Rails 3.1.x/3.2.x and you want to disable the default whitelisting that occurs in later versions of Rails because you are going to use Strong Parameters, change the config.active_record.whitelist_attributes property in your `config/application.rb`:
|
49
76
|
|
50
|
-
|
77
|
+
config.active_record.whitelist_attributes = false
|
51
78
|
|
52
|
-
|
79
|
+
This will allow you to remove / not have to use attr_accessible and do mass assignment inside your code and tests. Instead you will put this information into your permitters.
|
53
80
|
|
54
|
-
|
81
|
+
Note that restful_json automatically includes `ActiveModel::ForbiddenAttributesProtection` on all models, as is done in Rails 4.
|
55
82
|
|
56
|
-
|
83
|
+
#### JSON Response Generators
|
57
84
|
|
58
|
-
|
85
|
+
##### ActiveModel Serializers
|
59
86
|
|
60
|
-
If you want to
|
87
|
+
If you want to use [ActiveModel Serializers][[active_model_serializers], use:
|
61
88
|
|
62
|
-
|
89
|
+
gem 'active_model_serializers'
|
63
90
|
|
64
|
-
|
91
|
+
Then create your serializers, e.g.:
|
92
|
+
|
93
|
+
/app/serializers/singular_model_name_serializer.rb
|
94
|
+
|
95
|
+
Without having to do anything else, each restful_json controller will use `/app/serializers/singular_model_name_serializer.rb`, e.g. `/app/serializers/bar_serializer.rb` for the actions: index, show, new, create, update, destroy (not edit).
|
96
|
+
|
97
|
+
If you want to define a different serializer another action, e.g. the index action so that a list of instances has a different JSON format:
|
98
|
+
|
99
|
+
serialize_action :index, with: BarsSerializer
|
100
|
+
|
101
|
+
You can also use a specific format for multiple actions:
|
102
|
+
|
103
|
+
serialize_action :index, :my_other_list_action, with: BarsSerializer
|
104
|
+
|
105
|
+
The built-in actions that support custom serializers (you can add more) are: index, show, new, create, update, destroy, and any action you automatically have created via using the restful_json `query_for` method (keep reading!).
|
106
|
+
|
107
|
+
##### JBuilder
|
108
|
+
|
109
|
+
If you want to use JBuilder instead to render, first:
|
65
110
|
|
66
|
-
|
111
|
+
gem 'jbuilder'
|
67
112
|
|
68
|
-
|
113
|
+
If you want to enable JBuilder for all restful_json services, you need to disable all renders and respond_withs in the controller:
|
114
|
+
|
115
|
+
RestfulJson.render_enabled = false
|
116
|
+
|
117
|
+
Or you can also just enable/disable rendering in a controller via setting `self.render_enabled`:
|
118
|
+
|
119
|
+
self.render_enabled = false
|
120
|
+
|
121
|
+
Then make sure you add a JBuilder view for each action you require. Although all may not be relevant, we support: index, show, new, edit, create, update, destroy. Maybe you'd create:
|
122
|
+
|
123
|
+
/app/views/plural_name_of_model/index.json.jbuilder
|
124
|
+
/app/views/plural_name_of_model/show.json.jbuilder
|
125
|
+
/app/views/plural_name_of_model/create.json.jbuilder
|
126
|
+
/app/views/plural_name_of_model/update.json.jbuilder
|
127
|
+
|
128
|
+
See [Railscast #320][railscast320] for examples.
|
129
|
+
|
130
|
+
### App-level Configuration
|
69
131
|
|
70
132
|
At the bottom of `config/environment.rb`, you can set config items one at a time like:
|
71
133
|
|
@@ -74,67 +136,149 @@ At the bottom of `config/environment.rb`, you can set config items one at a time
|
|
74
136
|
or in bulk like:
|
75
137
|
|
76
138
|
RestfulJson.configure do
|
77
|
-
|
78
|
-
|
79
|
-
self.
|
80
|
-
|
81
|
-
|
82
|
-
self.
|
83
|
-
|
139
|
+
|
140
|
+
# default for :using in can_filter_by
|
141
|
+
self.can_filter_by_default_using = [:eq]
|
142
|
+
|
143
|
+
# to output debugging info during request handling
|
144
|
+
self.debug = false
|
145
|
+
|
146
|
+
# delimiter for values in request parameter values
|
147
|
+
self.filter_split = ','
|
148
|
+
|
149
|
+
# equivalent to specifying respond_to :json, :html in the controller, and can be overriden in the controller. Note that by default responders gem sets respond_to :html in application_controller.rb.
|
150
|
+
self.formats = :json, :html
|
151
|
+
|
152
|
+
# default number of records to return if using the page request function
|
153
|
+
self.number_of_records_in_a_page = 15
|
154
|
+
|
155
|
+
# delimiter for ARel predicate in the request parameter name
|
156
|
+
self.predicate_prefix = '!'
|
157
|
+
|
158
|
+
# if true, will render resource and HTTP 201 for post/create or resource and HTTP 200 for put/update
|
159
|
+
self.return_resource = false
|
160
|
+
|
84
161
|
end
|
85
162
|
|
86
|
-
|
163
|
+
### Controller-specific Configuration
|
164
|
+
|
165
|
+
In the controller, you can set a variety of class attributes with `self.something = ...` in the body of your controller.
|
166
|
+
|
167
|
+
All of the app-level configuration parameters are configurable at the controller level:
|
168
|
+
|
169
|
+
self.can_filter_by_default_using = [:eq]
|
170
|
+
self.debug = false
|
171
|
+
self.filter_split = ','
|
172
|
+
self.formats = :json, :html
|
173
|
+
self.number_of_records_in_a_page = 15
|
174
|
+
self.predicate_prefix = '!'
|
175
|
+
self.return_resource = false
|
176
|
+
|
177
|
+
In addition there are some that are controller-only...
|
178
|
+
|
179
|
+
If you don't use the standard controller naming convention, you can define this in the controller:
|
87
180
|
|
88
|
-
|
181
|
+
self.model_class = YourModel
|
182
|
+
|
183
|
+
If it doesn't handle the other forms well, you can explicitly define the singular/plural names:
|
184
|
+
|
185
|
+
self.model_singular_name = 'your_model'
|
186
|
+
self.model_plural_name = 'your_models'
|
187
|
+
|
188
|
+
These are used for *_url method definitions, to set instance variables like `@foobar` and `@foobars` dynamically, etc.
|
189
|
+
|
190
|
+
Other class attributes are available for setting/overriding, but they are all set by the other class methods defined in the next section.
|
89
191
|
|
90
192
|
### Usage
|
91
193
|
|
92
|
-
By using `acts_as_restful_json
|
194
|
+
By using `acts_as_restful_json`, you have a configurable generic Rails 3.1+/4+ controller that does the index, show, create, and update and other custom actions easily for you.
|
93
195
|
|
94
|
-
Everything is well-declared and concise
|
196
|
+
Everything is well-declared and fairly concise.
|
95
197
|
|
96
|
-
|
198
|
+
You can have something as simple as:
|
199
|
+
|
200
|
+
class FoobarsController < ApplicationController
|
201
|
+
|
97
202
|
acts_as_restful_json
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
which would use the restful_json configuration and the controller's classname for the service definition.
|
207
|
+
|
208
|
+
Or you can define more bells and whistles (read on to see what these do...):
|
209
|
+
|
210
|
+
class FoobarsController < ApplicationController
|
211
|
+
|
212
|
+
acts_as_restful_json
|
213
|
+
|
98
214
|
query_for :index, is: ->(t,q) {q.joins(:apples, :pears).where(apples: {color: 'green'}).where(pears: {color: 'green'})}
|
215
|
+
|
99
216
|
# args sent to can_filter_by are the request parameter name(s)
|
100
|
-
|
101
|
-
|
217
|
+
|
218
|
+
# implies using: [:eq] because RestfulJson.can_filter_by_default_using = [:eq]
|
219
|
+
can_filter_by :foo_id
|
220
|
+
|
221
|
+
# can specify multiple predicates and optionally a default value
|
222
|
+
can_filter_by :foo_date, :bar_date, using: [:lt, :eq, :gt], with_default: Time.now
|
223
|
+
|
102
224
|
can_filter_by :a_request_param_name, with_query: ->(t,q,param_value) {q.joins(:some_assoc).where(:some_assocs_table_name=>{some_attr: param_value})}
|
225
|
+
|
103
226
|
can_filter_by :and_another, through: [:some_attribute_on_this_model]
|
227
|
+
|
104
228
|
can_filter_by :one_more, through: [:some_association, :some_attribute_on_some_association_model]
|
229
|
+
|
105
230
|
can_filter_by :and_one_more, through: [:my_assoc, :my_assocs_assoc, :my_assocs_assocs_assoc, :an_attribute_on_my_assocs_assocs_assoc]
|
231
|
+
|
106
232
|
supports_functions :count, :uniq, :take, :skip, :page, :page_count
|
233
|
+
|
107
234
|
order_by {:foo_date => :asc}, :foo_color, {:bar_date => :desc} # an ordered array of hashes, assumes :asc if not a hash
|
108
|
-
|
235
|
+
|
236
|
+
# comma-delimited if you want more than :json, e.g. :json, :html
|
237
|
+
respond_to :json, :html
|
238
|
+
|
109
239
|
end
|
110
240
|
|
111
|
-
|
241
|
+
#### Default Filtering by Attribute(s)
|
112
242
|
|
113
|
-
|
114
|
-
Arel::Predications.public_instance_methods.sort
|
243
|
+
First, declare in the controller:
|
115
244
|
|
116
|
-
|
245
|
+
can_filter_by :foo_id
|
117
246
|
|
118
|
-
|
247
|
+
If `RestfulJson.can_filter_by_default_using = [:eq]` as it is by default, then you can now get Foobars with a foo_id of '1':
|
119
248
|
|
120
|
-
|
249
|
+
http://localhost:3000/foobars?foo_id=1
|
121
250
|
|
122
|
-
`can_filter_by` can
|
251
|
+
`can_filter_by` without an option means you can send in that request param (via routing or directly, just like normal in Rails) and it will use that in the ARel query (safe from SQL injection and only letting you do what you tell it). `:using` means you can use those [ARel][arel] predicates for filtering. If you do `Arel::Predications.public_instance_methods.sort` in Rails console, you can see a list of the available predicates. So, you could get crazy with:
|
123
252
|
|
124
|
-
|
253
|
+
can_filter_by :does_not_match, :does_not_match_all, :does_not_match_any, :eq, :eq_all, :eq_any, :gt, :gt_all, :gt_any, :gteq, :gteq_all, :gteq_any, :in, :in_all, :in_any, :lt, :lt_all, :lt_any, :lteq, :lteq_all, :lteq_any, :matches, :matches_all, :matches_any, :not_eq, :not_eq_all, :not_eq_any, :not_in, :not_in_all, :not_in_any
|
254
|
+
|
255
|
+
`can_filter_by` can also specify a `:with_query` to provide a lambda that takes the request parameter in when it is provided by the request.
|
125
256
|
|
126
257
|
can_filter_by :a_request_param_name, with_query: ->(t,q,param_value) {q.joins(:some_assoc).where(:some_assocs_table_name=>{some_attr: param_value})}
|
127
|
-
can_filter_by :a_request_param_name, through: [:some_assoc, :some_attr] # much easier, but not as flexible as a lambda
|
128
258
|
|
129
|
-
|
259
|
+
And `can_filter_by` can specify a `:through` to provide an easy way to inner join through a bunch of models using ActiveRecord relations, by specifying 0-to-many association names to go "through" to the final argument, which is the attribute name on the last model. The following is equivalent to the last query:
|
130
260
|
|
131
|
-
|
261
|
+
can_filter_by :a_request_param_name, through: [:some_assoc, :some_attr]
|
132
262
|
|
133
|
-
|
263
|
+
Let's say you are in MagicalValleyController, and the MagicalValley model `has many :magical_unicorns`. The MagicalUnicorn model has an attribute called `name`. You want to return MagicalValleys that are associated with all of the MagicalUnicorns named 'Rainbow'. You could do either:
|
134
264
|
|
135
|
-
|
265
|
+
can_filter_by :magical_unicorn_name, with_query: ->(t,q,param_value) {q.joins(:magical_unicorns).where(:magical_unicorns=>{name: param_value})}
|
136
266
|
|
137
|
-
|
267
|
+
or:
|
268
|
+
|
269
|
+
can_filter_by :magical_unicorn_name, through: [:magical_unicorns, :name]
|
270
|
+
|
271
|
+
and you can then use this:
|
272
|
+
|
273
|
+
http://localhost:3000/magical_valleys?magical_unicorn_name=Rainbow
|
274
|
+
|
275
|
+
or if a MagicalUnicorn `has_many :friends` and a MagicalUnicorn's friend has a name attribute:
|
276
|
+
|
277
|
+
can_filter_by :magical_unicorn_friend_name, through: [:magical_unicorns, :friends, :name]
|
278
|
+
|
279
|
+
and use this to get valleys associated with unicorns who in turn have a friend named Oscar:
|
280
|
+
|
281
|
+
http://localhost:3000/magical_valleys?magical_unicorn_friend_name=Oscar
|
138
282
|
|
139
283
|
#### Other Filters by Attribute(s)
|
140
284
|
|
@@ -150,17 +294,23 @@ Multiple values are separated by `filter_split` (configurable):
|
|
150
294
|
|
151
295
|
http://localhost:3000/foobars?seen_on!eq_any=2012-08-08,2012-09-09
|
152
296
|
|
153
|
-
####
|
297
|
+
#### Supported Functions
|
298
|
+
|
299
|
+
##### Declaring
|
300
|
+
|
301
|
+
`supports_functions` lets you allow the [ARel][arel] functions: `:uniq`, `:skip`, `:take`, and/or `:count`.
|
302
|
+
|
303
|
+
##### Unique (DISTINCT)
|
154
304
|
|
155
305
|
First, declare in the controller:
|
156
306
|
|
157
307
|
supports_functions :uniq
|
158
308
|
|
159
|
-
To return a simple unique view of a model, combine use of an
|
309
|
+
To return a simple unique view of a model, combine use of an [ActiveModel Serializer][active_model_serializers] that returns just the attribute you want along with the uniq param, e.g. to return unique/distinct colors of foobars you'd have a serializer to just return the color and then use:
|
160
310
|
|
161
311
|
http://localhost:3000/foobars?uniq=
|
162
312
|
|
163
|
-
|
313
|
+
##### Count
|
164
314
|
|
165
315
|
First, declare in the controller:
|
166
316
|
|
@@ -170,7 +320,7 @@ This is another filter that can be used with the others, but instead of returnin
|
|
170
320
|
|
171
321
|
http://localhost:3000/foobars?count=
|
172
322
|
|
173
|
-
|
323
|
+
##### Paging
|
174
324
|
|
175
325
|
In controller make sure these are included:
|
176
326
|
|
@@ -198,7 +348,7 @@ To set page size at controller level:
|
|
198
348
|
|
199
349
|
For a better idea of how this works on the backend, look at [ARel][arel]'s skip and take, or see Variable Paging.
|
200
350
|
|
201
|
-
|
351
|
+
###### Skip and Take (OFFSET and LIMIT)
|
202
352
|
|
203
353
|
In controller make sure these are included:
|
204
354
|
|
@@ -212,8 +362,6 @@ To limit the number of rows returned, use 'take'. It is called take, because tak
|
|
212
362
|
|
213
363
|
http://localhost:3000/foobars.json?take=5
|
214
364
|
|
215
|
-
##### Variable Paging
|
216
|
-
|
217
365
|
Combine skip and take for manual completely customized paging.
|
218
366
|
|
219
367
|
Get the first page of 15 results:
|
@@ -257,7 +405,7 @@ or use the `->` Ruby 1.9 lambda stab operator. You can also filter out items tha
|
|
257
405
|
.where(pears: {color: 'green'})
|
258
406
|
}
|
259
407
|
|
260
|
-
##### Custom Actions
|
408
|
+
##### Define Custom Actions with Custom Queries
|
261
409
|
|
262
410
|
`query_for` also will `alias_method (some action), :index` anything other than `:index`, so you can easily create custom non-RESTful action methods:
|
263
411
|
|
@@ -267,6 +415,8 @@ or use the `->` Ruby 1.9 lambda stab operator. You can also filter out items tha
|
|
267
415
|
|
268
416
|
Note that it is a proc so you can really do whatever you want with it and will have access to other things in the environment or can call another method, etc.
|
269
417
|
|
418
|
+
You are still working with regular controllers here, so add or override methods if you want more!
|
419
|
+
|
270
420
|
### Routing
|
271
421
|
|
272
422
|
Respects regular and nested Rails resourceful routing and controller namespacing, e.g. in `config/routes.rb`:
|
@@ -279,6 +429,113 @@ Respects regular and nested Rails resourceful routing and controller namespacing
|
|
279
429
|
end
|
280
430
|
end
|
281
431
|
|
432
|
+
### Experimental Usage
|
433
|
+
|
434
|
+
#### Rails-api
|
435
|
+
|
436
|
+
If you want to try out [rails-api][rails-api], maybe use:
|
437
|
+
|
438
|
+
gem 'rails-api', '~> 0.0.3'
|
439
|
+
|
440
|
+
class ServiceController < ActionController::API
|
441
|
+
|
442
|
+
include AbstractController::Translation
|
443
|
+
include ActionController::HttpAuthentication::Basic::ControllerMethods
|
444
|
+
include AbstractController::Layouts
|
445
|
+
include ActionController::MimeResponds
|
446
|
+
include ActionController::Cookies
|
447
|
+
include ActionController::ParamsWrapper
|
448
|
+
acts_as_restful_json
|
449
|
+
|
450
|
+
def current_user
|
451
|
+
User.new
|
452
|
+
end
|
453
|
+
|
454
|
+
end
|
455
|
+
|
456
|
+
Note that in `/config/initializers/wrap_parameters.rb` you might need to add `include ActionController::ParamsWrapper` prior to the `wrap_parameters` call. For example, for unwrapped JSON, it would look like:
|
457
|
+
|
458
|
+
ActiveSupport.on_load(:action_controller) do
|
459
|
+
# without include of ParamsWrapper, will get undefined method `wrap_parameters' for ActionController::API:Class (NoMethodError)
|
460
|
+
include ActionController::ParamsWrapper
|
461
|
+
# in this case it's expecting unwrapped params, but we could maybe use wrap_parameters format: [:json]
|
462
|
+
wrap_parameters format: []
|
463
|
+
end
|
464
|
+
|
465
|
+
# Disable root element in JSON by default.
|
466
|
+
ActiveSupport.on_load(:active_record) do
|
467
|
+
self.include_root_in_json = false
|
468
|
+
end
|
469
|
+
|
470
|
+
#### Customing the Default Behavior
|
471
|
+
|
472
|
+
In `app/controllers/able_to_mark_on_destroy.rb`, you could have:
|
473
|
+
|
474
|
+
module AbleToMarkOnDestroy
|
475
|
+
extend ActiveSupport::Concern
|
476
|
+
|
477
|
+
included do
|
478
|
+
# things you would put in the body of the class
|
479
|
+
class_attribute :deleted_status_column, instance_writer: true
|
480
|
+
end
|
481
|
+
|
482
|
+
# no need to define class methods for this example, but this is where you'd do it
|
483
|
+
#module ClassMethods
|
484
|
+
#end
|
485
|
+
|
486
|
+
# instance methods from here on out...
|
487
|
+
|
488
|
+
# Note: overriding destroy in restful_json to support marking deleted_status_column with 'deleted'
|
489
|
+
def destroy
|
490
|
+
# to_s for vulnerabilities similar to CVE-2013-1854 in older Rails...
|
491
|
+
@value = @model_class.find(params[:id].to_s)
|
492
|
+
unless @value.nil? || !self.deleted_status_column
|
493
|
+
@value.update_attributes!({self.deleted_status_column.to_sym => 'deleted'})
|
494
|
+
else
|
495
|
+
@value.destroy
|
496
|
+
end
|
497
|
+
|
498
|
+
instance_variable_set(@model_at_singular_name_sym, @value)
|
499
|
+
|
500
|
+
if self.render_enabled
|
501
|
+
# you could take out the custom_action_serializer support if you aren't using
|
502
|
+
custom_action_serializer = self.action_to_serializer[params[:action].to_s]
|
503
|
+
|
504
|
+
if !@value.nil? && RestfulJson.return_resource
|
505
|
+
respond_with(@value) do |format|
|
506
|
+
format.json do
|
507
|
+
# for errors added in before_destroy validation or in update_attributes
|
508
|
+
if @value.errors.empty?
|
509
|
+
render custom_action_serializer ? {json: @value, status: :ok, serializer: custom_action_serializer} : {json: @value, status: :ok}
|
510
|
+
else
|
511
|
+
render custom_action_serializer ? {json: {errors: @value.errors}, status: :unprocessable_entity, serializer: custom_action_serializer} : {json: {errors: @value.errors}, status: :unprocessable_entity}
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
else
|
516
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
Then in your controller:
|
523
|
+
|
524
|
+
acts_as_restful_json
|
525
|
+
include AbleToMarkOnDestroy
|
526
|
+
|
527
|
+
self.deleted_status_column = :foo_deleted
|
528
|
+
|
529
|
+
And when you do a RESTful call to destroy id 123 via a DELETE:
|
530
|
+
|
531
|
+
http://localhost:3000/foobars/123
|
532
|
+
|
533
|
+
Instead of deleting the DB row, the 'foobars' table row's column 'foo_deleted' would be set to 'deleted'.
|
534
|
+
|
535
|
+
### Thanks!
|
536
|
+
|
537
|
+
Without our users, where would we be? Feedback, bug reports, and code/documentation contributions are always welcome!
|
538
|
+
|
282
539
|
### Contributors
|
283
540
|
|
284
541
|
* Gary Weaver (https://github.com/garysweaver)
|
@@ -288,6 +545,8 @@ Respects regular and nested Rails resourceful routing and controller namespacing
|
|
288
545
|
|
289
546
|
Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
|
290
547
|
|
548
|
+
[employee-training-tracker]: https://github.com/FineLinePrototyping/employee-training-tracker
|
549
|
+
[built_with_angularjs]: http://builtwith.angularjs.org/
|
291
550
|
[permitter]: http://broadcastingadam.com/2012/07/parameter_authorization_in_rails_apis/
|
292
551
|
[cancan]: https://github.com/ryanb/cancan
|
293
552
|
[strong_parameters]: https://github.com/rails/strong_parameters
|
@@ -296,4 +555,6 @@ Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
|
|
296
555
|
[devise]: https://github.com/plataformatec/devise
|
297
556
|
[arel]: https://github.com/rails/arel
|
298
557
|
[ar]: http://api.rubyonrails.org/classes/ActiveRecord/Relation.html
|
558
|
+
[rails-api]: https://github.com/rails-api/rails-api
|
559
|
+
[railscast320]: http://railscasts.com/episodes/320-jbuilder
|
299
560
|
[lic]: http://github.com/rubyservices/restful_json/blob/master/LICENSE
|
data/lib/restful_json/config.rb
CHANGED
@@ -6,7 +6,8 @@ module RestfulJson
|
|
6
6
|
:formats,
|
7
7
|
:number_of_records_in_a_page,
|
8
8
|
:predicate_prefix,
|
9
|
-
:return_resource
|
9
|
+
:return_resource,
|
10
|
+
:render_enabled
|
10
11
|
]
|
11
12
|
|
12
13
|
class << self
|
@@ -24,4 +25,5 @@ RestfulJson.configure do
|
|
24
25
|
self.number_of_records_in_a_page = 15
|
25
26
|
self.predicate_prefix = '!'
|
26
27
|
self.return_resource = false
|
28
|
+
self.render_enabled = true
|
27
29
|
end
|
@@ -40,6 +40,7 @@ module RestfulJson
|
|
40
40
|
class_attribute :action_to_query, instance_writer: true
|
41
41
|
class_attribute :param_to_query, instance_writer: true
|
42
42
|
class_attribute :param_to_through, instance_writer: true
|
43
|
+
class_attribute :action_to_serializer, instance_writer: true
|
43
44
|
|
44
45
|
# use values from config
|
45
46
|
# debug uses RestfulJson.debug? because until this is done no local debug class attribute exists to check
|
@@ -54,6 +55,7 @@ module RestfulJson
|
|
54
55
|
self.action_to_query ||= {}
|
55
56
|
self.param_to_query ||= {}
|
56
57
|
self.param_to_through ||= {}
|
58
|
+
self.action_to_serializer ||= {}
|
57
59
|
end
|
58
60
|
|
59
61
|
module ClassMethods
|
@@ -134,6 +136,21 @@ module RestfulJson
|
|
134
136
|
def order_by(args)
|
135
137
|
self.ordered_by = (Array.wrap(self.ordered_by) + Array.wrap(args)).flatten.compact.collect {|item|item.is_a?(Hash) ? item : {item.to_sym => :asc}}
|
136
138
|
end
|
139
|
+
|
140
|
+
# Associate a non-standard ActiveModel Serializer for one or more actions, e.g.
|
141
|
+
# serialize_action :index, with: FoosSerializer
|
142
|
+
# or
|
143
|
+
# serialize_action :index, :some_custom_action, with: FoosSerializer
|
144
|
+
def serialize_action(*args)
|
145
|
+
options = args.extract_options!
|
146
|
+
args.each do |an_action|
|
147
|
+
if options[:with]
|
148
|
+
self.action_to_serializer[an_action.to_s] = options[:with]
|
149
|
+
else
|
150
|
+
raise "#{self.class.name} must supply an :with option with serialize_action #{an_action.inspect}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
137
154
|
end
|
138
155
|
|
139
156
|
# In initialize we:
|
@@ -268,7 +285,11 @@ module RestfulJson
|
|
268
285
|
|
269
286
|
@value = value
|
270
287
|
instance_variable_set(@model_at_plural_name_sym, @value)
|
271
|
-
|
288
|
+
|
289
|
+
if self.render_enabled
|
290
|
+
custom_action_serializer = self.action_to_serializer[params[:action].to_s]
|
291
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
292
|
+
end
|
272
293
|
end
|
273
294
|
|
274
295
|
# The controller's show (get) method to return a resource.
|
@@ -276,13 +297,21 @@ module RestfulJson
|
|
276
297
|
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
277
298
|
@value = @model_class.find(params[:id].to_s)
|
278
299
|
instance_variable_set(@model_at_singular_name_sym, @value)
|
279
|
-
|
300
|
+
|
301
|
+
if self.render_enabled
|
302
|
+
custom_action_serializer = self.action_to_serializer[params[:action].to_s]
|
303
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
304
|
+
end
|
280
305
|
end
|
281
306
|
|
282
307
|
# The controller's new method (e.g. used for new record in html format).
|
283
308
|
def new
|
284
309
|
@value = @model_class.new
|
285
|
-
|
310
|
+
|
311
|
+
if self.render_enabled
|
312
|
+
custom_action_serializer = self.action_to_serializer[params[:action].to_s]
|
313
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
314
|
+
end
|
286
315
|
end
|
287
316
|
|
288
317
|
# The controller's edit method (e.g. used for edit record in html format).
|
@@ -298,18 +327,27 @@ module RestfulJson
|
|
298
327
|
@value = @model_class.new(permitted_params)
|
299
328
|
@value.save
|
300
329
|
instance_variable_set(@model_at_singular_name_sym, @value)
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
330
|
+
|
331
|
+
if self.render_enabled
|
332
|
+
custom_action_serializer = self.action_to_serializer[params[:action].to_s]
|
333
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
334
|
+
end
|
335
|
+
|
336
|
+
if self.render_enabled
|
337
|
+
custom_action_serializer = self.action_to_serializer[params[:action].to_s]
|
338
|
+
if !@value.nil? && RestfulJson.return_resource
|
339
|
+
respond_with(@value) do |format|
|
340
|
+
format.json do
|
341
|
+
if @value.errors.empty?
|
342
|
+
render custom_action_serializer ? {json: @value, status: :created, serializer: custom_action_serializer} : {json: @value, status: :created}
|
343
|
+
else
|
344
|
+
render custom_action_serializer ? {json: {errors: @value.errors}, status: :unprocessable_entity, serializer: custom_action_serializer} : {json: {errors: @value.errors}, status: :unprocessable_entity}
|
345
|
+
end
|
308
346
|
end
|
309
347
|
end
|
348
|
+
else
|
349
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
310
350
|
end
|
311
|
-
else
|
312
|
-
respond_with @value
|
313
351
|
end
|
314
352
|
end
|
315
353
|
|
@@ -320,18 +358,22 @@ module RestfulJson
|
|
320
358
|
@value = @model_class.find(params[:id].to_s)
|
321
359
|
@value.update_attributes(permitted_params)
|
322
360
|
instance_variable_set(@model_at_singular_name_sym, @value)
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
361
|
+
|
362
|
+
if self.render_enabled
|
363
|
+
custom_action_serializer = self.action_to_serializer[params[:action].to_s]
|
364
|
+
if !@value.nil? && RestfulJson.return_resource
|
365
|
+
respond_with(@value) do |format|
|
366
|
+
format.json do
|
367
|
+
if @value.errors.empty?
|
368
|
+
render custom_action_serializer ? {json: @value, status: :ok, serializer: custom_action_serializer} : {json: @value, status: :ok}
|
369
|
+
else
|
370
|
+
render custom_action_serializer ? {json: {errors: @value.errors}, status: :unprocessable_entity, serializer: custom_action_serializer} : {json: {errors: @value.errors}, status: :unprocessable_entity}
|
371
|
+
end
|
330
372
|
end
|
331
373
|
end
|
374
|
+
else
|
375
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
332
376
|
end
|
333
|
-
else
|
334
|
-
respond_with @value
|
335
377
|
end
|
336
378
|
end
|
337
379
|
|
@@ -341,7 +383,10 @@ module RestfulJson
|
|
341
383
|
@value = @model_class.find(params[:id].to_s)
|
342
384
|
@value.destroy
|
343
385
|
instance_variable_set(@model_at_singular_name_sym, @value)
|
344
|
-
|
386
|
+
|
387
|
+
if self.render_enabled
|
388
|
+
respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
|
389
|
+
end
|
345
390
|
end
|
346
391
|
|
347
392
|
end
|
data/lib/restful_json/version.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
# from Adam Hawkins's gist:
|
2
|
+
# https://gist.github.com/3150306
|
3
|
+
# http://www.broadcastingadam.com/2012/07/parameter_authorization_in_rails_apis/
|
4
|
+
class ApplicationPermitter
|
5
|
+
class PermittedAttribute < Struct.new(:name, :options) ; end
|
6
|
+
|
7
|
+
delegate :authorize!, :to => :ability
|
8
|
+
class_attribute :permitted_attributes
|
9
|
+
self.permitted_attributes = []
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def permit(*args)
|
13
|
+
options = args.extract_options!
|
14
|
+
|
15
|
+
args.each do |name|
|
16
|
+
self.permitted_attributes += [PermittedAttribute.new(name, options)]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def scope(name)
|
21
|
+
with_options :scope => name do |nested|
|
22
|
+
yield nested
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(params, user, ability = nil)
|
28
|
+
@params, @user, @ability = params, user, ability
|
29
|
+
end
|
30
|
+
|
31
|
+
def permitted_params
|
32
|
+
authorize_params!
|
33
|
+
filtered_params
|
34
|
+
end
|
35
|
+
|
36
|
+
def resource_name
|
37
|
+
self.class.to_s.match(/(.+)Permitter/)[1].underscore.to_sym
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def authorize_params!
|
44
|
+
needing_authorization = permitted_attributes.select { |a| a.options[:authorize] }
|
45
|
+
|
46
|
+
needing_authorization.each do |attribute|
|
47
|
+
if attribute.options[:scope]
|
48
|
+
values = Array.wrap(filtered_params[attribute.options[:scope]]).collect do |hash|
|
49
|
+
hash[attribute.name]
|
50
|
+
end.compact
|
51
|
+
else
|
52
|
+
values = Array.wrap filtered_params[attribute.name]
|
53
|
+
end
|
54
|
+
|
55
|
+
klass = (attribute.options[:as].try(:to_s) || attribute.name.to_s.split(/(.+)_ids?/)[1]).classify.constantize
|
56
|
+
|
57
|
+
values.each do |record_id|
|
58
|
+
record = klass.find record_id
|
59
|
+
permission = attribute.options[:authorize].to_sym || :read
|
60
|
+
authorize! permission, record
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def filtered_params
|
66
|
+
scopes = {}
|
67
|
+
unscoped_attributes = []
|
68
|
+
|
69
|
+
permitted_attributes.each do |attribute|
|
70
|
+
if attribute.options[:scope]
|
71
|
+
key = attribute.options[:scope]
|
72
|
+
scopes[key] ||= []
|
73
|
+
scopes[key] << attribute.name
|
74
|
+
else
|
75
|
+
unscoped_attributes << attribute.name
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
@filtered_params ||= params.require(resource_name).permit(*unscoped_attributes, scopes)
|
80
|
+
end
|
81
|
+
|
82
|
+
def params
|
83
|
+
@params
|
84
|
+
end
|
85
|
+
|
86
|
+
def user
|
87
|
+
@user
|
88
|
+
end
|
89
|
+
|
90
|
+
def ability
|
91
|
+
@ability ||= Ability.new user
|
92
|
+
end
|
93
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restful_json
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0
|
4
|
+
version: 3.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,40 +10,8 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-03-
|
13
|
+
date: 2013-03-22 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
|
-
- !ruby/object:Gem::Dependency
|
16
|
-
name: rails-api
|
17
|
-
requirement: !ruby/object:Gem::Requirement
|
18
|
-
none: false
|
19
|
-
requirements:
|
20
|
-
- - ! '>='
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '0'
|
23
|
-
type: :runtime
|
24
|
-
prerelease: false
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
none: false
|
27
|
-
requirements:
|
28
|
-
- - ! '>='
|
29
|
-
- !ruby/object:Gem::Version
|
30
|
-
version: '0'
|
31
|
-
- !ruby/object:Gem::Dependency
|
32
|
-
name: active_model_serializers
|
33
|
-
requirement: !ruby/object:Gem::Requirement
|
34
|
-
none: false
|
35
|
-
requirements:
|
36
|
-
- - ! '>='
|
37
|
-
- !ruby/object:Gem::Version
|
38
|
-
version: '0'
|
39
|
-
type: :runtime
|
40
|
-
prerelease: false
|
41
|
-
version_requirements: !ruby/object:Gem::Requirement
|
42
|
-
none: false
|
43
|
-
requirements:
|
44
|
-
- - ! '>='
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '0'
|
47
15
|
- !ruby/object:Gem::Dependency
|
48
16
|
name: actionpack
|
49
17
|
requirement: !ruby/object:Gem::Requirement
|
@@ -51,7 +19,7 @@ dependencies:
|
|
51
19
|
requirements:
|
52
20
|
- - ! '>='
|
53
21
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
22
|
+
version: 3.1.0
|
55
23
|
type: :runtime
|
56
24
|
prerelease: false
|
57
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -59,7 +27,7 @@ dependencies:
|
|
59
27
|
requirements:
|
60
28
|
- - ! '>='
|
61
29
|
- !ruby/object:Gem::Version
|
62
|
-
version:
|
30
|
+
version: 3.1.0
|
63
31
|
- !ruby/object:Gem::Dependency
|
64
32
|
name: activerecord
|
65
33
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,7 +35,7 @@ dependencies:
|
|
67
35
|
requirements:
|
68
36
|
- - ! '>='
|
69
37
|
- !ruby/object:Gem::Version
|
70
|
-
version:
|
38
|
+
version: 3.1.0
|
71
39
|
type: :runtime
|
72
40
|
prerelease: false
|
73
41
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -75,7 +43,7 @@ dependencies:
|
|
75
43
|
requirements:
|
76
44
|
- - ! '>='
|
77
45
|
- !ruby/object:Gem::Version
|
78
|
-
version:
|
46
|
+
version: 3.1.0
|
79
47
|
- !ruby/object:Gem::Dependency
|
80
48
|
name: cancan
|
81
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -83,7 +51,7 @@ dependencies:
|
|
83
51
|
requirements:
|
84
52
|
- - ! '>='
|
85
53
|
- !ruby/object:Gem::Version
|
86
|
-
version:
|
54
|
+
version: 1.6.7
|
87
55
|
type: :runtime
|
88
56
|
prerelease: false
|
89
57
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -91,7 +59,7 @@ dependencies:
|
|
91
59
|
requirements:
|
92
60
|
- - ! '>='
|
93
61
|
- !ruby/object:Gem::Version
|
94
|
-
version:
|
62
|
+
version: 1.6.7
|
95
63
|
- !ruby/object:Gem::Dependency
|
96
64
|
name: strong_parameters
|
97
65
|
requirement: !ruby/object:Gem::Requirement
|
@@ -99,7 +67,7 @@ dependencies:
|
|
99
67
|
requirements:
|
100
68
|
- - ! '>='
|
101
69
|
- !ruby/object:Gem::Version
|
102
|
-
version:
|
70
|
+
version: 0.1.3
|
103
71
|
type: :runtime
|
104
72
|
prerelease: false
|
105
73
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -107,7 +75,7 @@ dependencies:
|
|
107
75
|
requirements:
|
108
76
|
- - ! '>='
|
109
77
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
78
|
+
version: 0.1.3
|
111
79
|
description: Develop declarative, featureful JSON RESTful-ish service controllers
|
112
80
|
to use with modern Javascript MVC frameworks like AngularJS, Ember, etc. with much
|
113
81
|
less code.
|
@@ -124,6 +92,7 @@ files:
|
|
124
92
|
- lib/restful_json/railtie.rb
|
125
93
|
- lib/restful_json/version.rb
|
126
94
|
- lib/restful_json.rb
|
95
|
+
- lib/twinturbo/application_permitter.rb
|
127
96
|
- lib/twinturbo/controller.rb
|
128
97
|
- Rakefile
|
129
98
|
- README.md
|