restful_json 3.2.2 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,5 +1,18 @@
1
- require 'rubygems'
2
1
  require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
3
  require 'appraisal'
4
+
4
5
  require 'rspec/core/rake_task'
5
- RSpec::Core::RakeTask.new('spec')
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default do |t|
9
+ if ENV['BUNDLE_GEMFILE'] =~ /gemfiles/
10
+ exec 'rake spec'
11
+ else
12
+ Rake::Task['appraise'].execute
13
+ end
14
+ end
15
+
16
+ task :appraise => ['appraisal:install'] do |t|
17
+ exec 'rake appraisal'
18
+ end
data/lib/restful_json.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  require 'restful_json/version'
2
2
  require 'restful_json/config'
3
- #if defined?(::Rails)
4
- if defined?(::ActionController::StrongParameters) && defined?(::CanCan::ModelAdditions)
5
- require 'application_permitter'
6
- require 'twinturbo/controller'
7
- end
8
- require 'restful_json/model'
9
- require 'restful_json/controller'
10
- require 'restful_json/railtie'
11
- #end
3
+ require 'twinturbo/application_permitter'
4
+ require 'twinturbo/controller'
5
+ require 'restful_json/base_controller'
6
+ require 'restful_json/controller'
7
+ require 'restful_json/default_controller'
8
+
9
+ ActiveSupport::Dependencies.autoload_paths << "#{Rails.root}/app/permitters" unless ActiveSupport::Dependencies.autoload_paths.include?("#{Rails.root}/app/permitters")
@@ -0,0 +1,13 @@
1
+ module RestfulJson
2
+ module BaseController
3
+ extend ::ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # <b>DEPRECATED:</b> Please use <tt>include RestfulJson::DefaultController</tt> instead.
7
+ def acts_as_restful_json
8
+ warn "[DEPRECATION] `acts_as_restful_json` is deprecated. Please use `include RestfulJson::DefaultController` or see documentation."
9
+ include ::RestfulJson::DefaultController
10
+ end
11
+ end
12
+ end
13
+ end
@@ -8,7 +8,8 @@ module RestfulJson
8
8
  :predicate_prefix,
9
9
  :return_resource,
10
10
  :render_enabled,
11
- :use_permitters
11
+ :use_permitters,
12
+ :avoid_respond_with
12
13
  ]
13
14
 
14
15
  class << self
@@ -28,4 +29,5 @@ RestfulJson.configure do
28
29
  self.return_resource = false
29
30
  self.render_enabled = true
30
31
  self.use_permitters = true
32
+ self.avoid_respond_with = true
31
33
  end
@@ -22,434 +22,387 @@ module RestfulJson
22
22
  module Controller
23
23
  extend ::ActiveSupport::Concern
24
24
 
25
- module ClassMethods
26
-
27
- def acts_as_restful_json(options = {})
28
- if defined?(::ActionController::Serialization)
29
- include ::ActionController::Serialization
30
- end
31
- if defined?(::ActionController::StrongParameters)
32
- include ::ActionController::StrongParameters
33
- end
34
- if defined?(::CanCan::ModelAdditions)
35
- include ::CanCan::ControllerAdditions
36
- end
37
- if defined?(::ActionController::StrongParameters) && defined?(::CanCan::ModelAdditions)
38
- include ::TwinTurbo::Controller
39
- end
40
- include ActsAsRestfulJson
25
+ NILS = ['NULL'.freeze,'null'.freeze,'nil'.freeze]
26
+ SINGLE_VALUE_ACTIONS = ['create'.freeze,'update'.freeze,'destroy'.freeze,'new'.freeze]
27
+
28
+ included do
29
+ # this can be overriden in the controller via defining respond_to
30
+ formats = RestfulJson.formats || ::Mime::EXTENSION_LOOKUP.keys.collect{|m|m.to_sym}
31
+ respond_to *formats
32
+
33
+ # create class attributes for each controller option and set the value to the value in the app configuration
34
+ class_attribute :model_class, instance_writer: true
35
+ class_attribute :model_singular_name, instance_writer: true
36
+ class_attribute :model_plural_name, instance_writer: true
37
+ class_attribute :param_to_attr_and_arel_predicate, instance_writer: true
38
+ class_attribute :supported_functions, instance_writer: true
39
+ class_attribute :ordered_by, instance_writer: true
40
+ class_attribute :action_to_query, instance_writer: true
41
+ class_attribute :param_to_query, instance_writer: true
42
+ class_attribute :param_to_through, instance_writer: true
43
+ class_attribute :action_to_serializer, instance_writer: true
44
+ class_attribute :action_to_serializer_for, instance_writer: true
45
+
46
+ # use values from config
47
+ # debug uses RestfulJson.debug? because until this is done no local debug class attribute exists to check
48
+ RestfulJson::CONTROLLER_OPTIONS.each do |key|
49
+ class_attribute key, instance_writer: true
50
+ self.send("#{key}=".to_sym, RestfulJson.send(key))
41
51
  end
