introspective_grape 0.0.4 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +2 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +1164 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +4 -2
  7. data/CHANGELOG.md +58 -0
  8. data/Gemfile +5 -3
  9. data/README.md +70 -17
  10. data/introspective_grape.gemspec +8 -8
  11. data/lib/.DS_Store +0 -0
  12. data/lib/introspective_grape/api.rb +177 -216
  13. data/lib/introspective_grape/camel_snake.rb +28 -59
  14. data/lib/introspective_grape/filters.rb +66 -0
  15. data/lib/introspective_grape/formatter/camel_json.rb +14 -0
  16. data/lib/introspective_grape/helpers.rb +63 -0
  17. data/lib/introspective_grape/traversal.rb +54 -0
  18. data/lib/introspective_grape/version.rb +1 -1
  19. data/lib/introspective_grape.rb +11 -0
  20. data/spec/.DS_Store +0 -0
  21. data/spec/dummy/Gemfile +5 -3
  22. data/spec/dummy/app/api/.DS_Store +0 -0
  23. data/spec/dummy/app/api/api_helpers.rb +5 -6
  24. data/spec/dummy/app/api/dummy/chat_api.rb +1 -2
  25. data/spec/dummy/app/api/dummy/company_api.rb +16 -1
  26. data/spec/dummy/app/api/dummy/location_api.rb +3 -3
  27. data/spec/dummy/app/api/dummy/project_api.rb +1 -0
  28. data/spec/dummy/app/api/dummy/sessions.rb +4 -8
  29. data/spec/dummy/app/api/dummy/user_api.rb +3 -1
  30. data/spec/dummy/app/api/dummy_api.rb +6 -6
  31. data/spec/dummy/app/api/error_handlers.rb +2 -2
  32. data/spec/dummy/app/models/chat_user.rb +1 -1
  33. data/spec/dummy/app/models/image.rb +2 -2
  34. data/spec/dummy/app/models/role.rb +1 -1
  35. data/spec/dummy/app/models/user/chatter.rb +6 -6
  36. data/spec/dummy/app/models/user_project_job.rb +3 -3
  37. data/spec/dummy/config/application.rb +1 -1
  38. data/spec/dummy/db/migrate/20150824215701_create_images.rb +3 -3
  39. data/spec/dummy/db/schema.rb +1 -1
  40. data/spec/models/image_spec.rb +1 -1
  41. data/spec/models/role_spec.rb +5 -5
  42. data/spec/models/user_location_spec.rb +2 -2
  43. data/spec/models/user_project_job_spec.rb +1 -1
  44. data/spec/rails_helper.rb +3 -1
  45. data/spec/requests/company_api_spec.rb +28 -0
  46. data/spec/requests/location_api_spec.rb +19 -2
  47. data/spec/requests/project_api_spec.rb +34 -3
  48. data/spec/requests/sessions_api_spec.rb +1 -1
  49. data/spec/requests/user_api_spec.rb +24 -3
  50. data/spec/support/blueprints.rb +3 -3
  51. data/spec/support/location_helper.rb +26 -21
  52. data/spec/support/request_helpers.rb +1 -3
  53. metadata +58 -28
  54. data/spec/dummy/app/api/active_record_helpers.rb +0 -17
@@ -1,10 +1,15 @@
1
1
  require 'action_controller'
2
+ require 'grape-kaminari'
2
3
 
3
4
  module IntrospectiveGrape
4
- # Allow files to be uploaded through ActionController:
5
- ActionController::Parameters::PERMITTED_SCALAR_TYPES.push Rack::Multipart::UploadedFile, ActionController::Parameters
6
-
7
5
  class API < Grape::API
6
+ extend IntrospectiveGrape::Helpers
7
+ extend IntrospectiveGrape::Filters
8
+ extend IntrospectiveGrape::Traversal
9
+
10
+ # Allow files to be uploaded through ActionController:
11
+ ActionController::Parameters::PERMITTED_SCALAR_TYPES.push Rack::Multipart::UploadedFile, ActionController::Parameters
12
+
8
13
  # Generate uniform RESTful APIs for an ActiveRecord Model:
