restful_json 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,299 @@
1
+ restful_json
2
+ =====
3
+
4
+ Develop declarative, featureful JSON RESTful-ish service controllers to use with modern Javascript MVC frameworks like AngularJS, Ember, etc. with much less code.
5
+
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
+
8
+ Uses Adam Hawkin's [permitter][permitter] code which uses [strong_parameters][strong_parameters] and encourages use of [active_model_serializers][active_model_serializers].
9
+
10
+ ### Installation
11
+
12
+ In your Rails app's `Gemfile`, add:
13
+
14
+ gem 'restful_json'
15
+
16
+ and:
17
+
18
+ bundle install
19
+
20
+ You need to setup [cancan][cancan]. Here are the basics:
21
+
22
+ In your `app/controllers/application_controller.rb` or in your service controllers, make sure `current_user` is set:
23
+
24
+ class ApplicationController < ActionController::Base
25
+ protect_from_forgery
26
+
27
+ def current_user
28
+ User.new
29
+ end
30
+ end
31
+
32
+ You could do that better by setting up some real authentication with [Authlogic][authlogic], [Devise][devise], or whatever cancan will support.
33
+
34
+ In `app/models/ability.rb`, setup a basic cancan ability. Just for testing we'll allow everything:
35
+
36
+ class Ability
37
+ include CanCan::Ability
38
+
39
+ def initialize(user)
40
+ can :manage, :all
41
+ end
42
+ end
43
+
44
+ ### Responders
45
+
46
+ If you plan to use responders outside of json with restful_json, you may want to formally install it, for flash messages, etc.:
47
+
48
+ gem install responders
49
+
50
+ Add this to your Gemfile and bundle install:
51
+
52
+ gem 'responders'
53
+
54
+ And do:
55
+
56
+ rails generate responders:install
57
+
58
+ ### Strong Parameters
59
+
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`:
61
+
62
+ config.active_record.whitelist_attributes = false
63
+
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.
65
+
66
+ Note that restful_json automatically includes `ActiveModel::ForbiddenAttributesProtection` on all models as will be done in Rails 4.
67
+
68
+ ### Configuration
69
+
70
+ At the bottom of `config/environment.rb`, you can set config items one at a time like:
71
+
72
+ RestfulJson.debug = true
73
+
74
+ or in bulk like:
75
+
76
+ 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
84
+ end
85
+
86
+ #### Advanced Configuration
87
+
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`.
89
+
90
+ ### Usage
91
+
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.
93
+
94
+ Everything is well-declared and concise:
95
+
96
+ class FoobarsController < ApplicationController
97
+ acts_as_restful_json
98
+ query_for :index, is: ->(t,q) {q.joins(:apples, :pears).where(apples: {color: 'green'}).where(pears: {color: 'green'})}
99
+ # 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
102
+ 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})}
103
+ can_filter_by :and_another, through: [:some_attribute_on_this_model]
104
+ can_filter_by :one_more, through: [:some_association, :some_attribute_on_some_association_model]
105
+ can_filter_by :and_one_more, through: [:my_assoc, :my_assocs_assoc, :my_assocs_assocs_assoc, :an_attribute_on_my_assocs_assocs_assoc]
106
+ supports_functions :count, :uniq, :take, :skip, :page, :page_count
107
+ 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.
109
+ end
110
+
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:
112
+
113
+ rails c
114
+ Arel::Predications.public_instance_methods.sort
115
+
116
+ at time of writing these were:
117
+
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]
119
+
120
+ `supports_functions` lets you do other [ARel][arel] functions. `:uniq`, `:skip`, `:take`, and `:count`.
121
+
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.
123
+
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:
125
+
126
+ 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
+
129
+ #### Default Filter by Attribute(s)
130
+
131
+ First, declare in the controller:
132
+
133
+ can_filter_by :foo_id
134
+
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':
136
+
137
+ http://localhost:3000/foobars?foo_id=1
138
+
139
+ #### Other Filters by Attribute(s)
140
+
141
+ First, declare in the controller:
142
+
143
+ can_filter_by :seen_on, using: [:gteq, :eq_any]
144
+
145
+ Get Foobars with seen_on of 2012-08-08 or later using the [ARel][arel] gteq predicate splitting the request param on `predicate_prefix` (configurable), you'd use:
146
+
147
+ http://localhost:3000/foobars?seen_on!gteq=2012-08-08
148
+
149
+ Multiple values are separated by `filter_split` (configurable):
150
+
151
+ http://localhost:3000/foobars?seen_on!eq_any=2012-08-08,2012-09-09
152
+
153
+ #### Unique (DISTINCT)
154
+
155
+ First, declare in the controller:
156
+
157
+ supports_functions :uniq
158
+
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:
160
+
161
+ http://localhost:3000/foobars?uniq=
162
+
163
+ #### Count
164
+
165
+ First, declare in the controller:
166
+
167
+ supports_functions :count
168
+
169
+ This is another filter that can be used with the others, but instead of returning the json objects, it returns their count, which is useful for paging to determine how many results you can page through:
170
+
171
+ http://localhost:3000/foobars?count=
172
+
173
+ #### Paging
174
+
175
+ In controller make sure these are included:
176
+
177
+ supports_functions :page, :page_count
178
+
179
+ To get the first page of results:
180
+
181
+ http://localhost:3000/foobars?page=1
182
+
183
+ To get the second page of results:
184
+
185
+ http://localhost:3000/foobars?page=2
186
+
187
+ To get the total number of pages of results:
188
+
189
+ http://localhost:3000/foobars?page_count=
190
+
191
+ To set page size at application level:
192
+
193
+ RestfulJson.number_of_records_in_a_page = 15
194
+
195
+ To set page size at controller level:
196
+
197
+ self.number_of_records_in_a_page = 15
198
+
199
+ For a better idea of how this works on the backend, look at [ARel][arel]'s skip and take, or see Variable Paging.
200
+
201
+ ##### Skip and Take (OFFSET and LIMIT)
202
+
203
+ In controller make sure these are included:
204
+
205
+ supports_functions :skip, :take
206
+
207
+ To skip rows returned, use 'skip'. It is called take, because skip is the [ARel][arel] equivalent of SQL OFFSET:
208
+
209
+ http://localhost:3000/foobars?skip=5
210
+
211
+ To limit the number of rows returned, use 'take'. It is called take, because take is the [ARel][arel] equivalent of SQL LIMIT:
212
+
213
+ http://localhost:3000/foobars.json?take=5
214
+
215
+ ##### Variable Paging
216
+
217
+ Combine skip and take for manual completely customized paging.
218
+
219
+ Get the first page of 15 results:
220
+
221
+ http://localhost:3000/foobars?take=15
222
+
223
+ Second page of 15 results:
224
+
225
+ http://localhost:3000/foobars?skip=15&take=15
226
+
227
+ Third page of 15 results:
228
+
229
+ http://localhost:3000/foobars?skip=30&take=15
230
+
231
+ First page of 30 results:
232
+
233
+ http://localhost:3000/foobars?take=30
234
+
235
+ Second page of 30 results:
236
+
237
+ http://localhost:3000/foobars?skip=30&take=30
238
+
239
+ Third page of 30 results:
240
+
241
+ http://localhost:3000/foobars?skip=60&take=30
242
+
243
+ ##### Custom Queries
244
+
245
+ To filter the list where the status_code attribute is 'green':
246
+
247
+ # t is self.model_class.arel_table and q is self.model_class.scoped
248
+ query_for :index, is: lambda {|t,q| q.where(:status_code => 'green')}
249
+
250
+ or use the `->` Ruby 1.9 lambda stab operator. You can also filter out items that have associations that don't have a certain attribute value (or anything else you can think up with [ARel][arel]/[ActiveRecord relations][ar]). To filter the list where the object's apples and pears associations are green:
251
+
252
+ # t is self.model_class.arel_table and q is self.model_class.scoped
253
+ # note: must be no space between -> and parenthesis
254
+ query_for :index, is: ->(t,q) {
255
+ q.joins(:apples, :pears)
256
+ .where(apples: {color: 'green'})
257
+ .where(pears: {color: 'green'})
258
+ }
259
+
260
+ ##### Custom Actions
261
+
262
+ `query_for` also will `alias_method (some action), :index` anything other than `:index`, so you can easily create custom non-RESTful action methods:
263
+
264
+ # t is self.model_class.arel_table and q is self.model_class.scoped
265
+ # note: must be no space between -> and parenthesis in lambda syntax!
266
+ query_for :some_action, is: ->(t,q) {q.where(:status_code => 'green')}
267
+
268
+ 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
+
270
+ ### Routing
271
+
272
+ Respects regular and nested Rails resourceful routing and controller namespacing, e.g. in `config/routes.rb`:
273
+
274
+ MyAwesomeApp::Application.routes.draw do
275
+ namespace :my_service_controller_module do
276
+ resources :foobars
277
+ # why use nested if you only want to provide ways of querying via path
278
+ match 'bars/:bar_id/foobars(.:format)' => 'foobars#index'
279
+ end
280
+ end
281
+
282
+ ### Contributors
283
+
284
+ * Gary Weaver (https://github.com/garysweaver)
285
+ * Tommy Odom (https://github.com/tpodom)
286
+
287
+ ### License
288
+
289
+ Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
290
+
291
+ [permitter]: http://broadcastingadam.com/2012/07/parameter_authorization_in_rails_apis/
292
+ [cancan]: https://github.com/ryanb/cancan
293
+ [strong_parameters]: https://github.com/rails/strong_parameters
294
+ [active_model_serializers]: https://github.com/josevalim/active_model_serializers
295
+ [authlogic]: https://github.com/binarylogic/authlogic
296
+ [devise]: https://github.com/plataformatec/devise
297
+ [arel]: https://github.com/rails/arel
298
+ [ar]: http://api.rubyonrails.org/classes/ActiveRecord/Relation.html
299
+ [lic]: http://github.com/rubyservices/restful_json/blob/master/LICENSE
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new('spec')
3
+ task :default => :spec
@@ -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
@@ -0,0 +1,27 @@
1
+ module RestfulJson
2
+ CONTROLLER_OPTIONS = [
3
+ :can_filter_by_default_using,
4
+ :debug,
5
+ :filter_split,
6
+ :formats,
7
+ :number_of_records_in_a_page,
8
+ :predicate_prefix,
9
+ :return_resource
10
+ ]
11
+
12
+ class << self
13
+ CONTROLLER_OPTIONS.each{|o|attr_accessor o}
14
+ alias_method :debug?, :debug
15
+ def configure(&blk); class_eval(&blk); end
16
+ end
17
+ end
18
+
19
+ RestfulJson.configure do
20
+ self.can_filter_by_default_using = [:eq]
21
+ self.debug = false
22
+ self.filter_split = ','
23
+ self.formats = :json, :html
24
+ self.number_of_records_in_a_page = 15
25
+ self.predicate_prefix = '!'
26
+ self.return_resource = false
27
+ end
@@ -0,0 +1,346 @@
1
+ require 'restful_json/config'
2
+ require 'twinturbo/controller'
3
+ require 'active_model_serializers'
4
+ require 'strong_parameters'
5
+ require 'cancan'
6
+
7
+ module RestfulJson
8
+ module Controller
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ def acts_as_restful_json(options = {})
13
+ include ::ActionController::Serialization
14
+ include ::ActionController::StrongParameters
15
+ include ::CanCan::ControllerAdditions
16
+ include ::TwinTurbo::Controller
17
+ include ActsAsRestfulJson
18
+ end
19
+ end
20
+
21
+ module ActsAsRestfulJson
22
+ extend ActiveSupport::Concern
23
+
24
+ NILS = ['NULL','null','nil']
25
+
26
+ included do
27
+ # this can be overriden in the controller via defining respond_to
28
+ formats = RestfulJson.formats || Mime::EXTENSION_LOOKUP.keys.collect{|m|m.to_sym}
29
+ respond_to *formats
30
+
31
+ # create class attributes for each controller option and set the value to the value in the app configuration
32
+ class_attribute :model_class, instance_writer: true
33
+ class_attribute :model_singular_name, instance_writer: true
34
+ class_attribute :model_plural_name, instance_writer: true
35
+ class_attribute :param_to_attr_and_arel_predicate, instance_writer: true
36
+ class_attribute :supported_functions, instance_writer: true
37
+ class_attribute :ordered_by, instance_writer: true
38
+ class_attribute :action_to_query, instance_writer: true
39
+ class_attribute :param_to_query, instance_writer: true
40
+ class_attribute :param_to_through, instance_writer: true
41
+
42
+ # use values from config
43
+ # debug uses RestfulJson.debug? because until this is done no local debug class attribute exists to check
44
+ RestfulJson::CONTROLLER_OPTIONS.each do |key|
45
+ class_attribute key, instance_writer: true
46
+ self.send("#{key}=".to_sym, RestfulJson.send(key))
47
+ end
48
+
49
+ self.param_to_attr_and_arel_predicate ||= {}
50
+ self.supported_functions ||= []
51
+ self.ordered_by ||= []
52
+ self.action_to_query ||= {}
53
+ self.param_to_query ||= {}
54
+ self.param_to_through ||= {}
55
+ end
56
+
57
+ module ClassMethods
58
+ # A whitelist of filters and definition of filter options related to request parameters.
59
+ #
60
+ # If no options are provided or the :using option is provided, defines attributes that are queryable through the operation(s) already defined in can_filter_by_default_using, or can specify attributes:
61
+ # can_filter_by :attr_name_1, :attr_name_2 # implied using: [eq] if RestfulJson.can_filter_by_default_using = [:eq]
62
+ # can_filter_by :attr_name_1, :attr_name_2, using: [:eq, :not_eq]
63
+ #
64
+ # When :with_query is specified, it will call a supplied lambda. e.g. t is self.model_class.arel_table, q is self.model_class.scoped, and p is params[:my_param_name]:
65
+ # can_filter_by :my_param_name, with_query: ->(t,q,p) {...}
66
+ #
67
+ # When :through is specified, it will take the array supplied to through as 0 to many model names following by an attribute name. It will follow through
68
+ # each association until it gets to the attribute to filter by that via ARel joins, e.g. if the model Foobar has an association to :foo, and on the Foo model there is an assocation
69
+ # to :bar, and you want to filter by bar.name (foobar.foo.bar.name):
70
+ # can_filter_by :my_param_name, through: [:foo, :bar, :name]
71
+ def can_filter_by(*args)
72
+ options = args.extract_options!
73
+
74
+ # :using is the default action if no options are present
75
+ if options[:using] || options.size == 0
76
+ predicates = Array.wrap(options[:using] || self.can_filter_by_default_using)
77
+ predicates.each do |predicate|
78
+ predicate_sym = predicate.to_sym
79
+ args.each do |attr|
80
+ attr_sym = attr.to_sym
81
+ self.param_to_attr_and_arel_predicate[attr_sym] = [attr_sym, :eq, options] if predicate_sym == :eq
82
+ self.param_to_attr_and_arel_predicate["#{attr}#{self.predicate_prefix}#{predicate}".to_sym] = [attr_sym, predicate_sym, options]
83
+ end
84
+ end
85
+ end
86
+
87
+ if options[:with_query]
88
+ args.each do |with_query_key|
89
+ self.param_to_query[with_query_key.to_sym] = options[:with_query]
90
+ end
91
+ end
92
+
93
+ if options[:through]
94
+ args.each do |through_key|
95
+ self.param_to_through[through_key.to_sym] = options[:through]
96
+ end
97
+ end
98
+ end
99
+
100
+ # Can specify additional functions in the index, e.g.
101
+ # supports_functions :skip, :uniq, :take, :count
102
+ def supports_functions(*args)
103
+ options = args.extract_options! # overkill, sorry
104
+ self.supported_functions += args
105
+ end
106
+
107
+ # Specify a custom query. If action specified does not have a method, it will alias_method index to create a new action method with that query.
108
+ #
109
+ # t is self.model_class.arel_table and q is self.model_class.scoped, e.g.
110
+ # query_for :index, is: -> {|t,q| q.where(:status_code => 'green')}
111
+ def query_for(*args)
112
+ options = args.extract_options!
113
+ # TODO: support custom actions to be automaticaly defined
114
+ args.each do |an_action|
115
+ if options[:is]
116
+ self.action_to_query[an_action.to_s] = options[:is]
117
+ else
118
+ raise "#{self.class.name} must supply an :is option with query_for #{an_action.inspect}"
119
+ end
120
+ unless an_action.to_sym == :index
121
+ alias_method an_action.to_sym, :index
122
+ end
123
+ end
124
+ end
125
+
126
+ # Takes an string, symbol, array, hash to indicate order. If not a hash, assumes is ascending. Is cumulative and order defines order of sorting, e.g:
127
+ # #would order by foo_color attribute ascending
128
+ # order_by :foo_color
129
+ # or
130
+ # order_by {:foo_date => :asc}, :foo_color, 'foo_name', {:bar_date => :desc}
131
+ def order_by(args)
132
+ if args.is_a?(Array)
133
+ self.ordered_by += args
134
+ elsif args.is_a?(Hash)
135
+ self.ordered_by.merge!(args)
136
+ else
137
+ raise ArgumentError.new("order_by takes a hash or array of hashes")
138
+ end
139
+ end
140
+ end
141
+
142
+ def initialize
143
+ super
144
+
145
+ # if not set, use controller classname
146
+ qualified_controller_name = self.class.name.chomp('Controller')
147
+ @model_class = self.model_class || qualified_controller_name.split('::').last.singularize.constantize
148
+
149
+ raise "#{self.class.name} failed to initialize. self.model_class was nil in #{self} which shouldn't happen!" if @model_class.nil?
150
+ raise "#{self.class.name} assumes that #{self.model_class} extends ActiveRecord::Base, but it didn't. Please fix, or remove this constraint." unless @model_class.ancestors.include?(ActiveRecord::Base)
151
+
152
+ @model_singular_name = self.model_singular_name || self.model_class.name.underscore
153
+ @model_plural_name = self.model_plural_name || @model_singular_name.pluralize
154
+ @model_at_plural_name_sym = "@#{@model_plural_name}".to_sym
155
+ @model_at_singular_name_sym = "@#{@model_singular_name}".to_sym
156
+ underscored_modules_and_underscored_plural_model_name = qualified_controller_name.gsub('::','_').underscore
157
+
158
+ # This workaround for models that are in a different module than the model only works if the controller's base part of the unqualified name in the plural model name.
159
+ # If the model name is different than the controller name, you will need to define methods to return the right urls.
160
+ class_eval "def #{@model_plural_name}_url;#{underscored_modules_and_underscored_plural_model_name}_url;end;def #{@model_singular_name}_url(record);#{underscored_modules_and_underscored_plural_model_name.singularize}_url(record);end"
161
+ end
162
+
163
+ def convert_request_param_value_for_filtering(attr_sym, value)
164
+ value && NILS.include?(value) ? nil : value
165
+ end
166
+
167
+ # The controller's index (list) method to list resources.
168
+ #
169
+ # Note: this method be alias_method'd by query_for, so it is more than just index.
170
+ def index
171
+ t = @model_class.arel_table
172
+ value = @model_class.scoped # returns ActiveRecord::Relation equivalent to select with no where clause
173
+ custom_query = self.action_to_query[params[:action].to_s]
174
+ if custom_query
175
+ value = custom_query.call(t, value)
176
+ end
177
+
178
+ self.param_to_query.each do |param_name, param_query|
179
+ if params[param_name]
180
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
181
+ value = param_query.call(t, value, params[param_name].to_s)
182
+ end
183
+ end
184
+
185
+ self.param_to_through.each do |param_name, through_array|
186
+ if params[param_name]
187
+ # build query
188
+ # e.g. SomeModel.scoped.joins({:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}).where(sub_sub_sub_assoc_model_table_name: {column_name: value})
189
+ last_model_class = @model_class
190
+ joins = nil # {:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}
191
+ through_array.each do |association_or_attribute|
192
+ if association_or_attribute == through_array.last
193
+ # must convert param value to string before possibly using with ARel because of CVE-2013-1854, fixed in: 3.2.13 and 3.1.12
194
+ # https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/jgJ4cjjS8FE/BGbHRxnDRTIJ
195
+ value = value.joins(joins).where(last_model_class.table_name.to_sym => {association_or_attribute => params[param_name].to_s})
196
+ else
197
+ found_classes = last_model_class.reflections.collect {|association_name, reflection| reflection.class_name.constantize if association_name.to_sym == association_or_attribute}.compact
198
+ if found_classes.size > 0
199
+ last_model_class = found_classes[0]
200
+ else
201
+ # bad can_filter_by :through found at runtime
202
+ raise "Association #{association_or_attribute.inspect} not found on #{last_model_class}."
203
+ end
204
+
205
+ if joins.nil?
206
+ joins = association_or_attribute
207
+ else
208
+ joins = {association_or_attribute => joins}
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ self.param_to_attr_and_arel_predicate.keys.each do |param_name|
216
+ options = param_to_attr_and_arel_predicate[param_name][2]
217
+ param = params[param_name] || options[:with_default]
218
+ if param.present? && param_to_attr_and_arel_predicate[param_name]
219
+ attr_sym = param_to_attr_and_arel_predicate[param_name][0]
220
+ predicate_sym = param_to_attr_and_arel_predicate[param_name][1]
221
+ if predicate_sym == :eq
222
+ value = value.where(attr_sym => convert_request_param_value_for_filtering(attr_sym, param))
223
+ else
224
+ one_or_more_param = param.split(self.filter_split).collect{|v|convert_request_param_value_for_filtering(attr_sym, v)}
225
+ value = value.where(t[attr_sym].try(predicate_sym, one_or_more_param))
226
+ end
227
+ end
228
+ end
229
+
230
+ if params[:page] && self.supported_functions.include?(:page)
231
+ page = params[:page].to_i
232
+ page = 1 if page < 1 # to avoid people using this as a way to get all records unpaged, as that probably isn't the intent?
233
+ #TODO: to_s is hack to avoid it becoming an Arel::SelectManager for some reason which not sure what to do with
234
+ value = value.skip((self.number_of_records_in_a_page * (page - 1)).to_s)
235
+ value = value.take((self.number_of_records_in_a_page).to_s)
236
+ end
237
+
238
+ if params[:skip] && self.supported_functions.include?(:skip)
239
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
240
+ value = value.skip(params[:skip].to_s)
241
+ end
242
+
243
+ if params[:take] && self.supported_functions.include?(:take)
244
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
245
+ value = value.take(params[:take].to_s)
246
+ end
247
+
248
+ if params[:uniq] && self.supported_functions.include?(:uniq)
249
+ value = value.uniq
250
+ end
251
+
252
+ # these must happen at the end and are independent
253
+ if params[:count] && self.supported_functions.include?(:count)
254
+ value = value.count.to_i
255
+ elsif params[:page_count] && self.supported_functions.include?(:page_count)
256
+ count_value = value.count.to_i # this executes the query so nothing else can be done in AREL
257
+ value = (count_value / self.number_of_records_in_a_page) + (count_value % self.number_of_records_in_a_page ? 1 : 0)
258
+ else
259
+ self.ordered_by.each do |attr_to_direction|
260
+ # this looks nasty, but makes no sense to iterate keys if only single of each
261
+ value = value.order(t[attr_to_direction.keys[0]].call(attr_to_direction.values[0]))
262
+ end
263
+ value = value.to_a
264
+ end
265
+
266
+ @value = value
267
+ instance_variable_set(@model_at_plural_name_sym, @value)
268
+ respond_with @value
269
+ end
270
+
271
+ # The controller's show (get) method to return a resource.
272
+ def show
273
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
274
+ @value = @model_class.find(params[:id].to_s)
275
+ instance_variable_set(@model_at_singular_name_sym, @value)
276
+ respond_with @value
277
+ end
278
+
279
+ # The controller's new method (e.g. used for new record in html format).
280
+ def new
281
+ @value = @model_class.new
282
+ respond_with @value
283
+ end
284
+
285
+ # The controller's edit method (e.g. used for edit record in html format).
286
+ def edit
287
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
288
+ @value = @model_class.find(params[:id].to_s)
289
+ instance_variable_set(@model_at_singular_name_sym, @value)
290
+ end
291
+
292
+ # The controller's create (post) method to create a resource.
293
+ def create
294
+ authorize! :create, @model_class
295
+ @value = @model_class.new(permitted_params)
296
+ @value.save
297
+ instance_variable_set(@model_at_singular_name_sym, @value)
298
+ if RestfulJson.return_resource
299
+ respond_with(@value) do |format|
300
+ format.json do
301
+ if @value.errors.empty?
302
+ render json: @value, status: :created
303
+ else
304
+ render json: {errors: @value.errors}, status: :unprocessable_entity
305
+ end
306
+ end
307
+ end
308
+ else
309
+ respond_with @value
310
+ end
311
+ end
312
+
313
+ # The controller's update (put) method to update a resource.
314
+ def update
315
+ authorize! :update, @model_class
316
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
317
+ @value = @model_class.find(params[:id].to_s)
318
+ @value.update_attributes(permitted_params)
319
+ instance_variable_set(@model_at_singular_name_sym, @value)
320
+ if RestfulJson.return_resource
321
+ respond_with(@value) do |format|
322
+ format.json do
323
+ if @value.errors.empty?
324
+ render json: @value, status: :ok
325
+ else
326
+ render json: {errors: @value.errors}, status: :unprocessable_entity
327
+ end
328
+ end
329
+ end
330
+ else
331
+ respond_with @value
332
+ end
333
+ end
334
+
335
+ # The controller's destroy (delete) method to destroy a resource.
336
+ def destroy
337
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
338
+ @value = @model_class.find(params[:id].to_s)
339
+ @value.destroy
340
+ instance_variable_set(@model_at_singular_name_sym, @value)
341
+ respond_with @value
342
+ end
343
+
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,13 @@
1
+ require 'cancan'
2
+
3
+ module RestfulJson
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+ included do
7
+ # strong parameters
8
+ include ::ActiveModel::ForbiddenAttributesProtection
9
+ # cancan, depended on by twinturbo's permitters
10
+ include ::CanCan::ModelAdditions
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ require 'restful_json'
2
+
3
+ module RestfulJson
4
+ class Railtie < Rails::Railtie
5
+ initializer "restful_json.action_controller" do
6
+ ActiveSupport.on_load(:action_controller) do
7
+ puts "Extending #{self} with RestfulJson::Controller" if RestfulJson.debug?
8
+ include RestfulJson::Controller
9
+ end
10
+ end
11
+
12
+ initializer "restful_json.active_record" do
13
+ ActiveSupport.on_load(:active_record) do
14
+ puts "Extending #{self} with RestfulJson::Model" if RestfulJson.debug?
15
+ include RestfulJson::Model
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module RestfulJson
2
+ VERSION = '3.0.0'
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'restful_json/version'
2
+ require 'restful_json/config'
3
+ require 'application_permitter'
4
+ require 'twinturbo/controller'
5
+ require 'restful_json/model'
6
+ require 'restful_json/controller'
7
+ require 'restful_json/railtie' if defined?(Rails)
@@ -0,0 +1,35 @@
1
+ module TwinTurbo
2
+ module Controller
3
+ # Instance Methods:
4
+
5
+ # the following methods are from Adam Hawkins's post:
6
+ # http://www.broadcastingadam.com/2012/07/parameter_authorization_in_rails_apis/
7
+ # with modification to only try to call permitted params if is a permitter
8
+
9
+ def permitted_params
10
+ # if you send invalid content, it will return an HTTP 20x for a put and a 422 for a post, instead of a 500 for both.
11
+ @permitted_params ||= safe_permitted_params
12
+ end
13
+
14
+ def permitter
15
+ return unless permitter_class
16
+
17
+ @permitter ||= permitter_class.new params, current_user, current_ability
18
+ end
19
+
20
+ def permitter_class
21
+ begin
22
+ "#{self.class.to_s.match(/(.*?::)?(?<controller_name>.+)Controller/)[:controller_name].singularize}Permitter".constantize
23
+ rescue NameError
24
+ nil
25
+ end
26
+ end
27
+
28
+ def safe_permitted_params
29
+ begin
30
+ permitter.send(:permitted_params)
31
+ rescue
32
+ end
33
+ end
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: restful_json
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Gary S. Weaver
9
+ - Tommy Odom
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-03-19 00:00:00.000000000 Z
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
+ - !ruby/object:Gem::Dependency
48
+ name: actionpack
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: activerecord
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: cancan
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: strong_parameters
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Develop declarative, featureful JSON RESTful-ish service controllers
112
+ to use with modern Javascript MVC frameworks like AngularJS, Ember, etc. with much
113
+ less code.
114
+ email:
115
+ - garysweaver@gmail.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - lib/application_permitter.rb
121
+ - lib/restful_json/config.rb
122
+ - lib/restful_json/controller.rb
123
+ - lib/restful_json/model.rb
124
+ - lib/restful_json/railtie.rb
125
+ - lib/restful_json/version.rb
126
+ - lib/restful_json.rb
127
+ - lib/twinturbo/controller.rb
128
+ - Rakefile
129
+ - README.md
130
+ homepage: https://github.com/rubyservices/restful_json
131
+ licenses:
132
+ - MIT
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ! '>='
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubyforge_project:
151
+ rubygems_version: 1.8.25
152
+ signing_key:
153
+ specification_version: 3
154
+ summary: RESTful JSON controllers using Rails 3.1+, Rails 4+.
155
+ test_files: []