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 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
- Uses Adam Hawkin's [permitter][permitter] code which uses [strong_parameters][strong_parameters] and encourages use of [active_model_serializers][active_model_serializers].
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
- You need to setup [cancan][cancan]. Here are the basics:
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
- ### Responders
69
+ #### Strong Parameters
70
+
71
+ To use [strong_parameters][strong_parameters], in your Gemfile, add:
45
72
 
46
- If you plan to use responders outside of json with restful_json, you may want to formally install it, for flash messages, etc.:
73
+ gem 'strong_parameters', '~> 0.1.3'
47
74
 
48
- gem install responders
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
- Add this to your Gemfile and bundle install:
77
+ config.active_record.whitelist_attributes = false
51
78
 
52
- gem 'responders'
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
- And do:
81
+ Note that restful_json automatically includes `ActiveModel::ForbiddenAttributesProtection` on all models, as is done in Rails 4.
55
82
 
56
- rails generate responders:install
83
+ #### JSON Response Generators
57
84
 
58
- ### Strong Parameters
85
+ ##### ActiveModel Serializers
59
86
 
60
- If you want to now disable the default whitelisting that occurs in later versions of Rails, change the config.active_record.whitelist_attributes property in your `config/application.rb`:
87
+ If you want to use [ActiveModel Serializers][[active_model_serializers], use:
61
88
 
62
- config.active_record.whitelist_attributes = false
89
+ gem 'active_model_serializers'
63
90
 
64
- 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.
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
- Note that restful_json automatically includes `ActiveModel::ForbiddenAttributesProtection` on all models as will be done in Rails 4.
111
+ gem 'jbuilder'
67
112
 
68
- ### Configuration
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
- self.can_filter_by_default_using = [:eq] # default for :using in can_filter_by
78
- self.debug = false # to output debugging info during request handling
79
- self.filter_split = ',' # delimiter for values in request parameter values
80
- self.formats = :json, :html # 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.
81
- self.number_of_records_in_a_page = 15 # default number of records to return if using the page request function
82
- self.predicate_prefix = '!' # delimiter for ARel predicate in the request parameter name
83
- self.return_resource = false # if true, will render resource and HTTP 201 for post/create or resource and HTTP 200 for put/update
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
- #### Advanced Configuration
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
- In the controller, you can set a variety of class attributes with `self.something = ...` in the body of your controller to set model class, variable names, messages, etc. Take a look at the class_attribute definitions in `lib/restful_json/controller.rb`.
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` you have a configurable generic Rails 3.1+ controller that does the index, show, create, and update and other custom actions easily for you.
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
- class FoobarsController < ApplicationController
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
- can_filter_by :foo_id # implies using: [:eq] because RestfulJson.can_filter_by_default_using = [:eq]
101
- can_filter_by :foo_date, :bar_date, using: [:lt, :eq, :gt], with_default: Time.now # can specify multiple predicates and optionally a default value
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
- respond_to :json, :html # specify if you want more than :json. It dynamically sets model variables with the right names, e.g. @foobar and @foobars.
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
- `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. For a full list of available ones do:
241
+ #### Default Filtering by Attribute(s)
112
242
 
113
- rails c
114
- Arel::Predications.public_instance_methods.sort
243
+ First, declare in the controller:
115
244
 
116
- at time of writing these were:
245
+ can_filter_by :foo_id
117
246
 
118
- [: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]
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
- `supports_functions` lets you do other [ARel][arel] functions. `:uniq`, `:skip`, `:take`, and `:count`.
249
+ http://localhost:3000/foobars?foo_id=1
121
250
 
122
- `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.
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
- And `can_filter_by` can also specify a `:through` to provide an easy way to inner join through a bunch 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, e.g. the two following are equivalent:
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
- #### Default Filter by Attribute(s)
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
- First, declare in the controller:
261
+ can_filter_by :a_request_param_name, through: [:some_assoc, :some_attr]
132
262
 
133
- can_filter_by :foo_id
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
- 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':
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
- http://localhost:3000/foobars?foo_id=1
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
- #### Unique (DISTINCT)
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 active_model_serializer 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:
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
- #### Count
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
- #### Paging
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
- ##### Skip and Take (OFFSET and LIMIT)
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
@@ -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
- respond_with @value
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
- respond_with @value
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
- respond_with @value
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
- if RestfulJson.return_resource
302
- respond_with(@value) do |format|
303
- format.json do
304
- if @value.errors.empty?
305
- render json: @value, status: :created
306
- else
307
- render json: {errors: @value.errors}, status: :unprocessable_entity
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
- if RestfulJson.return_resource
324
- respond_with(@value) do |format|
325
- format.json do
326
- if @value.errors.empty?
327
- render json: @value, status: :ok
328
- else
329
- render json: {errors: @value.errors}, status: :unprocessable_entity
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
- respond_with @value
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
@@ -1,3 +1,3 @@
1
1
  module RestfulJson
2
- VERSION = '3.0.1'
2
+ VERSION = '3.1.0'
3
3
  end
@@ -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.1
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-19 00:00:00.000000000 Z
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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