introspective_grape 0.4.1 → 0.5.2

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +23 -0
  3. data/.github/workflows/security.yml +32 -0
  4. data/.github/workflows/test.yml +18 -0
  5. data/.rubocop.yml +84 -1173
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +30 -0
  8. data/Gemfile +3 -5
  9. data/README.md +94 -63
  10. data/Rakefile +1 -6
  11. data/introspective_grape.gemspec +63 -53
  12. data/lib/introspective_grape/api.rb +166 -137
  13. data/lib/introspective_grape/camel_snake.rb +5 -3
  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 +56 -54
  21. data/lib/introspective_grape/validators.rb +23 -23
  22. data/lib/introspective_grape/version.rb +3 -1
  23. data/spec/dummy/.ruby-version +1 -0
  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 +10 -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. data/spec/requests/company_api_spec.rb +9 -0
  35. metadata +153 -45
  36. data/.coveralls.yml +0 -2
  37. data/.travis.yml +0 -40
  38. data/bin/rails +0 -12
  39. data/gemfiles/Gemfile.rails.5.0.0 +0 -14
  40. data/gemfiles/Gemfile.rails.5.0.1 +0 -14
  41. data/gemfiles/Gemfile.rails.5.1.0 +0 -14
  42. data/gemfiles/Gemfile.rails.5.2.0 +0 -14
  43. 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)
@@ -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