introspective_grape 0.4.3 → 0.5.4

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 +35 -0
  8. data/Gemfile +3 -5
  9. data/README.md +94 -63
  10. data/Rakefile +1 -6
  11. data/introspective_grape.gemspec +63 -51
  12. data/lib/introspective_grape/api.rb +167 -136
  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 +150 -42
  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,18 +187,18 @@ 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
-
201
+
251
202
  if IntrospectiveGrape.config.skip_object_reload
252
203
  present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
253
204
  else
@@ -257,18 +208,66 @@ module IntrospectiveGrape
257
208
  end
258
209
  end
259
210
 
211
+ # rubocop:enable Metrics/AbcSize
260
212
  def define_destroy(dsl, routes, _model, _api_params)
261
213
  klass = routes.first.klass
262
214
  name = routes.last.name.singularize
263
215
  dsl.desc "destroy a #{name}" do
264
- detail klass.destroy_documentation || "destroys the details of a #{name}"
216
+ detail klass.destroy_documentation(name)
265
217
  end
266
- dsl.delete ":#{routes.last.swaggerKey}" do
218
+ dsl.delete ":#{routes.last.swagger_key}" do
267
219
  authorize @model, :destroy?
268
220
  present status: (klass.find_leaf(routes, @model, params).destroy ? true : false)
269
221
  end
270
222
  end
271
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
272
271
 
273
272
  def build_routes(routes, model, reflection_name=nil)
274
273
  routes = routes.clone
@@ -278,21 +277,26 @@ module IntrospectiveGrape
278
277
  #
279
278
  # Constructs an array representation of the route's models and their associations,
280
279
  # a /root/:rootId/branch/:branchId/leaf/:leafId path would have flat array like
281
- # [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
282
281
  # manipulate ActiveRecord relationships and params hashes and so on.
283
- parent_model = routes.last.try(:model)
282
+ parent_model = routes.last&.model
284
283
  return routes if model == parent_model
285
284
 
286
- name = reflection_name || model.name.underscore
287
- reflection = parent_model.try(:reflections).try(:fetch,reflection_name)
288
- many = parent_model && PLURAL_REFLECTIONS.include?( reflection.class ) ? true : false
289
- 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"
290
288
 
291
- 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)
292
293
  end
293
294
 
295
+ def plural?(model, reflection)
296
+ (model && PLURAL_REFLECTIONS.include?(reflection.class))
297
+ end
294
298
 
295
- def build_nested_attributes(routes,hash)
299
+ def build_nested_attributes(routes, hash)
296
300
  # Recursively re-express the flat attributes hash from nested routes as nested
297
301
  # attributes that can be used to perform an update on the root model.
298
302
 
@@ -313,13 +317,12 @@ module IntrospectiveGrape
313
317
  end
314
318
  end
315
319
 
316
-
317
-
318
320
  def generate_params(dsl, action, model, fields, is_root_endpoint=false)
319
321
  # We'll be doing a recursive walk (to handle nested attributes) down the
320
322
  # whitelisted params, generating the Grape param definitions by introspecting
321
323
  # on the model and its associations.
322
- raise "Invalid action: #{action}" unless [:update, :create].include?(action)
324
+ raise "Invalid action: #{action}" unless %i(update create).include?(action)
325
+
323
326
  # dsl : The Grape::Validations::ParamsScope object
324
327
  # action: create or update
325
328
  # model : The ActiveRecord model class
@@ -330,16 +333,16 @@ module IntrospectiveGrape
330
333
  fields -= [:id] if is_root_endpoint
331
334
 
332
335
  fields.each do |field|
333
- if field.kind_of?(Hash)
334
- generate_nested_params(dsl,action,model,field)
335
- 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)
336
339
  # All params are optional on an update, only require them during creation.
337
340
  # Updating a record with new child models will have to rely on ActiveRecord
338
341
  # validations:
339
- 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) )
340
343
  else
341
- #dsl.optional field, *options
342
- 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) )
343
346
  end
344
347
  end
345
348
  end
@@ -348,25 +351,23 @@ module IntrospectiveGrape
348
351
  (model.try(:grape_validations) || {}).with_indifferent_access[field] || {}
349
352
  end
350
353
 
351
- def generate_nested_params(dsl,action,model,fields)
354
+ def generate_nested_params(dsl, action, model, fields)
352
355
  klass = self
353
- fields.each do |r,v|
356
+ fields.each do |r, v|
354
357
  # Look at model.reflections to find the association's class name:
355
- reflection = r.to_s.sub(/_attributes$/,'') # the reflection name
356
- 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)
357
360
 
358
- if is_file_attachment?(model,r)
361
+ if file_attachment?(model, r)
359
362
  # Handle Carrierwave file upload fields
360
- s = [:filename, :type, :name, :tempfile, :head]-v
361
- if s.present?
362
- Rails.logger.warn "Missing required file upload parameters #{s} for uploader field #{r}"
363
- end
364
- 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)
365
366
  # In case you need a refresher on how these work:
366
367
  # http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
367
368
  dsl.optional r, type: Array do |dl|
368
- klass.generate_params(dl,action,relation,v)
369
- 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)
370
371
  end
371
372
  else
372
373
  # TODO: handle any remaining correctly. Presently defaults to a Hash
@@ -374,50 +375,80 @@ module IntrospectiveGrape
374
375
  # ThroughReflection, HasOneReflection,
375
376
  # HasAndBelongsToManyReflection, BelongsToReflection
376
377
  dsl.optional r, type: Hash do |dl|
377
- klass.generate_params(dl,action,relation,v)
378
- 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)
379
380
  end
380
381
  end
381
382
  end
382
383
  end
383
384
 
384
- def is_file_attachment?(model,field)
385
- 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
386
399
  (model.respond_to?(:attachment_definitions) && model.attachment_definitions[field.to_sym]) ||
387
- 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))
388
401
  end
389
402
 
390
- def param_type(model,f)
403
+ def param_type(model, field)
391
404
  # Translate from the AR type to the GrapeParam types
392
- f = f.to_s
393
- db_type = (model.try(:columns_hash)||{})[f].try(:type)
405
+ field = field.to_s
406
+ db_type = (model&.try(:columns_hash) || {})[field]&.type
394
407
 
395
408
  # Check if it's a file attachment, look for an override class from the model,
396
- # check Pg2Ruby, use the database type, or fail over to a String:
397
- ( is_file_attachment?(model,f) && Rack::Multipart::UploadedFile ) ||
398
- (model.try(:grape_param_types)||{}).with_indifferent_access[f] ||
399
- Pg2Ruby[db_type] ||
400
- begin db_type.to_s.camelize.constantize rescue nil end ||
401
- 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
415
+ end
416
+
417
+ def uploaded_file?(model, field)
418
+ file_attachment?(model, field) && Rack::Multipart::UploadedFile
402
419
  end
403
420
 
404
- def param_required?(model,f)
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)
405
434
  # Detect if the field is a required field for the create action
406
- return false if skip_presence_validations.include?(f)
435
+ return false if skip_presence_validations.include?(field)
407
436
 
408
- 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
409
438
 
410
- 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 }
411
440
  end
412
441
 
413
- def add_destroy_param(dsl,model,reflection)
414
- 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
+
415
449
  # If destruction is allowed append the _destroy field
416
- if model.nested_attributes_options[reflection.to_sym][:allow_destroy]
417
- dsl.optional '_destroy', type: Integer
418
- end
450
+ dsl.optional '_destroy', type: Integer
419
451
  end
420
-
421
452
  end
422
453
  end
423
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