restful_json 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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