introspective_grape 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +23 -0
  3. data/.github/workflows/test.yml +18 -0
  4. data/.rubocop.yml +84 -1173
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +23 -0
  7. data/Gemfile +3 -5
  8. data/README.md +95 -63
  9. data/Rakefile +1 -6
  10. data/introspective_grape.gemspec +63 -52
  11. data/lib/introspective_grape/api.rb +166 -137
  12. data/lib/introspective_grape/camel_snake.rb +5 -3
  13. data/lib/introspective_grape/configuration.rb +15 -0
  14. data/lib/introspective_grape/create_helpers.rb +3 -5
  15. data/lib/introspective_grape/doc.rb +19 -5
  16. data/lib/introspective_grape/filters.rb +98 -83
  17. data/lib/introspective_grape/formatter/camel_json.rb +2 -3
  18. data/lib/introspective_grape/helpers.rb +55 -48
  19. data/lib/introspective_grape/snake_params.rb +1 -2
  20. data/lib/introspective_grape/traversal.rb +33 -31
  21. data/lib/introspective_grape/validators.rb +23 -23
  22. data/lib/introspective_grape/version.rb +3 -1
  23. data/lib/introspective_grape.rb +3 -1
  24. data/spec/dummy/Gemfile +5 -4
  25. data/spec/dummy/app/api/api_helpers.rb +1 -1
  26. data/spec/dummy/app/api/dummy/company_api.rb +1 -1
  27. data/spec/dummy/app/api/dummy/project_api.rb +1 -0
  28. data/spec/dummy/app/api/dummy/sessions.rb +1 -1
  29. data/spec/dummy/app/api/dummy_api.rb +8 -2
  30. data/spec/dummy/app/assets/config/manifest.js +4 -0
  31. data/spec/dummy/app/models/user.rb +1 -1
  32. data/spec/dummy/config/database.yml +1 -1
  33. data/spec/rails_helper.rb +1 -1
  34. metadata +161 -41
  35. data/.coveralls.yml +0 -2
  36. data/.travis.yml +0 -40
  37. data/bin/rails +0 -12
  38. data/gemfiles/Gemfile.rails.5.0.0 +0 -14
  39. data/gemfiles/Gemfile.rails.5.0.1 +0 -14
  40. data/gemfiles/Gemfile.rails.5.1.0 +0 -14
  41. data/gemfiles/Gemfile.rails.5.2.0 +0 -14
  42. data/gemfiles/Gemfile.rails.master +0 -14
@@ -1,5 +1,6 @@
1
1
  require 'action_controller'
2
2
  require 'kaminari'
3
+ require 'byebug'
3
4
  require 'grape-kaminari'
4
5
  require 'introspective_grape/validators'
5
6
 
@@ -7,13 +8,16 @@ class IntrospectiveGrapeError < StandardError
7
8
  end
8
9
 
9
10
  module IntrospectiveGrape
11
+ # rubocop:disable Metrics/ClassLength
10
12
  class API < Grape::API::Instance
13
+ # rubocop:enable Metrics/ClassLength
11
14
  extend IntrospectiveGrape::Helpers
12
15
  extend IntrospectiveGrape::CreateHelpers
13
16
  extend IntrospectiveGrape::Filters
14
17
  extend IntrospectiveGrape::Traversal
15
18
  extend IntrospectiveGrape::Doc
16
19
  extend IntrospectiveGrape::SnakeParams
20
+ include Grape::Kaminari
17
21
 
18
22
  # Allow files to be uploaded through ActionController:
19
23
  ActionController::Parameters::PERMITTED_SCALAR_TYPES.push Rack::Multipart::UploadedFile, ActionController::Parameters
@@ -36,7 +40,7 @@ module IntrospectiveGrape
36
40
  # types for the attributes specified in a hash:
37
41
  #
38
42
  # def self.grape_param_types
39
- # { "<attribute name>" => Virtus::Attribute::Boolean,
43
+ # { "<attribute name>" => Grape::API::Boolean,
40
44
  # "<attribute name>" => Integer,