42
52
 
53
+ self.param_to_attr_and_arel_predicate ||= {}
54
+ self.supported_functions ||= []
55
+ self.ordered_by ||= []
56
+ self.action_to_query ||= {}
57
+ self.param_to_query ||= {}
58
+ self.param_to_through ||= {}
59
+ self.action_to_serializer ||= {}
60
+ self.action_to_serializer_for ||= {}
43
61
  end
44
-
45
- module ActsAsRestfulJson
46
- extend ::ActiveSupport::Concern
47
-
48
- NILS = ['NULL','null','nil']
49
-
50
- included do
51
- # this can be overriden in the controller via defining respond_to
52
- formats = RestfulJson.formats || ::Mime::EXTENSION_LOOKUP.keys.collect{|m|m.to_sym}
53
- respond_to *formats
54
-
55
- # create class attributes for each controller option and set the value to the value in the app configuration
56
- class_attribute :model_class, instance_writer: true
57
- class_attribute :model_singular_name, instance_writer: true
58
- class_attribute :model_plural_name, instance_writer: true
59
- class_attribute :param_to_attr_and_arel_predicate, instance_writer: true
60
- class_attribute :supported_functions, instance_writer: true
61
- class_attribute :ordered_by, instance_writer: true
62
- class_attribute :action_to_query, instance_writer: true
63
- class_attribute :param_to_query, instance_writer: true
64
- class_attribute :param_to_through, instance_writer: true
65
- class_attribute :action_to_serializer, instance_writer: true
66
-
67
- # use values from config
68
- # debug uses RestfulJson.debug? because until this is done no local debug class attribute exists to check
69
- RestfulJson::CONTROLLER_OPTIONS.each do |key|
70
- class_attribute key, instance_writer: true
71
- self.send("#{key}=".to_sym, RestfulJson.send(key))
72
- end
73
62
 
74
- self.param_to_attr_and_arel_predicate ||= {}
75
- self.supported_functions ||= []
76
- self.ordered_by ||= []
77
- self.action_to_query ||= {}
78
- self.param_to_query ||= {}
79
- self.param_to_through ||= {}
80
- self.action_to_serializer ||= {}
81
- end
82
-
83
- module ClassMethods
84
-
85
- # A whitelist of filters and definition of filter options related to request parameters.
86
- #
87
- # 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:
88
- # can_filter_by :attr_name_1, :attr_name_2 # implied using: [eq] if RestfulJson.can_filter_by_default_using = [:eq]
89
- # can_filter_by :attr_name_1, :attr_name_2, using: [:eq, :not_eq]
90
- #
91
- # 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]:
92
- # can_filter_by :my_param_name, with_query: ->(t,q,p) {...}
93
- #
94
- # 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
95
- # 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
96
- # to :bar, and you want to filter by bar.name (foobar.foo.bar.name):
97
- # can_filter_by :my_param_name, through: [:foo, :bar, :name]
98
- def can_filter_by(*args)
99
- options = args.extract_options!
100
-
101
- # :using is the default action if no options are present
102
- if options[:using] || options.size == 0
103
- predicates = Array.wrap(options[:using] || self.can_filter_by_default_using)
104
- predicates.each do |predicate|
105
- predicate_sym = predicate.to_sym
106
- args.each do |attr|
107
- attr_sym = attr.to_sym
108
- self.param_to_attr_and_arel_predicate[attr_sym] = [attr_sym, :eq, options] if predicate_sym == :eq
109
- self.param_to_attr_and_arel_predicate["#{attr}#{self.predicate_prefix}#{predicate}".to_sym] = [attr_sym, predicate_sym, options]
110
- end
111
- end
112
- end
113
-
114
- if options[:with_query]
115
- args.each do |with_query_key|
116
- self.param_to_query[with_query_key.to_sym] = options[:with_query]
117
- end
118
- end
63
+ module ClassMethods
119
64
 