9
14
  #
10
15
  # class <Some API Controller> < IntrospectiveGrape::API
@@ -33,33 +38,21 @@ module IntrospectiveGrape
33
38
  #
34
39
 
35
40
  class << self
36
- PLURAL_REFLECTIONS = [ ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasManyReflection]
37
-
38
- Pg2Ruby = {
39
- # mapping of activerecord/postgres 'type's to ruby data classes, where they differ
40
- datetime: DateTime
41
- }
42
-
43
- def exclude_actions(model, *args)
44
- @exclude_actions ||= {}
45
- @@api_actions ||= [:index,:show,:create,:update,:destroy,nil]
46
- raise "#{model.name} defines invalid exclude_actions: #{args-@@api_actions}" if (args.flatten-@@api_actions).present?
47
- @exclude_actions[model.name] = args.present? ? args.flatten : @exclude_actions[model.name] || []
48
- end
49
-
50
- def default_includes(model, *args)
51
- @default_includes ||= {}
52
- @default_includes[model.name] = args.present? ? args.flatten : @default_includes[model.name] || []
53
- end
41
+ PLURAL_REFLECTIONS = [ ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasManyReflection].freeze
42
+ # mapping of activerecord/postgres 'type's to ruby data classes, where they differ
43
+ Pg2Ruby = { datetime: DateTime }.freeze
54
44
 
55
45
  def inherited(child)
56
46
  super(child)
57
47
  child.before do
48
+ # Ensure that a user is logged in.
49
+ self.send(IntrospectiveGrape::API.authentication_method(self))
50
+ end
51
+
52
+ child.after_validation do
58
53
  # Convert incoming camel case params to snake case: grape will totally blow this
59
- # if the params hash is not a Hashie::Mash, so make it one of those:
60
- @params = Hashie::Mash.new(snake_keys(params))
61
- # ensure that a user is logged in
62
- authorize!
54
+ # if the params hash does not come back as a Hashie::Mash.
55
+ @params = (params||Hashie::Mash.new).with_snake_keys if IntrospectiveGrape.config.camelize_parameters
63
56
  end
64
57
  end
65
58
 
@@ -93,8 +86,7 @@ module IntrospectiveGrape
93
86
  # As routes are nested keep track of the routes, we are preventing siblings from
94
87
  # appending to the routes array here:
95
88
  routes = build_routes(routes, model)
96
-
97
- define_routes(routes,whitelist)
89
+ define_routes(routes, whitelist)
98
90
 
99
91
  resource routes.first.name.pluralize do
100
92
  # yield to append additional routes under the root namespace
@@ -102,13 +94,12 @@ module IntrospectiveGrape
102
94
  end
103
95
  end
104
96
 
105
- def define_routes(routes, whitelist)
106
- define_endpoint(routes, whitelist)
107
-
97
+ def define_routes(routes, api_params)
98
+ define_endpoints(routes, api_params)
108
99
  # recursively define endpoints
109
100
  model = routes.last.model || return
110
101
 
111
- whitelist.select{|a| a.kind_of?(Hash) }.each do |nested|
102
+ api_params.select{|a| a.kind_of?(Hash) }.each do |nested|
112
103
  # Recursively add RESTful nested routes for every nested model:
113
104
  nested.each do |r,fields|
114
105
  # Look at model.reflections to find the association's class name:
@@ -120,162 +111,179 @@ module IntrospectiveGrape
120
111
  end
121
112
 
122
113
  next_routes = build_routes(routes, relation, reflection_name)
123
-
124
114
  define_routes(next_routes, fields)
125
115
  end
126
116
  end
127
117
  end
128
118
 