41
45
  # "<attribute name>" => String }
42
46
  # end
@@ -48,30 +52,30 @@ module IntrospectiveGrape
48
52
  #
49
53
 
50
54
  class << self
51
- PLURAL_REFLECTIONS = [ ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasManyReflection].freeze
55
+ PLURAL_REFLECTIONS = [ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasManyReflection].freeze
52
56
  # mapping of activerecord/postgres 'type's to ruby data classes, where they differ
53
- Pg2Ruby = { datetime: DateTime }.freeze
57
+ PG2RUBY = { datetime: DateTime }.freeze
54
58
 
55
59
  def inherited(child)
56
60
  super(child)
57
61
  child.before do
58
62
  # Ensure that a user is logged in.
59
- self.send(IntrospectiveGrape::API.authentication_method(self))
63
+ send(IntrospectiveGrape::API.authentication_method(self))
60
64
  end
61
65
 
62
66
  child.snake_params_before_validation if IntrospectiveGrape.config.camelize_parameters
63
67
  end
64
68
 
65
69
  # We will probably need before and after hooks eventually, but haven't yet...
66
- #api_actions.each do |a|
67
- # define_method "before_#{a}_hook" do |model,params| ; end
68
- # define_method "after_#{a}_hook" do |model,params| ; end
69
- #end
70
+ # api_actions.each do |a|
71
+ # define_method "before_#{a}_hook" do |model, params| ; end
72
+ # define_method "after_#{a}_hook" do |model, params| ; end
73
+ # end
70
74
 
75
+ # rubocop:disable Metrics/AbcSize
71
76
  def restful(model, strong_params=[], routes=[])
72
- if model.respond_to?(:attribute_param_types)
73
- raise IntrospectiveGrapeError.new("#{model.name}'s attribute_param_types class method needs to be changed to grape_param_types")
74
- end
77
+ raise IntrospectiveGrapeError.new("#{model.name}'s attribute_param_types class method needs to be changed to grape_param_types") if model.respond_to?(:attribute_param_types)
78
+
75
79
  # Recursively define endpoints for the model and any nested models.
76
80
  #
77
81
  # model: the model class for the API
@@ -84,9 +88,9 @@ module IntrospectiveGrape
84
88
  begin ActiveRecord::Migration.check_pending! rescue return end
85
89
 
86
90
  # normalize the whitelist to symbols
87
- strong_params.map!{|f| f.kind_of?(String) ? f.to_sym : f }
91
+ strong_params.map! {|f| f.is_a?(String) ? f.to_sym : f }
88
92
  # default to a flat representation of the model's attributes if left unspecified
89
- strong_params = strong_params.blank? ? model.attribute_names.map(&:to_sym)-[:id, :updated_at, :created_at] : strong_params
93
+ strong_params = strong_params.blank? ? model.attribute_names.map(&:to_sym) - %i(id updated_at created_at) : strong_params
90
94
 
91
95
  # The strong params will be the same for all routes, differing from the Grape params
92
96
  # when routes are nested
@@ -109,14 +113,14 @@ module IntrospectiveGrape
109
113
  # recursively define endpoints
110
114
  model = routes.last.model || return
111
115
 
112
- api_params.select{|a| a.kind_of?(Hash) }.each do |nested|
116
+ api_params.select {|a| a.is_a?(Hash) }.each do |nested|
113
117
  # Recursively add RESTful nested routes for every nested model:
114
- nested.each do |r,fields|
118
+ nested.each do |r, fields|
115
119
  # Look at model.reflections to find the association's class name:
116
- reflection_name = r.to_s.sub(/_attributes$/,'')
120
+ reflection_name = r.to_s.sub(/_attributes$/, '')
117
121
  begin
118
122
  relation = model.reflections[reflection_name].class_name.constantize
119
- rescue
123
+ rescue StandardError
120
124
  Rails.logger.fatal "Can't find associated model for #{r} on #{model}"
121
125
  end
122
126
 
@@ -126,69 +130,18 @@ module IntrospectiveGrape
126
130
  end