120
- if options[:through]
121
- args.each do |through_key|
122
- self.param_to_through[through_key.to_sym] = options[:through]
65
+ # A whitelist of filters and definition of filter options related to request parameters.
66
+ #
67
+ # 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:
68
+ # can_filter_by :attr_name_1, :attr_name_2 # implied using: [eq] if RestfulJson.can_filter_by_default_using = [:eq]
69
+ # can_filter_by :attr_name_1, :attr_name_2, using: [:eq, :not_eq]
70
+ #
71
+ # 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]:
72
+ # can_filter_by :my_param_name, with_query: ->(t,q,p) {...}
73
+ #
74
+ # 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
75
+ # 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
76
+ # to :bar, and you want to filter by bar.name (foobar.foo.bar.name):
77
+ # can_filter_by :my_param_name, through: [:foo, :bar, :name]
78
+ def can_filter_by(*args)
79
+ options = args.extract_options!
80
+
81
+ # :using is the default action if no options are present
82
+ if options[:using] || options.size == 0
83
+ predicates = Array.wrap(options[:using] || self.can_filter_by_default_using)
84
+ predicates.each do |predicate|
85
+ predicate_sym = predicate.to_sym
86
+ args.each do |attr|
87
+ attr_sym = attr.to_sym
88
+ self.param_to_attr_and_arel_predicate[attr_sym] = [attr_sym, :eq, options] if predicate_sym == :eq
89
+ self.param_to_attr_and_arel_predicate["#{attr}#{self.predicate_prefix}#{predicate}".to_sym] = [attr_sym, predicate_sym, options]
123
90
  end
124
91
  end
125
92
  end
126
93
 
127
- # Can specify additional functions in the index, e.g.
128
- # supports_functions :skip, :uniq, :take, :count
129
- def supports_functions(*args)
130
- options = args.extract_options! # overkill, sorry
131
- self.supported_functions += args
132
- end
133
-
134
- # 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.
135
- #
136
- # t is self.model_class.arel_table and q is self.model_class.scoped, e.g.
137
- # query_for :index, is: -> {|t,q| q.where(:status_code => 'green')}
138
- def query_for(*args)
139
- options = args.extract_options!
140
- # TODO: support custom actions to be automaticaly defined
141
- args.each do |an_action|
142
- if options[:is]
143
- self.action_to_query[an_action.to_s] = options[:is]
144
- else
145
- raise "#{self.class.name} must supply an :is option with query_for #{an_action.inspect}"
146
- end
147
- unless an_action.to_sym == :index
148
- alias_method an_action.to_sym, :index
149
- end
94
+ if options[:with_query]
95
+ args.each do |with_query_key|
96
+ self.param_to_query[with_query_key.to_sym] = options[:with_query]
150
97
  end
151
98
  end
152
99
 
153
- # 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:
154
- # #would order by foo_color attribute ascending
155
- # order_by :foo_color
156
- # or
157
- # order_by {:foo_date => :asc}, :foo_color, 'foo_name', {:bar_date => :desc}
158
- def order_by(args)
159
- self.ordered_by = (Array.wrap(self.ordered_by) + Array.wrap(args)).flatten.compact.collect {|item|item.is_a?(Hash) ? item : {item.to_sym => :asc}}
100
+ if options[:through]
101
+ args.each do |through_key|
102
+ self.param_to_through[through_key.to_sym] = options[:through]
103
+ end
160
104
  end
105
+ end
161
106
 
162
- # Associate a non-standard ActiveModel Serializer for one or more actions, e.g.
163
- # serialize_action :index, with: FoosSerializer
164
- # or
165
- # serialize_action :index, :some_custom_action, with: FoosSerializer
166
- def serialize_action(*args)
167
- options = args.extract_options!
168
- args.each do |an_action|
169
- if options[:with]
170
- self.action_to_serializer[an_action.to_s] = options[:with]
171
- else
172
- raise "#{self.class.name} must supply an :with option with serialize_action #{an_action.inspect}"
173
- end
107
+ # Can specify additional functions in the index, e.g.
108
+ # supports_functions :skip, :uniq, :take, :count
109
+ def supports_functions(*args)
110
+ args.extract_options! # remove hash from array- we're not using it yet
111
+ self.supported_functions += args
112
+ end
113
+
114
+ # 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.
115
+ #
116
+ # t is self.model_class.arel_table and q is self.model_class.scoped, e.g.
117
+ # query_for :index, is: -> {|t,q| q.where(:status_code => 'green')}
118
+ def query_for(*args)
119
+ options = args.extract_options!
120
+ # TODO: support custom actions to be automaticaly defined
121
+ args.each do |an_action|
122
+ if options[:is]
123
+ self.action_to_query[an_action.to_s] = options[:is]
124
+ else
125
+ raise "#{self.class.name} must supply an :is option with query_for #{an_action.inspect}"
126
+ end
127
+ unless an_action.to_sym == :index
128
+ alias_method an_action.to_sym, :index
174
129
  end
175
130
  end
176
131
  end
177
132
 