129
- def build_routes(routes, model, reflection_name=nil)
130
- routes = routes.clone
131
- # routes: the existing routes array passed from the parent
132
- # model: the model being manipulated in this leaf
133
- # reflection_name: the association name from the original strong_params declaration
134
- #
135
- # Constructs an array representation of the route's models and their associations,
136
- # a /root/:rootId/branch/:branchId/leaf/:leafId path would have flat array like
137
- # [root,branch,leaf] representing the path structure and its models, used to
138
- # manipulate ActiveRecord relationships and params hashes and so on.
139
- parent_model = routes.last.try(:model)
140
- return routes if model == parent_model
141
119
 
142
- name = reflection_name || model.name.underscore
143
- reflection = parent_model && parent_model.reflections[reflection_name]
144
- many = parent_model && PLURAL_REFLECTIONS.include?( reflection.class ) ? true : false
145
- routes.push OpenStruct.new(name: name, param: "#{name}_attributes", model: model, many?: many, key: "#{name.singularize}_id".to_sym, swaggerKey: "#{name.singularize.camelize(:lower)}Id", reflection: reflection)
146
- end
147
-
148
- def define_endpoint(routes,api_params)
149
- # Inside the Grape DSL "self" will derefernece to its Endpoint classes,
150
- # so save a reference to our API class:
151
- klass = self
120
+ def define_endpoints(routes,api_params)
152
121
  # De-reference these as local variables from their class scope, or when we make
153
122
  # calls to the API they will be whatever they were last set to by the recursive
154
123
  # calls to "nest_routes".
155
124
  routes = routes.clone
156
125
  api_params = api_params.clone
157
126
 
158
- root = routes.first
159
127
  model = routes.last.model || return
160
- name = routes.last.name.singularize
128
+
161
129
  # We define the param keys for ID fields in camelcase for swagger's URL substitution,
162
130
  # they'll come back in snake case in the params hash, the API as a whole is agnostic:
163
- swaggerKey = routes.last.swaggerKey
164
-
165
- namespace = routes[0..-2].map{|p| "#{p.name.pluralize}/:#{p.swaggerKey}/" }.join + name.pluralize
131
+ namespace = routes[0..-2].map{|p| "#{p.name.pluralize}/:#{p.swaggerKey}/" }.join + routes.last.name.pluralize
166
132
 
167
133
  resource namespace do
134
+ convert_nested_params_hash(self, routes)
135
+ define_restful_api(self, routes, model, api_params)
136
+ end
137
+ end
168
138
 
169
- after_validation do
170
- # After Grape validates its parameters:
171
- # 1) Find the root model instance for the API if its passed (implicitly either
172
- # an update/destroy on the root node or it's a nested route
173
- # 2) For nested endpoints convert the params hash into Rails-compliant nested
174
- # attributes, to be passed to the root instance for update. This keeps
175
- # behavior consistent between bulk and single record updates.
176
- if params[root.key]
177
- default_includes = routes.size > 1 ? [] : root.model.default_includes
178
- @model = root.model.includes( default_includes ).find(params[root.key])
179
- end
180
-
181
- if routes.size > 1
182
- nested_attributes = klass.build_nested_attributes(routes[1..-1], params.except(root.key,:api_key) )
183
-
184
- @params.merge!( nested_attributes ) if nested_attributes.kind_of?(Hash)
185
- end
186
-
187
- end
188
-
189
- unless model.exclude_actions.include?(:index)
190
- desc "list #{name.pluralize}" do
191
- detail "returns list of all #{name.pluralize}"
192
- end
193
- get '/' do
194
- # Invoke the policy for the action, defined in the policy classes for the model:
195
- authorize root.model.new, :index?
196
-
197
- # Nested route indexes need to be scoped by the API's top level policy class:
198
- records = policy_scope( root.model.includes(klass.default_includes(root.model)) )
139
+ def define_restful_api(dsl, routes, model, api_params)
140
+ # declare index, show, update, create, and destroy methods:
141
+ API_ACTIONS.each do |action|
142
+ send("define_#{action}", dsl, routes, model, api_params) unless exclude_actions(model).include?(action)
143
+ end
144
+ end
199
145
 