127
131
  end
128
132
 
129
- def convert_nested_params_hash(dsl, routes)
130
- root = routes.first
131
- klass = root.klass
132
- dsl.after_validation do
133
- # After Grape validates its parameters:
134
- # 1) Find the root model instance for the API if its passed (implicitly either
135
- # an update/destroy on the root node or it's a nested route
136
- # 2) For nested endpoints convert the params hash into Rails-compliant nested
137
- # attributes, to be passed to the root instance for update. This keeps
138
- # behavior consistent between bulk and single record updates.
139
-
140
- if params[root.key]
141
- @model = root.model.includes( klass.default_includes(root.model) ).find(params[root.key])
142
- end
143
-
144
- if routes.size > 1
145
- nested_attributes = klass.build_nested_attributes(routes[1..-1], params.except(root.key,:api_key) )
146
- @params.merge!( nested_attributes ) if nested_attributes.kind_of?(Hash)
147
- end
148
- end
149
- end
150
-
151
- def define_restful_api(dsl, routes, model, api_params)
152
- # declare index, show, update, create, and destroy methods:
153
- API_ACTIONS.each do |action|
154
- send("define_#{action}", dsl, routes, model, api_params) unless exclude_actions(model).include?(action)
155
- end
156
- end
157
-
158
- def define_endpoints(routes,api_params)
159
- # De-reference these as local variables from their class scope, or when we make
160
- # calls to the API they will be whatever they were last set to by the recursive
161
- # calls to "nest_routes".
162
- routes = routes.clone
163
- api_params = api_params.clone
164
-
165
- model = routes.last.model || return
166
-
167
- # We define the param keys for ID fields in camelcase for swagger's URL substitution,
168
- # they'll come back in snake case in the params hash, the API as a whole is agnostic:
169
- namespace = routes[0..-2].map{|p| "#{p.name.pluralize}/:#{p.swaggerKey}/" }.join + routes.last.name.pluralize
170
-
171
- resource namespace do
172
- convert_nested_params_hash(self, routes)
173
- define_restful_api(self, routes, model, api_params)
174
- end
175
- end
176
-
177
133
  def define_index(dsl, routes, model, api_params)
178
- include Grape::Kaminari
179
134
  root = routes.first
180
135
  klass = routes.first.klass
181
136
  name = routes.last.name.pluralize
182
137
  simple_filters(klass, model, api_params)
183
138
 
184
139
  dsl.desc "list #{name}" do
185
- detail klass.index_documentation || "returns list of all #{name}"
140
+ detail klass.index_documentation(name)
186
141
  end
187
142
  dsl.params do
188
143
  klass.declare_filter_params(self, klass, model, api_params)
189
- end
190
- if klass.pagination
191
- paginate per_page: klass.pagination[:per_page]||25, max_per_page: klass.pagination[:max_per_page], offset: klass.pagination[:offset]||0
144
+ use :pagination, per_page: klass.pagination[:per_page]||25, max_per_page: klass.pagination[:max_per_page], offset: klass.pagination[:offset]||0 if klass.pagination
192
145
  end
193
146
  dsl.get '/' do
194
147
  # Invoke the policy for the action, defined in the policy classes for the model:
@@ -196,10 +149,8 @@ module IntrospectiveGrape
196
149
 
197
150
  # Nested route indexes need to be scoped by the API's top level policy class:
198
151
  records = policy_scope( root.model.includes(klass.default_includes(root.model)) )
199
-
200
152
  records = klass.apply_filter_params(klass, model, api_params, params, records)