178
- # In initialize we:
179
- # * guess model name, if unspecified, from controller name
180
- # * define instance variables containing model name
181
- # * define the (model_plural_name)_url method, needed if controllers are not in the same module as the models
182
- # Note: if controller name is not based on model name *and* controller is in different module than model, you'll need to
183
- # redefine the appropriate method(s) to return urls if needed.
184
- def initialize
185
- super
186
-
187
- # if not set, use controller classname
188
- qualified_controller_name = self.class.name.chomp('Controller')
189
- @model_class = self.model_class || qualified_controller_name.split('::').last.singularize.constantize
190
-
191
- raise "#{self.class.name} failed to initialize. self.model_class was nil in #{self} which shouldn't happen!" if @model_class.nil?
192
- 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)
193
-
194
- @model_singular_name = self.model_singular_name || self.model_class.name.underscore
195
- @model_plural_name = self.model_plural_name || @model_singular_name.pluralize
196
- @model_at_plural_name_sym = "@#{@model_plural_name}".to_sym
197
- @model_at_singular_name_sym = "@#{@model_singular_name}".to_sym
198
-
199
- # next 3 are for vanilla strong_parameters
200
- @model_singular_name_params_sym = "#{@model_singular_name}_params".to_sym
201
- @create_model_singular_name_params_sym = "create_#{@model_singular_name}_params".to_sym
202
- @update_model_singular_name_params_sym = "update_#{@model_singular_name}_params".to_sym
203
-
204
- underscored_modules_and_underscored_plural_model_name = qualified_controller_name.gsub('::','_').underscore
205
-
206
- # This is a workaround for controllers 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.
207
- # If the model name is different than the controller name, you will need to define methods to return the right urls.
208
- 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"
133
+ # 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:
134
+ # #would order by foo_color attribute ascending
135
+ # order_by :foo_color
136
+ # or
137
+ # order_by {:foo_date => :asc}, :foo_color, 'foo_name', {:bar_date => :desc}
138
+ def order_by(args)
139
+ self.ordered_by = (Array.wrap(self.ordered_by) + Array.wrap(args)).flatten.compact.collect {|item|item.is_a?(Hash) ? item : {item.to_sym => :asc}}
209
140
  end
210
141
 
211
- def convert_request_param_value_for_filtering(attr_sym, value)
212
- value && NILS.include?(value) ? nil : value
142
+ # Associate a non-standard ActiveModel Serializer for one or more actions, e.g.
143
+ # serialize_action :index, with: FoosSerializer
144
+ # or
145
+ # serialize_action :index, :some_custom_action, with: FooSerializer
146
+ # The default functionality of each action is to use serialize for show, each, create, update, and destroy and serialize_each for index and
147
+ # any custom actions created with query_for. To override that, specify the :for option with value as :array or :each:
148
+ # serialize_action :index, :some_custom_action, with: FoosSerializer, for: :array
149
+ def serialize_action(*args)
150
+ options = args.extract_options!
151
+ args.each do |an_action|
152
+ if options[:with]
153
+ self.action_to_serializer[an_action.to_s] = options[:with]
154
+ self.action_to_serializer_for[an_action.to_s] = options[:for] if options[:for]
155
+ else
156
+ raise "#{self.class.name} must supply an :with option with serialize_action #{an_action.inspect}"
157
+ end
158
+ end
213
159
  end
160
+ end
214
161
 
215
- # The controller's index (list) method to list resources.
216
- #
217
- # Note: this method be alias_method'd by query_for, so it is more than just index.
218
- def index
219
- t = @model_class.arel_table
220
- value = @model_class.scoped # returns ActiveRecord::Relation equivalent to select with no where clause
221
- custom_query = self.action_to_query[params[:action].to_s]
222
- if custom_query
223
- value = custom_query.call(t, value)
224
- end
162
+ # In initialize we:
163
+ # * guess model name, if unspecified, from controller name
164
+ # * define instance variables containing model name
165
+ # * define the (model_plural_name)_url method, needed if controllers are not in the same module as the models
166
+ # Note: if controller name is not based on model name *and* controller is in different module than model, you'll need to
167
+ # redefine the appropriate method(s) to return urls if needed.
168
+ def initialize
169
+ super
170
+
171
+ # if not set, use controller classname
172
+ qualified_controller_name = self.class.name.chomp('Controller')
173
+ @model_class = self.model_class || qualified_controller_name.split('::').last.singularize.constantize
174
+
175
+ raise "#{self.class.name} failed to initialize. self.model_class was nil in #{self} which shouldn't happen!" if @model_class.nil?
176
+ 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)
177
+
178
+ @model_singular_name = self.model_singular_name || self.model_class.name.underscore
179
+ @model_plural_name = self.model_plural_name || @model_singular_name.pluralize
180
+ @model_at_plural_name_sym = "@#{@model_plural_name}".to_sym
181
+ @model_at_singular_name_sym = "@#{@model_singular_name}".to_sym
182
+
183
+ # next 3 are for vanilla strong_parameters
184
+ @model_singular_name_params_sym = "#{@model_singular_name}_params".to_sym
185
+ @create_model_singular_name_params_sym = "create_#{@model_singular_name}_params".to_sym
186
+ @update_model_singular_name_params_sym = "update_#{@model_singular_name}_params".to_sym
187
+
188
+ underscored_modules_and_underscored_plural_model_name = qualified_controller_name.gsub('::','_').underscore
189
+
190
+ # This is a workaround for controllers 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.
191
+ # If the model name is different than the controller name, you will need to define methods to return the right urls.
192
+ class_eval "def #{@model_plural_name}_url;#{underscored_modules_and_underscored_plural_model_name}_url;end" unless @model_plural_name == underscored_modules_and_underscored_plural_model_name
193
+ singularized_underscored_modules_and_underscored_plural_model_name = underscored_modules_and_underscored_plural_model_name
194
+ class_eval "def #{@model_singular_name}_url(record);#{singularized_underscored_modules_and_underscored_plural_model_name}_url(record);end" unless @model_singular_name == singularized_underscored_modules_and_underscored_plural_model_name
195
+ end
225
196
 