200
- records = records.map{|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
146
+ def define_index(dsl, routes, model, api_params)
147
+ include Grape::Kaminari
148
+ root = routes.first
149
+ klass = routes.first.klass
150
+ name = routes.last.name.pluralize
151
+ simple_filters(klass, model, api_params)
201
152
 
202
- present records, with: "#{klass}::#{model}Entity".constantize
203
- end
204
- end
153
+ dsl.desc "list #{name}" do
154
+ detail "returns list of all #{name}"
155
+ end
156
+ dsl.params do
157
+ klass.declare_filter_params(self, klass, model, api_params)
158
+ end
159
+ if klass.pagination
160
+ paginate per_page: klass.pagination[:per_page]||25, max_per_page: klass.pagination[:max_per_page], offset: klass.pagination[:offset]||0
161
+ end
162
+ dsl.get '/' do
163
+ # Invoke the policy for the action, defined in the policy classes for the model:
164
+ authorize root.model.new, :index?
165
+
166
+ # Nested route indexes need to be scoped by the API's top level policy class:
167
+ records = policy_scope( root.model.includes(klass.default_includes(root.model)) )
168
+
169
+ records = klass.apply_filter_params(klass, model, api_params, params, records)
170
+ records = records.where( JSON.parse(params[:query]) ) if params[:query].present?
171
+ records = records.map{|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
172
+ # paginate the records using Kaminari
173
+ records = paginate(Kaminari.paginate_array(records)) if klass.pagination
174
+ present records, with: "#{klass}::#{model}Entity".constantize
175
+ end
176
+ end
205
177
 
178
+ def define_show(dsl, routes, model, _api_params)
179
+ name = routes.last.name.singularize
180
+ klass = routes.first.klass
181
+ dsl.desc "retrieve a #{name}" do
182
+ detail "returns details on a #{name}"
183
+ end
184
+ dsl.get ":#{routes.last.swaggerKey}" do
185
+ authorize @model, :show?
186
+ present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
187
+ end
188
+ end
206
189
 
207
- unless model.exclude_actions.include?(:show)
208
- desc "retrieve a #{name}" do
209
- detail "returns details on a #{name}"
210
- end
211
- get ":#{swaggerKey}" do
212
- authorize @model, :show?
213
- present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
214
- end
190
+ def define_create(dsl, routes, model, api_params)
191
+ name = routes.last.name.singularize
192
+ klass = routes.first.klass
193
+ root = routes.first
194
+ dsl.desc "create a #{name}" do
195
+ detail "creates a new #{name} record"
196
+ end
197
+ dsl.params do
198
+ klass.generate_params(self, klass, :create, model, api_params)
199
+ end
200
+ dsl.post do
201
+ if @model
202
+ @model.update_attributes( safe_params(params).permit(klass.whitelist) )
203
+ else
204
+ @model = root.model.new( safe_params(params).permit(klass.whitelist) )
215
205
  end
206
+ authorize @model, :create?
207
+ @model.save!
208
+ present klass.find_leaves(routes, @model.reload, params), with: "#{klass}::#{model}Entity".constantize
209
+ end
210
+ end
216
211
 
212
+ def define_update(dsl, routes, model, api_params)
213
+ klass = routes.first.klass
214
+ name = routes.last.name.singularize
215
+ dsl.desc "update a #{name}" do
216
+ detail "updates the details of a #{name}"
217
+ end
218
+ dsl.params do
219
+ klass.generate_params(self, klass, :update, model, api_params)
220
+ end
221
+ dsl.put ":#{routes.last.swaggerKey}" do
222
+ authorize @model, :update?
217
223
 
218
- unless model.exclude_actions.include?(:create)
219
- desc "create a #{name}" do
220
- detail "creates a new #{name} record"
221
- end
222
- params do
223
- klass.generate_params(self, klass, :create, model, api_params)
224
- end
225
- post do
226
- if (@model)
227
- @model.update_attributes( safe_params(params).permit(klass.whitelist) )
228
- else
229
- @model = root.model.new( safe_params(params).permit(klass.whitelist) )
230
- end
231
- authorize @model, :create?
232
- @model.save!
233
- present klass.find_leaves(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
234
- end
235
- end
236
-
224
+ @model.update_attributes!( safe_params(params).permit(klass.whitelist) )
237
225
 
238
- unless model.exclude_actions.include?(:update)
239
- desc "update a #{name}" do
240
- detail "updates the details of a #{name}"
241
- end
242
- params do
243
- klass.generate_params(self, klass, :update, model, api_params)
244
- end
245
- put ":#{swaggerKey}" do
246
- authorize @model, :update?
226
+ present klass.find_leaf(routes, @model.reload, params), with: "#{klass}::#{model}Entity".constantize
227
+ end
228
+ end
247
229
 
248
- @model.update_attributes!( safe_params(params).permit(klass.whitelist) )
230
+ def define_destroy(dsl, routes, _model, _api_params)
231
+ klass = routes.first.klass
232
+ name = routes.last.name.singularize
233
+ dsl.desc "destroy a #{name}" do
234
+ detail "destroys the details of a #{name}"
235
+ end
236
+ dsl.delete ":#{routes.last.swaggerKey}" do
237
+ authorize @model, :destroy?
238
+ present status: (klass.find_leaf(routes, @model, params).destroy ? true : false)
239
+ end
240
+ end
249
241
 
250
- present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
251
- end
242
+ def convert_nested_params_hash(dsl, routes)
243
+ root = routes.first
244
+ klass = root.klass
245
+ dsl.after_validation do
246
+ # After Grape validates its parameters:
247
+ # 1) Find the root model instance for the API if its passed (implicitly either
248
+ # an update/destroy on the root node or it's a nested route
249
+ # 2) For nested endpoints convert the params hash into Rails-compliant nested
250
+ # attributes, to be passed to the root instance for update. This keeps
251
+ # behavior consistent between bulk and single record updates.
252
+ if params[root.key]
253
+ default_includes = routes.size > 1 ? [] : klass.default_includes(root.model)
254
+ @model = root.model.includes( default_includes ).find(params[root.key])
252
255
  end
253
256
 
254
-
255
- unless model.exclude_actions.include?(:destroy)
256
- desc "destroy a #{name}" do
257
- detail "destroys the details of a #{name}"
258
- end
259
- delete ":#{swaggerKey}" do
260
- authorize @model, :destroy?
261
- present status: (klass.find_leaf(routes, @model, params).destroy ? true : false)
262
- end
257
+ if routes.size > 1
258
+ nested_attributes = klass.build_nested_attributes(routes[1..-1], params.except(root.key,:api_key) )
259
+ @params.merge!( nested_attributes ) if nested_attributes.kind_of?(Hash)
263
260
  end
264
-
265
261
  end
266
262
  end
267
263
 
268
264
 
269
- def whitelist(whitelist=nil)
270
- return @whitelist if !whitelist
271
- @whitelist = whitelist
272
- end
265
+ def build_routes(routes, model, reflection_name=nil)
266
+ routes = routes.clone
267
+ # routes: the existing routes array passed from the parent
268
+ # model: the model being manipulated in this leaf
269
+ # reflection_name: the association name from the original strong_params declaration
270
+ #
271
+ # Constructs an array representation of the route's models and their associations,
272
+ # a /root/:rootId/branch/:branchId/leaf/:leafId path would have flat array like
273
+ # [root,branch,leaf] representing the path structure and its models, used to
274
+ # manipulate ActiveRecord relationships and params hashes and so on.
275
+ parent_model = routes.last.try(:model)
276
+ return routes if model == parent_model
277
+
278
+ name = reflection_name || model.name.underscore
279
+ reflection = parent_model.try(:reflections).try(:fetch,reflection_name)
280
+ many = parent_model && PLURAL_REFLECTIONS.include?( reflection.class ) ? true : false
281
+ swaggerKey = IntrospectiveGrape.config.camelize_parameters ? "#{name.singularize.camelize(:lower)}Id" : "#{name.singularize}_id"
273
282
 
274
- def skip_presence_validations(fields=nil)
275
- return @skip_presence_fields||[] if !fields
276
- @skip_presence_fields = [fields].flatten
283
+ 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)
277
284
  end
278
285
 
286
+
279
287
  def build_nested_attributes(routes,hash)
280
288
  # Recursively re-express the flat attributes hash from nested routes as nested
281
289
  # attributes that can be used to perform an update on the root model.
@@ -298,47 +306,6 @@ module IntrospectiveGrape
298
306
  end
299
307
 
300
308
 
301
- def find_leaves(routes, record, params)
302
- # Traverse down our route and find the leaf's siblings from its parent, e.g.
303
- # project/#/teams/#/team_users ~> project.find.teams.find.team_users.
304
- # (the traversal of the intermediate nodes occurs in find_leaf())
305
- return record if routes.size < 2 # the leaf is the root
306
- if record = find_leaf(routes, record, params)
307
- assoc = routes.last
308
- if assoc.many? && leaves = record.send( assoc.reflection.name ).includes( default_includes(assoc.model) )
309
- if (leaves.map(&:class) - [routes.last.model]).size > 0
310
- raise ActiveRecord::RecordNotFound.new("Records contain the wrong models, they should all be #{routes.last.model.name}, found #{records.map(&:class).map(&:name).join(',')}")
311
- end
312
-
313
- leaves
314
- else
315
- # has_one associations don't return a CollectionProxy and so don't support
316
- # eager loading.
317
- record.send( assoc.reflection.name )
318
- end
319
- end
320
- end
321
-
322
- def find_leaf(routes, record, params)
323
- return record unless routes.size > 1
324
- # For deeply nested routes we need to search from the root of the API to the leaf
325
- # of its nested associations in order to guarantee the validity of the relationship,
326
- # the authorization on the parent model, and the sanity of passed parameters.
327
- routes[1..-1].each_with_index do |r|
328
- if record && params[r.key]
329
- if ref = r.reflection
330
- record = record.send(ref.name).where( id: params[r.key] ).first
331
- end
332
- end
333
- end
334
-
335
- if params[routes.last.key] && record.class != routes.last.model
336
- raise ActiveRecord::RecordNotFound.new("No #{routes.last.model.name} with ID '#{params[routes.last.key]}'")
337
- end
338
-
339
- record
340
- end
341
-
342
309
 
343
310
  def generate_params(dsl, klass, action, model, fields)
344
311
  # We'll be doing a recursive walk (to handle nested attributes) down the
@@ -355,17 +322,15 @@ module IntrospectiveGrape
355
322
  # infer Grape's parameters
356
323
 
357
324
  (fields-[:id]).each do |field|
358
- if ( field.kind_of?(Hash) )
325
+ if field.kind_of?(Hash)
359
326
  generate_nested_params(dsl,klass,action,model,field)
327
+ elsif (action==:create && klass.param_required?(model,field) )
328
+ # All params are optional on an update, only require them during creation.
329
+ # Updating a record with new child models will have to rely on ActiveRecord
330
+ # validations:
331
+ dsl.requires field, type: klass.param_type(model,field)
360
332
  else
361
- if (action==:create && klass.param_required?(model,field) )
362
- # All params are optional on an update, only require them during creation.
363
- # Updating a record with new child models will have to rely on ActiveRecord
364
- # validations:
365
- dsl.requires field, type: klass.param_type(model,field)
366
- else
367
- dsl.optional field, type: klass.param_type(model,field)
368
- end
333
+ dsl.optional field, type: klass.param_type(model,field)
369
334
  end
370
335
  end
371
336
  end
@@ -374,32 +339,29 @@ module IntrospectiveGrape
374
339
  fields.each do |r,v|
375
340
  # Look at model.reflections to find the association's class name:
376
341
  reflection = r.to_s.sub(/_attributes$/,'') # the reflection name
377
- relation = begin
378
- model.reflections[reflection].class_name.constantize # its class
379
- rescue
380
- model
381
- end
342
+ relation = begin model.reflections[reflection].class_name.constantize rescue model end
382
343
 
383
344
  if is_file_attachment?(model,r)
384
345
  # Handle Carrierwave file upload fields
385
- if s = [:filename, :type, :name, :tempfile, :head]-v && s.present?
346
+ s = [:filename, :type, :name, :tempfile, :head]-v
347
+ if s.present?
386
348
  Rails.logger.warn "Missing required file upload parameters #{s} for uploader field #{r}"
387
349
  end
388
350
  elsif PLURAL_REFLECTIONS.include?( model.reflections[reflection].class )
389
351
  # In case you need a refresher on how these work:
390
352
  # http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
391
- dsl.optional r, type: Array do |dsl|
392
- klass.generate_params(dsl,klass,action,relation,v)
393
- klass.add_destroy_param(dsl,model,reflection) unless action==:create
353
+ dsl.optional r, type: Array do |dl|
354
+ klass.generate_params(dl,klass,action,relation,v)
355
+ klass.add_destroy_param(dl,model,reflection) unless action==:create
394
356
  end
395
357
  else
396
358
  # TODO: handle any remaining correctly. Presently defaults to a Hash
397
359
  # http://www.rubydoc.info/github/rails/rails/ActiveRecord/Reflection
398
360
  # ThroughReflection, HasOneReflection,
399
361
  # HasAndBelongsToManyReflection, BelongsToReflection
400
- dsl.optional r, type: Hash do |dsl|
401
- klass.generate_params(dsl,klass,action,relation,v)
402
- klass.add_destroy_param(dsl,model,reflection) unless action==:create
362
+ dsl.optional r, type: Hash do |dl|
363
+ klass.generate_params(dl,klass,action,relation,v)
364
+ klass.add_destroy_param(dl,model,reflection) unless action==:create
403
365
  end
404
366
  end
405
367
  end
@@ -407,8 +369,8 @@ module IntrospectiveGrape
407
369
 
408
370
  def is_file_attachment?(model,field)
409
371
  model.try(:uploaders) && model.uploaders[field.to_sym] || # carrierwave
410
- model.try(:paperclip_definitions) && model.paperclip_definitions[field.to_sym] || # paperclip
411
- model.send(:new).try(field).kind_of?(Paperclip::Attachment)
372
+ (model.try(:attachment_definitions) && model.attachment_definitions[field.to_sym]) ||
373
+ defined?(Paperclip::Attachment) && model.send(:new).try(field).kind_of?(Paperclip::Attachment)
412
374
  end
413
375
 
414
376
  def param_type(model,f)
@@ -416,8 +378,8 @@ module IntrospectiveGrape
416
378
  f = f.to_s
417
379
  db_type = (model.try(:columns_hash)||{})[f].try(:type)
418
380
 
419
- # Look for an override class from the model, check Pg2Ruby, use the database type,
420
- # or fail over to a String:
381
+ # Check if it's a file attachment, look for an override class from the model,
382
+ # check Pg2Ruby, use the database type, or fail over to a String:
421
383
  ( is_file_attachment?(model,f) && Rack::Multipart::UploadedFile ) ||
422
384
  (model.try(:attribute_param_types)||{})[f] ||
423
385
  Pg2Ruby[db_type] ||
@@ -441,5 +403,4 @@ module IntrospectiveGrape
441
403
 
442
404
  end
443
405
  end
444
-
445
406
  end
@@ -1,71 +1,40 @@
1
1
  require 'grape-swagger'
2
2
  require 'active_support' #/core_ext/module/aliasing'
3
- module IntrospectiveGrape::CamelSnake
4
- def snake_keys(data)
5
- if data.kind_of? Array
6
- data.map { |v| snake_keys(v) }
7
- elsif data.kind_of? Hash
8
- Hash[data.map {|k, v| [k.to_s.underscore, snake_keys(v)] }]
9
- else
10
- data
11
- end
12
- end
13
-
14
- def camel_keys(data)
15
- if data.kind_of? Array
16
- data.map { |v| camel_keys(v) }
17
- elsif data.kind_of?(Hash)
18
- Hash[data.map {|k, v| [k.to_s.camelize(:lower), camel_keys(v)] }]
19
- else
20
- data
21
- end
22
- end
23
- end
3
+ require 'camel_snake_keys'
24
4
 
25
- # Duck type Grape's built in JSON formatter to convert all snake case hash keys
26
- # to camel case.
27
- module Grape
28
- module Formatter
29
- module Json
5
+ if IntrospectiveGrape.config.camelize_parameters
6
+ # Camelize the parameters in the swagger documentation.
7
+ if Gem::Version.new( GrapeSwagger::VERSION ) <= Gem::Version.new('0.11.0')
8
+ Grape::API.class_eval do
30
9
  class << self
31
- include IntrospectiveGrape::CamelSnake
32
- def call(object, env)
33
- if object.respond_to?(:to_json)
34
- camel_keys(JSON.parse(object.to_json)).to_json
35
- else
36
- camel_keys(MultiJson.dump(object)).to_json
37
- end
38
- end
39
- end
40
- end
41
- end
42
- end
43
-
44
- # Camelize the parameters in the swagger documentation.
45
- module Grape
46
- class API
47
- class << self
48
- private
49
- def create_documentation_class_with_camelized
50
- doc = create_documentation_class_without_camelized
51
- doc.class_eval do
52
- class << self
53
- def parse_params_with_camelized(params, path, method, options = {})
54
- parsed_params = parse_params_without_camelized(params, path, method)
55
- parsed_params.each_with_index do |param|
56
- param[:name] = param[:name]
57
- .camelize(:lower)
58
- .gsub(/\[Destroy\]/,'[_destroy]')
10
+ private
11
+ def create_documentation_class_with_camelized
12
+ doc = create_documentation_class_without_camelized
13
+ doc.class_eval do
14
+ class << self
15
+ def parse_params_with_camelized(params, path, method, _options = {})
16
+ parsed_params = parse_params_without_camelized(params, path, method)
17
+ parsed_params.each_with_index do |param|
18
+ param[:name] = param[:name]
19
+ .camelize(:lower)
20
+ .gsub(/Destroy/,'_destroy')
21
+ end
22
+ parsed_params
59
23
  end
60
- parsed_params
61
- end
62
24
 
63
- alias_method_chain :parse_params, :camelized
25
+ alias_method_chain :parse_params, :camelized
26
+ end
64
27
  end
28
+ doc
65
29
  end
66
- doc
30
+ alias_method_chain :create_documentation_class, :camelized
67
31
  end
68
- alias_method_chain :create_documentation_class, :camelized
69
32
  end
33
+ else Gem::Version.new( GrapeSwagger::VERSION ) > Gem::Version.new('0.11.0')
34
+ # Grape::Swagger 0.20.xx is not yet compatible with Grape >0.14 and will alter
35
+ # the way it parses params, so will not be compatible with introspective_grape,
36
+ # and produces swagger docs for SwaggerUI 2.1.4 that don't appear to be
37
+ # backwards compatible swagger.js 2.0.41, so this is pending.
70
38
  end
39
+
71
40
  end