201
-
202
- records = records.map{|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
153
+ records = records.map {|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
203
154
 
204
155
  # paginate the records using Kaminari
205
156
  records = paginate(Kaminari.paginate_array(records)) if klass.pagination
@@ -208,12 +159,12 @@ module IntrospectiveGrape
208
159
  end
209
160
 
210
161
  def define_show(dsl, routes, model, _api_params)
211
- name = routes.last.name.singularize
162
+ name = routes.last.name.singularize
212
163
  klass = routes.first.klass
213
164
  dsl.desc "retrieve a #{name}" do
214
- detail klass.show_documentation || "returns details on a #{name}"
165
+ detail klass.show_documentation(name)
215
166
  end
216
- dsl.get ":#{routes.last.swaggerKey}" do
167
+ dsl.get ":#{routes.last.swagger_key}" do
217
168
  authorize @model, :show?
218
169
  present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
219
170
  end
@@ -223,7 +174,7 @@ module IntrospectiveGrape
223
174
  name = routes.last.name.singularize
224
175
  klass = routes.first.klass
225
176
  dsl.desc "create a #{name}" do
226
- detail klass.create_documentation || "creates a new #{name} record"
177
+ detail klass.create_documentation(name)
227
178
  end
228
179
  dsl.params do
229
180
  klass.generate_params(self, :create, model, api_params, true)
@@ -236,19 +187,17 @@ module IntrospectiveGrape
236
187
 
237
188
  def define_update(dsl, routes, model, api_params)
238
189
  klass = routes.first.klass
239
- name = routes.last.name.singularize
190
+ name = routes.last.name.singularize
240
191
  dsl.desc "update a #{name}" do
241
- detail klass.update_documentation || "updates the details of a #{name}"
192
+ detail klass.update_documentation(name)
242
193
  end
243
194
  dsl.params do
244
195
  klass.generate_params(self, :update, model, api_params, true)
245
196
  end
246
- dsl.put ":#{routes.last.swaggerKey}" do
197
+ dsl.put ":#{routes.last.swagger_key}" do
247
198
  authorize @model, :update?
248
199
 
249
200
  @model.update_attributes!( safe_params(params).permit(klass.whitelist) )
250
-
251
- default_includes = routes.first.klass.default_includes(routes.first.model)
252
201
 
253
202
  if IntrospectiveGrape.config.skip_object_reload
254
203
  present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
@@ -259,18 +208,66 @@ module IntrospectiveGrape
259
208
  end
260
209
  end
261
210
 
211
+ # rubocop:enable Metrics/AbcSize
262
212
  def define_destroy(dsl, routes, _model, _api_params)
263
213
  klass = routes.first.klass
264
214
  name = routes.last.name.singularize
265
215
  dsl.desc "destroy a #{name}" do
266
- detail klass.destroy_documentation || "destroys the details of a #{name}"
216
+ detail klass.destroy_documentation(name)
267
217
  end
268
- dsl.delete ":#{routes.last.swaggerKey}" do
218
+ dsl.delete ":#{routes.last.swagger_key}" do
269
219
  authorize @model, :destroy?
270
220
  present status: (klass.find_leaf(routes, @model, params).destroy ? true : false)
271
221
  end
272
222
  end
273
223
 
224
+ def convert_nested_params_hash(dsl, routes)
225
+ root = routes.first
226
+ klass = self
227
+ dsl.after_validation do
228
+ next unless params[root.key] # there was no one, nothing to see
229
+
230
+ # After Grape validates its parameters:
231
+ # 1) Find the root model instance for the API if its passed (implicitly either
232
+ # an update/destroy on the root node or it's a nested route
233
+ # 2) For nested endpoints convert the params hash into Rails-compliant nested
234
+ # attributes, to be passed to the root instance for update. This keeps
235
+ # behavior consistent between bulk and single record updates.
236
+ @model = root.model.includes( root.klass.default_includes(root.model) ).find(params[root.key])
237
+ @params.merge!( klass.merge_nested_params(routes, params) )
238
+ end
239
+ end
240
+
241
+ def merge_nested_params(routes, params)
242
+ attr = params.reject {|k| [routes.first.key, :api_key].include?(k) }
243
+ build_nested_attributes(routes[1..-1], attr)
244
+ end
245
+
246
+ def define_restful_api(dsl, routes, model, api_params)
247
+ # declare index, show, update, create, and destroy methods:
248
+ API_ACTIONS.each do |action|
249
+ send("define_#{action}", dsl, routes, model, api_params) unless exclude_actions(model).include?(action)
250
+ end
251
+ end
252
+
253
+ def define_endpoints(routes, api_params)
254
+ # De-reference these as local variables from their class scope, or when we make
255
+ # calls to the API they will be whatever they were last set to by the recursive
256
+ # calls to "nest_routes".
257
+ routes = routes.clone
258
+ api_params = api_params.clone
259
+ model = routes.last.model || return
260
+
261
+ # We define the param keys for ID fields in camelcase for swagger's URL substitution,
262
+ # they'll come back in snake case in the params hash, the API as a whole is agnostic:
263
+ namespace = routes[0..-2].map {|p| "#{p.name.pluralize}/:#{p.swagger_key}/" }.join + routes.last.name.pluralize
264
+
265
+ klass = self # the 'resource' block changes the context to the Grape::API::Instance...
266
+ resource namespace do
267
+ klass.convert_nested_params_hash(self, routes)
268
+ klass.define_restful_api(self, routes, model, api_params)
269
+ end
270
+ end
274
271
 
275
272
  def build_routes(routes, model, reflection_name=nil)
276
273
  routes = routes.clone
@@ -280,21 +277,26 @@ module IntrospectiveGrape
280
277
  #
281
278
  # Constructs an array representation of the route's models and their associations,
282
279
  # a /root/:rootId/branch/:branchId/leaf/:leafId path would have flat array like
283
- # [root,branch,leaf] representing the path structure and its models, used to
280
+ # [root, branch, leaf] representing the path structure and its models, used to
284
281
  # manipulate ActiveRecord relationships and params hashes and so on.
285
- parent_model = routes.last.try(:model)
282
+ parent_model = routes.last&.model
286
283
  return routes if model == parent_model
287
284
 
288
- name = reflection_name || model.name.underscore
289
- reflection = parent_model.try(:reflections).try(:fetch,reflection_name)
290
- many = parent_model && PLURAL_REFLECTIONS.include?( reflection.class ) ? true : false
291
- swaggerKey = IntrospectiveGrape.config.camelize_parameters ? "#{name.singularize.camelize(:lower)}Id" : "#{name.singularize}_id"
285
+ name = reflection_name || model.name.underscore
286
+ reflection = parent_model&.reflections&.fetch(reflection_name)
287
+ swagger_key = IntrospectiveGrape.config.camelize_parameters ? "#{name.singularize.camelize(:lower)}Id" : "#{name.singularize}_id"
292
288
 
293
- routes.push OpenStruct.new(klass: self, name: name, param: "#{name}_attributes", model: model, many?: many, key: "#{name.singularize}_id".to_sym, swaggerKey: swaggerKey, reflection: reflection)
289
+ routes.push OpenStruct.new(klass: self, name: name, param: "#{name}_attributes", model: model,
290
+ many?: plural?(parent_model, reflection),
291
+ key: "#{name.singularize}_id".to_sym,
292
+ swagger_key: swagger_key, reflection: reflection)
294
293
  end
295
294
 
295
+ def plural?(model, reflection)
296
+ (model && PLURAL_REFLECTIONS.include?(reflection.class))
297
+ end
296
298
 
297
- def build_nested_attributes(routes,hash)
299
+ def build_nested_attributes(routes, hash)
298
300
  # Recursively re-express the flat attributes hash from nested routes as nested
299
301
  # attributes that can be used to perform an update on the root model.
300
302
 
@@ -315,13 +317,12 @@ module IntrospectiveGrape
315
317
  end
316
318
  end
317
319
 
318
-
319
-
320
320
  def generate_params(dsl, action, model, fields, is_root_endpoint=false)
321
321
  # We'll be doing a recursive walk (to handle nested attributes) down the
322
322
  # whitelisted params, generating the Grape param definitions by introspecting
323
323
  # on the model and its associations.
324
- raise "Invalid action: #{action}" unless [:update, :create].include?(action)
324
+ raise "Invalid action: #{action}" unless %i(update create).include?(action)
325
+
325
326
  # dsl : The Grape::Validations::ParamsScope object
326
327
  # action: create or update
327
328
  # model : The ActiveRecord model class
@@ -332,16 +333,16 @@ module IntrospectiveGrape
332
333
  fields -= [:id] if is_root_endpoint
333
334
 
334
335
  fields.each do |field|
335
- if field.kind_of?(Hash)
336
- generate_nested_params(dsl,action,model,field)
337
- elsif (action==:create && param_required?(model,field) )
336
+ if field.is_a?(Hash)
337
+ generate_nested_params(dsl, action, model, field)
338
+ elsif action == :create && param_required?(model, field)
338
339
  # All params are optional on an update, only require them during creation.
339
340
  # Updating a record with new child models will have to rely on ActiveRecord
340
341
  # validations:
341
- dsl.requires field, { type: param_type(model,field) }.merge( validations(model, field) )
342
+ dsl.requires field, { type: param_type(model, field) }.merge( validations(model, field) )
342
343
  else
343
- #dsl.optional field, *options
344
- dsl.optional field, { type: param_type(model,field) }.merge( validations(model, field) )
344
+ # dsl.optional field, *options
345
+ dsl.optional field, { type: param_type(model, field) }.merge( validations(model, field) )
345
346
  end
346
347
  end
347
348
  end
@@ -350,25 +351,23 @@ module IntrospectiveGrape
350
351
  (model.try(:grape_validations) || {}).with_indifferent_access[field] || {}
351
352
  end
352
353
 
353
- def generate_nested_params(dsl,action,model,fields)
354
+ def generate_nested_params(dsl, action, model, fields)
354
355
  klass = self
355
- fields.each do |r,v|
356
+ fields.each do |r, v|
356
357
  # Look at model.reflections to find the association's class name:
357
- reflection = r.to_s.sub(/_attributes$/,'') # the reflection name
358
- relation = begin model.reflections[reflection].class_name.constantize rescue model end
358
+ reflection = r.to_s.sub(/_attributes$/, '') # the reflection name
359
+ relation = find_relation(model, reflection)
359
360
 
360
- if is_file_attachment?(model,r)
361
+ if file_attachment?(model, r)
361
362
  # Handle Carrierwave file upload fields
362
- s = [:filename, :type, :name, :tempfile, :head]-v
363
- if s.present?
364
- Rails.logger.warn "Missing required file upload parameters #{s} for uploader field #{r}"
365
- end
366
- elsif PLURAL_REFLECTIONS.include?( model.reflections[reflection].class )
363
+ s = %i(filename type name tempfile head) - v
364
+ Rails.logger.warn "Missing required file upload parameters #{s} for uploader field #{r}" if s.present?
365
+ elsif plural_reflection?(model, reflection)
367
366
  # In case you need a refresher on how these work:
368
367
  # http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
369
368
  dsl.optional r, type: Array do |dl|
370
- klass.generate_params(dl,action,relation,v)
371
- klass.add_destroy_param(dl,model,reflection) unless action==:create
369
+ klass.generate_params(dl, action, relation, v)
370
+ klass.add_destroy_param(dl, model, reflection, action)
372
371
  end
373
372
  else
374
373
  # TODO: handle any remaining correctly. Presently defaults to a Hash
@@ -376,50 +375,80 @@ module IntrospectiveGrape
376
375
  # ThroughReflection, HasOneReflection,
377
376
  # HasAndBelongsToManyReflection, BelongsToReflection
378
377
  dsl.optional r, type: Hash do |dl|
379
- klass.generate_params(dl,action,relation,v)
380
- klass.add_destroy_param(dl,model,reflection) unless action==:create
378
+ klass.generate_params(dl, action, relation, v)
379
+ klass.add_destroy_param(dl, model, reflection, action)
381
380
  end
382
381
  end
383
382
  end
384
383
  end
385
384
 
386
- def is_file_attachment?(model,field)
387
- model.respond_to?(:uploaders) && model.uploaders[field.to_sym] || # carrierwave
385
+ def plural_reflection?(model, reflection)
386
+ PLURAL_REFLECTIONS.include?( model.reflections[reflection].class )
387
+ end
388
+
389
+ def find_relation(model, reflection)
390
+ begin
391
+ model.reflections[reflection].class_name.constantize
392
+ rescue StandardError
393
+ model
394
+ end
395
+ end
396
+
397
+ def file_attachment?(model, field)
398
+ (model.respond_to?(:uploaders) && model.uploaders[field.to_sym]) || # carrierwave
388
399
  (model.respond_to?(:attachment_definitions) && model.attachment_definitions[field.to_sym]) ||
389
- defined?(Paperclip::Attachment) && model.send(:new).try(field).kind_of?(Paperclip::Attachment)
400
+ (defined?(Paperclip::Attachment) && model.send(:new).try(field).is_a?(Paperclip::Attachment))
390
401
  end
391
402
 
392
- def param_type(model,f)
403
+ def param_type(model, field)
393
404
  # Translate from the AR type to the GrapeParam types
394
- f = f.to_s
395
- db_type = (model.try(:columns_hash)||{})[f].try(:type)
405
+ field = field.to_s
406
+ db_type = (model&.columns_hash || {})[field]&.type
396
407
 
397
408
  # Check if it's a file attachment, look for an override class from the model,
398
- # check Pg2Ruby, use the database type, or fail over to a String:
399
- ( is_file_attachment?(model,f) && Rack::Multipart::UploadedFile ) ||
400
- (model.try(:grape_param_types)||{}).with_indifferent_access[f] ||
401
- Pg2Ruby[db_type] ||
402
- begin db_type.to_s.camelize.constantize rescue nil end ||
403
- String
409
+ # check PG2RUBY, use the database type, or fail over to a String:
410
+ uploaded_file?(model, field) ||
411
+ check_model_for_type(model, field) ||
412
+ PG2RUBY[db_type] ||
413
+ db_type_constant(db_type) ||
414
+ String # default to String if nothing else works
404
415
  end
405
416
 
406
- def param_required?(model,f)
417
+ def uploaded_file?(model, field)
418
+ file_attachment?(model, field) && Rack::Multipart::UploadedFile
419
+ end
420
+
421
+ def check_model_for_type(model, field)
422
+ (model.try(:grape_param_types) || {}).with_indifferent_access[field]
423
+ end
424
+
425
+ def db_type_constant(db_type)
426
+ begin
427
+ db_type.to_s.camelize.constantize
428
+ rescue StandardError
429
+ nil
430
+ end
431
+ end
432
+
433
+ def param_required?(model, field)
407
434
  # Detect if the field is a required field for the create action
408
- return false if skip_presence_validations.include?(f)
435
+ return false if skip_presence_validations.include?(field)
409
436
 
410
- validated_field = f =~ /_id/ ? f.to_s.sub(/_id\z/,'').to_sym : f.to_sym
437
+ validated_field = field.match?(/_id/) ? field.to_s.sub(/_id\z/, '').to_sym : field.to_sym
411
438
 
412
- model.validators_on(validated_field).any? {|v| v.kind_of? ActiveRecord::Validations::PresenceValidator }
439
+ model.validators_on(validated_field).any? {|v| v.is_a? ActiveRecord::Validations::PresenceValidator }
413
440
  end
414
441
 
415
- def add_destroy_param(dsl,model,reflection)
416
- raise "#{model} does not accept nested attributes for #{reflection}" if !model.nested_attributes_options[reflection.to_sym]
442
+ def add_destroy_param(dsl, model, reflection, action)
443
+ return if action == :create
444
+
445
+ raise "#{model} does not accept nested attributes for #{reflection}" unless model.nested_attributes_options[reflection.to_sym]
446
+
447
+ return unless model.nested_attributes_options[reflection.to_sym][:allow_destroy]
448
+
417
449
  # If destruction is allowed append the _destroy field
418
- if model.nested_attributes_options[reflection.to_sym][:allow_destroy]
419
- dsl.optional '_destroy', type: Integer
420
- end
450
+ dsl.optional '_destroy', type: Integer
421
451
  end
422
-
423
452
  end
424
453
  end
425
454
  end
@@ -8,10 +8,10 @@ if IntrospectiveGrape.config.camelize_parameters
8
8
  module ParseParamsWithCamelized
9
9
  def parse_params(params, path, method, _options = {})
10
10
  parsed_params = parse_params_without_camelized(params, path, method)
11
- parsed_params.each_with_index do |param|
11
+ parsed_params.each do |param|
12
12
  param[:name] = param[:name]
13
- .camelize(:lower)
14
- .gsub(/Destroy/,'_destroy')
13
+ .camelize(:lower)
14
+ .gsub(/Destroy/, '_destroy')
15
15
  end
16
16
  super(params, path, method, _options = {})
17
17
  end
@@ -19,6 +19,7 @@ if IntrospectiveGrape.config.camelize_parameters
19
19
 
20
20
  module CreateCamelizedDocumentationClass
21
21
  private
22
+
22
23
  def create_documentation_class
23
24
  doc = super
24
25
  doc.class_eval do
@@ -29,6 +30,7 @@ if IntrospectiveGrape.config.camelize_parameters
29
30
  end
30
31
 
31
32
  Grape::API::Instance.singleton_class.send(:prepend, CreateCamelizedDocumentationClass)
33
+
32
34
  else
33
35
  module CallWithCamelized
34
36
  def call(*args)
@@ -0,0 +1,15 @@
1
+ module IntrospectiveGrape
2
+ def self.configure
3
+ self.config ||= Configuration.new
4
+ yield config
5
+ end
6
+
7
+ class Configuration
8
+ attr_accessor :camelize_parameters, :skip_object_reload
9
+
10
+ def initialize
11
+ @camelize_parameters = true
12
+ @skip_object_reload = false
13
+ end
14
+ end
15
+ end
@@ -1,18 +1,17 @@
1
1
  module IntrospectiveGrape
2
2
  module CreateHelpers
3
-
4
3
  def add_new_records_to_root_record(dsl, routes, params, model)
5
4
  dsl.send(:authorize, model, :create?)
6
5
  ActiveRecord::Base.transaction do
7
6
  old = find_leaves(routes, model, params)
8
- model.update_attributes( dsl.send(:safe_params,params).permit(whitelist) )
7
+ model.update_attributes( dsl.send(:safe_params, params).permit(whitelist) )
9
8
  new = find_leaves(routes, model, params)
10
- old.respond_to?(:size) ? new-old : new
9
+ old.respond_to?(:size) ? new - old : new
11
10
  end
12
11
  end
13
12
 
14
13
  def create_new_record(dsl, routes, params)
15
- model = routes.first.model.new( dsl.send(:safe_params,params).permit(whitelist) )
14
+ model = routes.first.model.new( dsl.send(:safe_params, params).permit(whitelist) )
16
15
  dsl.send(:authorize, model, :create?)
17
16
  model.save!
18
17
 
@@ -22,6 +21,5 @@ module IntrospectiveGrape
22
21
 
23
22
  find_leaves(routes, model, params)
24
23
  end
25
-
26
24
  end
27
25
  end
@@ -1,9 +1,23 @@
1
1
  module IntrospectiveGrape
2
2
  module Doc
3
- def index_documentation; end
4
- def show_documentation; end
5
- def create_documentation; end
6
- def update_documentation; end
7
- def destroy_documentation; end
3
+ def index_documentation(name=nil)
4
+ "returns list of all #{name}"
5
+ end
6
+
7
+ def show_documentation(name=nil)
8
+ "returns details on a #{name}"
9
+ end
10
+
11
+ def create_documentation(name=nil)
12
+ "creates a new #{name} record"
13
+ end
14
+
15
+ def update_documentation(name=nil)
16
+ "updates the details of a #{name}"
17
+ end
18
+
19
+ def destroy_documentation(name=nil)
20
+ "destroys the details of a #{name}"
21
+ end
8
22
  end
9
23
  end