226
- self.param_to_query.each do |param_name, param_query|
227
- if params[param_name]
228
- # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
229
- value = param_query.call(t, value, params[param_name].to_s)
230
- end
231
- end
197
+ def convert_request_param_value_for_filtering(attr_sym, value)
198
+ value && NILS.include?(value) ? nil : value
199
+ end
232
200
 
233
- self.param_to_through.each do |param_name, through_array|
234
- if params[param_name]
235
- # build query
236
- # 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})
237
- last_model_class = @model_class
238
- joins = nil # {:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}
239
- through_array.each do |association_or_attribute|
240
- if association_or_attribute == through_array.last
241
- # 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
242
- # https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/jgJ4cjjS8FE/BGbHRxnDRTIJ
243
- value = value.joins(joins).where(last_model_class.table_name.to_sym => {association_or_attribute => params[param_name].to_s})
201
+ def render_or_respond(read_only_action, success_code = :ok)
202
+ if self.render_enabled
203
+ if !@value.nil? && ((read_only_action && RestfulJson.return_resource) || RestfulJson.avoid_respond_with)
204
+ respond_with(@value) do |format|
205
+ format.json do
206
+ # define local variables in blocks, not outside of them, to be safe, even though would work in this case
207
+ custom_action_serializer = self.action_to_serializer[params[:action].to_s]
208
+ custom_action_serializer_for = self.action_to_serializer_for[params[:action].to_s]
209
+ serialization_key = single_value_response? ? (custom_action_serializer_for == :each ? :each_serializer : :serializer) : (custom_action_serializer_for == :array ? :serializer : :each_serializer)
210
+ puts "serialization key = #{serialization_key}, custom_action_serializer = #{custom_action_serializer}"
211
+ if !@value.respond_to?(:errors) || @value.errors.empty?
212
+ render custom_action_serializer ? {json: @value, status: success_code, serialization_key => custom_action_serializer} : {json: @value, status: success_code}
244
213
  else
245
- found_classes = last_model_class.reflections.collect {|association_name, reflection| reflection.class_name.constantize if association_name.to_sym == association_or_attribute}.compact
246
- if found_classes.size > 0
247
- last_model_class = found_classes[0]
248
- else
249
- # bad can_filter_by :through found at runtime
250
- raise "Association #{association_or_attribute.inspect} not found on #{last_model_class}."
251
- end
252
-
253
- if joins.nil?
254
- joins = association_or_attribute
255
- else
256
- joins = {association_or_attribute => joins}
257
- end
214
+ render custom_action_serializer ? {json: {errors: @value.errors}, status: :unprocessable_entity, serialization_key => custom_action_serializer} : {json: {errors: @value.errors}, status: :unprocessable_entity}
258
215
  end
259
216
  end
260
217
  end
218
+ else
219
+ # code duplicated from above because local vars don't always traverse well into block (based on wierd ruby-proc bug experienced)
220
+ custom_action_serializer = self.action_to_serializer[params[:action].to_s]
221
+ custom_action_serializer_for = self.action_to_serializer_for[params[:action].to_s]
222
+ serialization_key = single_value_response? ? (custom_action_serializer_for == :array ? :serializer : :each_serializer) : (custom_action_serializer_for == :each ? :each_serializer : :serializer)
223
+ respond_with @value, custom_action_serializer ? {(self.action_to_serializer_for[params[:action].to_s] == :each ? :each_serializer : :serializer) => custom_action_serializer} : {}
261
224
  end
225
+ else
226
+ @value
227
+ end
228
+ end
262
229
 
263
- self.param_to_attr_and_arel_predicate.keys.each do |param_name|
264
- options = param_to_attr_and_arel_predicate[param_name][2]
265
- # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
266
- param = params[param_name].to_s || options[:with_default]
267
-
268
- if param.present? && param_to_attr_and_arel_predicate[param_name]
269
- attr_sym = param_to_attr_and_arel_predicate[param_name][0]
270
- predicate_sym = param_to_attr_and_arel_predicate[param_name][1]
271
- if predicate_sym == :eq
272
- value = value.where(attr_sym => convert_request_param_value_for_filtering(attr_sym, param))
273
- else
274
- one_or_more_param = param.split(self.filter_split).collect{|v|convert_request_param_value_for_filtering(attr_sym, v)}
275
- value = value.where(t[attr_sym].try(predicate_sym, one_or_more_param))
276
- end
277
- end
278
- end
279
-
280
- if params[:page] && self.supported_functions.include?(:page)
281
- page = params[:page].to_i
282
- 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?
283
- #TODO: to_s is hack to avoid it becoming an Arel::SelectManager for some reason which not sure what to do with
284
- value = value.skip((self.number_of_records_in_a_page * (page - 1)).to_s)
285
- value = value.take((self.number_of_records_in_a_page).to_s)
286
- end
230
+ def single_value_response?
231
+ SINGLE_VALUE_ACTIONS.include?(params[:action].to_s)
232
+ end
287
233
 
288
- if params[:skip] && self.supported_functions.include?(:skip)
289
- # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
290
- value = value.skip(params[:skip].to_s)
291
- end
234
+ # The controller's index (list) method to list resources.
235
+ #
236
+ # Note: this method be alias_method'd by query_for, so it is more than just index.
237
+ def index
238
+ t = @model_class.arel_table
239
+ value = @model_class.scoped # returns ActiveRecord::Relation equivalent to select with no where clause
240
+ custom_query = self.action_to_query[params[:action].to_s]
241
+ if custom_query
242
+ value = custom_query.call(t, value)
243
+ end
292
244
 
293
- if params[:take] && self.supported_functions.include?(:take)
245
+ self.param_to_query.each do |param_name, param_query|
246
+ if params[param_name]
294
247
  # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
295
- value = value.take(params[:take].to_s)
248
+ value = param_query.call(t, value, params[param_name].to_s)
296
249
  end
250
+ end
297
251
 
298
- if params[:uniq] && self.supported_functions.include?(:uniq)
299
- value = value.uniq
300
- end
252
+ self.param_to_through.each do |param_name, through_array|
253
+ if params[param_name]
254
+ # build query
255
+ # 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})
256
+ last_model_class = @model_class
257
+ joins = nil # {:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}
258
+ through_array.each do |association_or_attribute|
259
+ if association_or_attribute == through_array.last
260
+ # 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
261
+ # https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/jgJ4cjjS8FE/BGbHRxnDRTIJ
262
+ value = value.joins(joins).where(last_model_class.table_name.to_sym => {association_or_attribute => params[param_name].to_s})
263
+ else
264
+ found_classes = last_model_class.reflections.collect {|association_name, reflection| reflection.class_name.constantize if association_name.to_sym == association_or_attribute}.compact
265
+ if found_classes.size > 0
266
+ last_model_class = found_classes[0]
267
+ else
268
+ # bad can_filter_by :through found at runtime
269
+ raise "Association #{association_or_attribute.inspect} not found on #{last_model_class}."
270
+ end
301
271
 
302
- # these must happen at the end and are independent
303
- if params[:count] && self.supported_functions.include?(:count)
304
- value = value.count.to_i
305
- elsif params[:page_count] && self.supported_functions.include?(:page_count)
306
- count_value = value.count.to_i # this executes the query so nothing else can be done in AREL
307
- value = (count_value / self.number_of_records_in_a_page) + (count_value % self.number_of_records_in_a_page ? 1 : 0)
308
- else
309
- self.ordered_by.each do |attr_to_direction|
310
- # this looks nasty, but makes no sense to iterate keys if only single of each
311
- value = value.order(t[attr_to_direction.keys[0]].send(attr_to_direction.values[0]))
272
+ if joins.nil?
273
+ joins = association_or_attribute
274
+ else
275
+ joins = {association_or_attribute => joins}
276
+ end
277
+ end
312
278
  end
313
- value = value.to_a
314
- end
315
-
316
- @value = value
317
- instance_variable_set(@model_at_plural_name_sym, @value)
318
-
319
- if self.render_enabled
320
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
321
- respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
322
- else
323
- @value
324
279
  end
325
280
  end
326
281
 
327
- # The controller's show (get) method to return a resource.
328
- def show
329
- # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
330
- @value = @model_class.find(params[:id].to_s)
331
- instance_variable_set(@model_at_singular_name_sym, @value)
332
-
333
- if self.render_enabled
334
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
335
- respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
336
- else
337
- @value
282
+ self.param_to_attr_and_arel_predicate.keys.each do |param_name|
283
+ options = param_to_attr_and_arel_predicate[param_name][2]
284
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
285
+ param = params[param_name].to_s || options[:with_default]
286
+
287
+ if param.present? && param_to_attr_and_arel_predicate[param_name]
288
+ attr_sym = param_to_attr_and_arel_predicate[param_name][0]
289
+ predicate_sym = param_to_attr_and_arel_predicate[param_name][1]
290
+ if predicate_sym == :eq
291
+ value = value.where(attr_sym => convert_request_param_value_for_filtering(attr_sym, param))
292
+ else
293
+ one_or_more_param = param.split(self.filter_split).collect{|v|convert_request_param_value_for_filtering(attr_sym, v)}
294
+ value = value.where(t[attr_sym].try(predicate_sym, one_or_more_param))
295
+ end
338
296
  end
339
297
  end
340
298
 
341
- # The controller's new method (e.g. used for new record in html format).
342
- def new
343
- @value = @model_class.new
299
+ if params[:page] && self.supported_functions.include?(:page)
300
+ page = params[:page].to_i
301
+ 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?
302
+ #TODO: to_s is hack to avoid it becoming an Arel::SelectManager for some reason which not sure what to do with
303
+ value = value.skip((self.number_of_records_in_a_page * (page - 1)).to_s)
304
+ value = value.take((self.number_of_records_in_a_page).to_s)
305
+ end
344
306
 
345
- if self.render_enabled
346
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
347
- respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
348
- else
349
- @value
350
- end
307
+ if params[:skip] && self.supported_functions.include?(:skip)
308
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
309
+ value = value.skip(params[:skip].to_s)
351
310
  end
352
311
 
353
- # The controller's edit method (e.g. used for edit record in html format).
354
- def edit
312
+ if params[:take] && self.supported_functions.include?(:take)
355
313
  # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
356
- @value = @model_class.find(params[:id].to_s)
357
- instance_variable_set(@model_at_singular_name_sym, @value)
358
- @value
314
+ value = value.take(params[:take].to_s)
359
315
  end
360
316
 
361
- # The controller's create (post) method to create a resource.
362
- def create
363
- if self.use_permitters
364
- authorize! :create, @model_class
365
- allowed_params = permitted_params
366
- elsif respond_to? @create_model_singular_name_params_sym
367
- allowed_params = send(@create_model_singular_name_params_sym)
368
- elsif respond_to? @model_singular_name_params_sym
369
- allowed_params = send(@model_singular_name_params_sym)
370
- else
371
- allowed_params = params
372
- end
373
- @value = @model_class.new(allowed_params)
374
- @value.save
375
- instance_variable_set(@model_at_singular_name_sym, @value)
376
-
377
- if self.render_enabled
378
- if !@value.nil? && RestfulJson.return_resource
379
- respond_with(@value) do |format|
380
- format.json do
381
- # define local variables in blocks, not outside of them, to be safe, even though would work in this case
382
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
383
- if @value.errors.empty?
384
- render custom_action_serializer ? {json: @value, status: :created, serializer: custom_action_serializer} : {json: @value, status: :created}
385
- else
386
- render custom_action_serializer ? {json: {errors: @value.errors}, status: :unprocessable_entity, serializer: custom_action_serializer} : {json: {errors: @value.errors}, status: :unprocessable_entity}
387
- end
388
- end
389
- end
390
- else
391
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
392
- respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
393
- end
394
- else
395
- @value
396
- end
317
+ if params[:uniq] && self.supported_functions.include?(:uniq)
318
+ value = value.uniq
397
319
  end
398
320
 
399
- # The controller's update (put) method to update a resource.
400
- def update
401
- if self.use_permitters
402
- authorize! :update, @model_class
403
- allowed_params = permitted_params
404
- elsif respond_to? @create_model_singular_name_params_sym
405
- allowed_params = send(@update_model_singular_name_params_sym)
406
- elsif respond_to? @model_singular_name_params_sym
407
- allowed_params = send(@model_singular_name_params_sym)
408
- else
409
- allowed_params = params
410
- end
411
- # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
412
- @value = @model_class.find(params[:id].to_s)
413
- @value.update_attributes(allowed_params)
414
- instance_variable_set(@model_at_singular_name_sym, @value)
415
-
416
- if self.render_enabled
417
- if !@value.nil? && RestfulJson.return_resource
418
- respond_with(@value) do |format|
419
- format.json do
420
- # define local variables in blocks, not outside of them, to be safe, even though would work in this case
421
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
422
- if @value.errors.empty?
423
- render custom_action_serializer ? {json: @value, status: :ok, serializer: custom_action_serializer} : {json: @value, status: :ok}
424
- else
425
- render custom_action_serializer ? {json: {errors: @value.errors}, status: :unprocessable_entity, serializer: custom_action_serializer} : {json: {errors: @value.errors}, status: :unprocessable_entity}
426
- end
427
- end
428
- end
429
- else
430
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
431
- respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
432
- end
433
- else
434
- @value
321
+ # these must happen at the end and are independent
322
+ if params[:count] && self.supported_functions.include?(:count)
323
+ value = value.count.to_i
324
+ elsif params[:page_count] && self.supported_functions.include?(:page_count)
325
+ count_value = value.count.to_i # this executes the query so nothing else can be done in AREL
326
+ value = (count_value / self.number_of_records_in_a_page) + (count_value % self.number_of_records_in_a_page ? 1 : 0)
327
+ else
328
+ self.ordered_by.each do |attr_to_direction|
329
+ # this looks nasty, but makes no sense to iterate keys if only single of each
330
+ value = value.order(t[attr_to_direction.keys[0]].send(attr_to_direction.values[0]))
435
331
  end
332
+ value = value.to_a
436
333
  end
437
334
 
438
- # The controller's destroy (delete) method to destroy a resource.
439
- def destroy
440
- # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
441
- @value = @model_class.find(params[:id].to_s)
442
- @value.destroy
443
- instance_variable_set(@model_at_singular_name_sym, @value)
335
+ @value = value
336
+ instance_variable_set(@model_at_plural_name_sym, @value)
337
+ render_or_respond(true)
338
+ end
444
339
 
445
- if self.render_enabled
446
- custom_action_serializer = self.action_to_serializer[params[:action].to_s]
447
- respond_with @value, custom_action_serializer ? {serializer: custom_action_serializer} : {}
448
- else
449
- @value
450
- end
340
+ # The controller's show (get) method to return a resource.
341
+ def show
342
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
343
+ @value = @model_class.find(params[:id].to_s)
344
+ instance_variable_set(@model_at_singular_name_sym, @value)
345
+ render_or_respond(true)
346
+ end
347
+
348
+ # The controller's new method (e.g. used for new record in html format).
349
+ def new
350
+ @value = @model_class.new
351
+ render_or_respond(true)
352
+ end
353
+
354
+ # The controller's edit method (e.g. used for edit record in html format).
355
+ def edit
356
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
357
+ @value = @model_class.find(params[:id].to_s)
358
+ instance_variable_set(@model_at_singular_name_sym, @value)
359
+ @value
360
+ end
361
+
362
+ # The controller's create (post) method to create a resource.
363
+ def create
364
+ if self.use_permitters
365
+ authorize! :create, @model_class
366
+ allowed_params = permitted_params
367
+ elsif respond_to? @create_model_singular_name_params_sym
368
+ allowed_params = send(@create_model_singular_name_params_sym)
369
+ elsif respond_to? @model_singular_name_params_sym
370
+ allowed_params = send(@model_singular_name_params_sym)
371
+ else
372
+ allowed_params = params
451
373
  end
374
+ @value = @model_class.new(allowed_params)
375
+ @value.save
376
+ instance_variable_set(@model_at_singular_name_sym, @value)
377
+ render_or_respond(false, :created)
378
+ end
379
+
380
+ # The controller's update (put) method to update a resource.
381
+ def update
382
+ if self.use_permitters
383
+ authorize! :update, @model_class
384
+ allowed_params = permitted_params
385
+ elsif respond_to? @create_model_singular_name_params_sym
386
+ allowed_params = send(@update_model_singular_name_params_sym)
387
+ elsif respond_to? @model_singular_name_params_sym
388
+ allowed_params = send(@model_singular_name_params_sym)
389
+ else
390
+ allowed_params = params
391
+ end
392
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
393
+ @value = @model_class.find(params[:id].to_s)
394
+ @value.update_attributes(allowed_params)
395
+ instance_variable_set(@model_at_singular_name_sym, @value)
396
+ render_or_respond(false)
397
+ end
452
398
 
399
+ # The controller's destroy (delete) method to destroy a resource.
400
+ def destroy
401
+ # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
402
+ @value = @model_class.find(params[:id].to_s)
403
+ @value.destroy
404
+ instance_variable_set(@model_at_singular_name_sym, @value)
405
+ render_or_respond(false)
453
406
  end
454
407
  end
455
408
  end