introspective_grape 0.0.3

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 (165) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +35 -0
  4. data/Gemfile +15 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +103 -0
  7. data/Rakefile +26 -0
  8. data/app/assets/images/introspective_grape/.keep +0 -0
  9. data/app/assets/stylesheets/introspective_grape/.keep +0 -0
  10. data/app/controllers/.keep +0 -0
  11. data/app/helpers/.keep +0 -0
  12. data/app/mailers/.keep +0 -0
  13. data/app/models/.keep +0 -0
  14. data/app/views/.keep +0 -0
  15. data/bin/rails +12 -0
  16. data/introspective_grape.gemspec +49 -0
  17. data/lib/introspective_grape/api.rb +445 -0
  18. data/lib/introspective_grape/camel_snake.rb +71 -0
  19. data/lib/introspective_grape/version.rb +3 -0
  20. data/lib/introspective_grape.rb +4 -0
  21. data/lib/tasks/introspective_grape_tasks.rake +4 -0
  22. data/spec/dummy/Gemfile +4 -0
  23. data/spec/dummy/README.rdoc +28 -0
  24. data/spec/dummy/Rakefile +6 -0
  25. data/spec/dummy/app/api/active_record_helpers.rb +17 -0
  26. data/spec/dummy/app/api/api_helpers.rb +36 -0
  27. data/spec/dummy/app/api/dummy/chat_api.rb +108 -0
  28. data/spec/dummy/app/api/dummy/company_api.rb +8 -0
  29. data/spec/dummy/app/api/dummy/entities.rb +25 -0
  30. data/spec/dummy/app/api/dummy/location_api.rb +37 -0
  31. data/spec/dummy/app/api/dummy/project_api.rb +51 -0
  32. data/spec/dummy/app/api/dummy/role_api.rb +7 -0
  33. data/spec/dummy/app/api/dummy/sessions.rb +55 -0
  34. data/spec/dummy/app/api/dummy/user_api.rb +32 -0
  35. data/spec/dummy/app/api/dummy_api.rb +57 -0
  36. data/spec/dummy/app/api/error_handlers.rb +28 -0
  37. data/spec/dummy/app/api/permissions_helper.rb +7 -0
  38. data/spec/dummy/app/assets/images/.keep +0 -0
  39. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  40. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  41. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  42. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  43. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  44. data/spec/dummy/app/mailers/.keep +0 -0
  45. data/spec/dummy/app/models/.keep +0 -0
  46. data/spec/dummy/app/models/abstract_adapter.rb +13 -0
  47. data/spec/dummy/app/models/admin_user.rb +6 -0
  48. data/spec/dummy/app/models/chat.rb +18 -0
  49. data/spec/dummy/app/models/chat_message.rb +34 -0
  50. data/spec/dummy/app/models/chat_message_user.rb +17 -0
  51. data/spec/dummy/app/models/chat_user.rb +16 -0
  52. data/spec/dummy/app/models/company.rb +14 -0
  53. data/spec/dummy/app/models/concerns/.keep +0 -0
  54. data/spec/dummy/app/models/image.rb +21 -0
  55. data/spec/dummy/app/models/job.rb +10 -0
  56. data/spec/dummy/app/models/locatable.rb +6 -0
  57. data/spec/dummy/app/models/location.rb +26 -0
  58. data/spec/dummy/app/models/location_beacon.rb +16 -0
  59. data/spec/dummy/app/models/location_gps.rb +14 -0
  60. data/spec/dummy/app/models/project.rb +20 -0
  61. data/spec/dummy/app/models/project_job.rb +7 -0
  62. data/spec/dummy/app/models/role.rb +30 -0
  63. data/spec/dummy/app/models/super_user.rb +11 -0
  64. data/spec/dummy/app/models/team.rb +9 -0
  65. data/spec/dummy/app/models/team_user.rb +13 -0
  66. data/spec/dummy/app/models/user/chatter.rb +79 -0
  67. data/spec/dummy/app/models/user.rb +84 -0
  68. data/spec/dummy/app/models/user_location.rb +28 -0
  69. data/spec/dummy/app/models/user_project_job.rb +16 -0
  70. data/spec/dummy/app/policies/application_policy.rb +47 -0
  71. data/spec/dummy/app/policies/chat_policy.rb +22 -0
  72. data/spec/dummy/app/policies/company_policy.rb +32 -0
  73. data/spec/dummy/app/policies/location_policy.rb +29 -0
  74. data/spec/dummy/app/policies/project_policy.rb +42 -0
  75. data/spec/dummy/app/policies/role_policy.rb +33 -0
  76. data/spec/dummy/app/policies/user_location_policy.rb +12 -0
  77. data/spec/dummy/app/policies/user_policy.rb +8 -0
  78. data/spec/dummy/app/views/layouts/application.html.erb +13 -0
  79. data/spec/dummy/bin/bundle +3 -0
  80. data/spec/dummy/bin/rails +4 -0
  81. data/spec/dummy/bin/rake +4 -0
  82. data/spec/dummy/bin/setup +29 -0
  83. data/spec/dummy/config/application.rb +38 -0
  84. data/spec/dummy/config/boot.rb +6 -0
  85. data/spec/dummy/config/database.yml +23 -0
  86. data/spec/dummy/config/environment.rb +11 -0
  87. data/spec/dummy/config/environments/development.rb +41 -0
  88. data/spec/dummy/config/environments/production.rb +79 -0
  89. data/spec/dummy/config/environments/test.rb +43 -0
  90. data/spec/dummy/config/initializers/assets.rb +11 -0
  91. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  92. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  93. data/spec/dummy/config/initializers/devise.rb +262 -0
  94. data/spec/dummy/config/initializers/devise_async.rb +2 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/paperclip.rb +13 -0
  99. data/spec/dummy/config/initializers/paperclip_adapter.rb +13 -0
  100. data/spec/dummy/config/initializers/session_store.rb +3 -0
  101. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  102. data/spec/dummy/config/locales/devise.en.yml +60 -0
  103. data/spec/dummy/config/locales/en.yml +23 -0
  104. data/spec/dummy/config/routes.rb +8 -0
  105. data/spec/dummy/config/secrets.yml +22 -0
  106. data/spec/dummy/config.ru +4 -0
  107. data/spec/dummy/db/migrate/20141002205024_devise_create_users.rb +42 -0
  108. data/spec/dummy/db/migrate/20141002211055_devise_create_admin_users.rb +48 -0
  109. data/spec/dummy/db/migrate/20141002211057_create_active_admin_comments.rb +19 -0
  110. data/spec/dummy/db/migrate/20141002220722_add_lockable_to_users.rb +8 -0
  111. data/spec/dummy/db/migrate/20150406213646_create_companies.rb +11 -0
  112. data/spec/dummy/db/migrate/20150414213154_add_user_authentication_token.rb +11 -0
  113. data/spec/dummy/db/migrate/20150415222005_create_roles.rb +12 -0
  114. data/spec/dummy/db/migrate/20150505181635_create_chats.rb +9 -0
  115. data/spec/dummy/db/migrate/20150505181636_create_chat_users.rb +11 -0
  116. data/spec/dummy/db/migrate/20150505181640_create_chat_messages.rb +11 -0
  117. data/spec/dummy/db/migrate/20150507191529_create_chat_message_users.rb +11 -0
  118. data/spec/dummy/db/migrate/20150601200526_create_locations.rb +12 -0
  119. data/spec/dummy/db/migrate/20150601200533_create_locatables.rb +10 -0
  120. data/spec/dummy/db/migrate/20150601212924_create_location_beacons.rb +15 -0
  121. data/spec/dummy/db/migrate/20150601213542_create_location_gps.rb +12 -0
  122. data/spec/dummy/db/migrate/20150609201823_create_user_locations.rb +14 -0
  123. data/spec/dummy/db/migrate/20150616205336_add_role_user_constraint.rb +9 -0
  124. data/spec/dummy/db/migrate/20150617232519_create_projects.rb +10 -0
  125. data/spec/dummy/db/migrate/20150617232521_create_jobs.rb +9 -0
  126. data/spec/dummy/db/migrate/20150617232522_create_project_jobs.rb +11 -0
  127. data/spec/dummy/db/migrate/20150623170133_create_user_project_jobs.rb +12 -0
  128. data/spec/dummy/db/migrate/20150701234929_create_teams.rb +11 -0
  129. data/spec/dummy/db/migrate/20150701234930_create_team_users.rb +11 -0
  130. data/spec/dummy/db/migrate/20150727214950_add_confirmable_to_devise.rb +11 -0
  131. data/spec/dummy/db/migrate/20150820190524_add_user_names.rb +6 -0
  132. data/spec/dummy/db/migrate/20150824215701_create_images.rb +15 -0
  133. data/spec/dummy/db/migrate/20150909225019_add_password_to_project.rb +5 -0
  134. data/spec/dummy/db/schema.rb +278 -0
  135. data/spec/dummy/lib/assets/.keep +0 -0
  136. data/spec/dummy/log/.keep +0 -0
  137. data/spec/dummy/public/404.html +67 -0
  138. data/spec/dummy/public/422.html +67 -0
  139. data/spec/dummy/public/500.html +66 -0
  140. data/spec/dummy/public/favicon.ico +0 -0
  141. data/spec/fixtures/images/avatar.jpeg +0 -0
  142. data/spec/fixtures/images/exif.jpeg +0 -0
  143. data/spec/models/chat_spec.rb +32 -0
  144. data/spec/models/image_spec.rb +14 -0
  145. data/spec/models/locatable_spec.rb +10 -0
  146. data/spec/models/project_spec.rb +17 -0
  147. data/spec/models/role_spec.rb +63 -0
  148. data/spec/models/team_spec.rb +17 -0
  149. data/spec/models/team_user_spec.rb +20 -0
  150. data/spec/models/user_location_spec.rb +35 -0
  151. data/spec/models/user_project_job_spec.rb +30 -0
  152. data/spec/models/user_spec.rb +125 -0
  153. data/spec/rails_helper.rb +23 -0
  154. data/spec/requests/chat_api_spec.rb +174 -0
  155. data/spec/requests/company_api_spec.rb +61 -0
  156. data/spec/requests/location_api_spec.rb +96 -0
  157. data/spec/requests/project_api_spec.rb +151 -0
  158. data/spec/requests/role_api_spec.rb +37 -0
  159. data/spec/requests/sessions_api_spec.rb +55 -0
  160. data/spec/requests/user_api_spec.rb +191 -0
  161. data/spec/support/blueprints.rb +103 -0
  162. data/spec/support/location_helper.rb +56 -0
  163. data/spec/support/pundit_helpers.rb +13 -0
  164. data/spec/support/request_helpers.rb +22 -0
  165. metadata +562 -0
@@ -0,0 +1,445 @@
1
+ require 'action_controller'
2
+
3
+ module IntrospectiveGrape
4
+ # Allow files to be uploaded through ActionController:
5
+ ActionController::Parameters::PERMITTED_SCALAR_TYPES.push Rack::Multipart::UploadedFile, ActionController::Parameters
6
+
7
+ class API < Grape::API
8
+ # Generate uniform RESTful APIs for an ActiveRecord Model:
9
+ #
10
+ # class <Some API Controller> < IntrospectiveGrape::API
11
+ # exclude_actions Model, :index,:show,:create,:update,:destroy
12
+ # default_includes Model, <associations for eager loading>
13
+ # restful <Model Class>, [<strong, param, fields>]
14
+ #
15
+ # class <Model>Entity < Grape::Entity
16
+ # expose :id, :attribute
17
+ # expose :association, using: <Association>Entity>
18
+ # end
19
+ # end
20
+ #
21
+ # To define a Grape param type for a virtual attribute or override the defaut param
22
+ # type from model introspection, define a class method in the model with the param
23
+ # types for the attributes specified in a hash:
24
+ #
25
+ # def self.attribute_param_types
26
+ # { "<attribute name>" => Virtus::Attribute::Boolean }
27
+ # end
28
+ #
29
+ # For nested models declared in Rails' strong params both the Grape params for the
30
+ # nested params as well as nested routes will be declared, allowing for
31
+ # a good deal of flexibility for API consumers out of the box, nested params for
32
+ # bulk updates and nested routes for interacting with single records.
33
+ #
34
+
35
+ 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
54
+
55
+ def inherited(child)
56
+ super(child)
57
+ child.before do
58
+ # 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!
63
+ end
64
+ end
65
+
66
+ # We will probably need before and after hooks eventually, but haven't yet...
67
+ #api_actions.each do |a|
68
+ # define_method "before_#{a}_hook" do |model,params| ; end
69
+ # define_method "after_#{a}_hook" do |model,params| ; end
70
+ #end
71
+
72
+ def restful(model, strong_params=[], routes=[])
73
+ # Recursively define endpoints for the model and any nested models.
74
+ #
75
+ # model: the model class for the API
76
+ # whitelist: a list of fields in Rail's strong params structure, also used to
77
+ # generate grape's permitted params.
78
+ # routes: An array of OpenStruct representations of a nested route's ancestors
79
+ #
80
+
81
+ # Defining the api will break pending migrations during db:migrate, so bail:
82
+ begin ActiveRecord::Migration.check_pending! rescue return end
83
+
84
+ # normalize the whitelist to symbols
85
+ strong_params.map!{|f| f.kind_of?(String) ? f.to_sym : f }
86
+ # default to a flat representation of the model's attributes if left unspecified
87
+ strong_params = strong_params.blank? ? model.attribute_names.map(&:to_sym)-[:id, :updated_at, :created_at] : strong_params
88
+
89
+ # The strong params will be the same for all routes, differing from the Grape params
90
+ # when routes are nested
91
+ whitelist = whitelist( strong_params )
92
+
93
+ # As routes are nested keep track of the routes, we are preventing siblings from
94
+ # appending to the routes array here:
95
+ routes = build_routes(routes, model)
96
+
97
+ define_routes(routes,whitelist)
98
+
99
+ resource routes.first.name.pluralize do
100
+ # yield to append additional routes under the root namespace
101
+ yield if block_given?
102
+ end
103
+ end
104
+
105
+ def define_routes(routes, whitelist)
106
+ define_endpoint(routes, whitelist)
107
+
108
+ # recursively define endpoints
109
+ model = routes.last.model || return
110
+
111
+ whitelist.select{|a| a.kind_of?(Hash) }.each do |nested|
112
+ # Recursively add RESTful nested routes for every nested model:
113
+ nested.each do |r,fields|
114
+ # Look at model.reflections to find the association's class name:
115
+ reflection_name = r.to_s.sub(/_attributes$/,'')
116
+ begin
117
+ relation = model.reflections[reflection_name].class_name.constantize
118
+ rescue
119
+ Rails.logger.fatal "Can't find associated model for #{r} on #{model}"
120
+ end
121
+
122
+ next_routes = build_routes(routes, relation, reflection_name)
123
+
124
+ define_routes(next_routes, fields)
125
+ end
126
+ end
127
+ end
128
+
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
+
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
152
+ # De-reference these as local variables from their class scope, or when we make
153
+ # calls to the API they will be whatever they were last set to by the recursive
154
+ # calls to "nest_routes".
155
+ routes = routes.clone
156
+ api_params = api_params.clone
157
+
158
+ root = routes.first
159
+ model = routes.last.model || return
160
+ name = routes.last.name.singularize
161
+ # We define the param keys for ID fields in camelcase for swagger's URL substitution,
162
+ # 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
166
+
167
+ resource namespace do
168
+
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)) )
199
+
200
+ records = records.map{|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
201
+
202
+ present records, with: "#{klass}::#{model}Entity".constantize
203
+ end
204
+ end
205
+
206
+
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
215
+ end
216
+
217
+
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
+
237
+
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?
247
+
248
+ @model.update_attributes!( safe_params(params).permit(klass.whitelist) )
249
+
250
+ present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
251
+ end
252
+ end
253
+
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
263
+ end
264
+
265
+ end
266
+ end
267
+
268
+
269
+ def whitelist(whitelist=nil)
270
+ return @whitelist if !whitelist
271
+ @whitelist = whitelist
272
+ end
273
+
274
+ def skip_presence_validations(fields=nil)
275
+ return @skip_presence_fields||[] if !fields
276
+ @skip_presence_fields = [fields].flatten
277
+ end
278
+
279
+ def build_nested_attributes(routes,hash)
280
+ # Recursively re-express the flat attributes hash from nested routes as nested
281
+ # attributes that can be used to perform an update on the root model.
282
+
283
+ # do nothing if the params are already nested.
284
+ return {} if routes.blank? || hash[routes.first.param]
285
+
286
+ route = routes.shift
287
+ # change 'x_id' to 'x_attributes': { id: id, y_attributes: {} }
288
+ id = hash.delete route.key
289
+ attributes = id ? { id: id } : {}
290
+
291
+ attributes.merge!( hash ) if routes.blank? # assign param values to the last reference
292
+
293
+ if route.many? # nest it in an array if it is a has_many association
294
+ { route.param => [attributes.merge( build_nested_attributes(routes, hash) )] }
295
+ else
296
+ { route.param => attributes.merge( build_nested_attributes(routes, hash) ) }
297
+ end
298
+ end
299
+
300
+
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
+
343
+ def generate_params(dsl, klass, action, model, fields)
344
+ # We'll be doing a recursive walk (to handle nested attributes) down the
345
+ # whitelisted params, generating the Grape param definitions by introspecting
346
+ # on the model and its associations.
347
+ raise "Invalid action: #{action}" unless [:update, :create].include?(action)
348
+ # dsl : The Grape::Validations::ParamsScope object
349
+ # klass : A reference back to the original descendant of IntrospectiveGrape::API.
350
+ # You have to pass this around to remember who you were before the DSL
351
+ # scope hijacked your identity.
352
+ # action: create or update
353
+ # model : The ActiveRecord model class
354
+ # fields: The whitelisted data structure for Rails' strong params, from which we
355
+ # infer Grape's parameters
356
+
357
+ (fields-[:id]).each do |field|
358
+ if ( field.kind_of?(Hash) )
359
+ generate_nested_params(dsl,klass,action,model,field)
360
+ 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
369
+ end
370
+ end
371
+ end
372
+
373
+ def generate_nested_params(dsl,klass,action,model,fields)
374
+ fields.each do |r,v|
375
+ # Look at model.reflections to find the association's class name:
376
+ 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
382
+
383
+ if is_file_attachment?(model,r)
384
+ # Handle Carrierwave file upload fields
385
+ if s = [:filename, :type, :name, :tempfile, :head]-v && s.present?
386
+ Rails.logger.warn "Missing required file upload parameters #{s} for uploader field #{r}"
387
+ end
388
+ elsif PLURAL_REFLECTIONS.include?( model.reflections[reflection].class )
389
+ # In case you need a refresher on how these work:
390
+ # 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
394
+ end
395
+ else
396
+ # TODO: handle any remaining correctly. Presently defaults to a Hash
397
+ # http://www.rubydoc.info/github/rails/rails/ActiveRecord/Reflection
398
+ # ThroughReflection, HasOneReflection,
399
+ # 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
403
+ end
404
+ end
405
+ end
406
+ end
407
+
408
+ def is_file_attachment?(model,field)
409
+ 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)
412
+ end
413
+
414
+ def param_type(model,f)
415
+ # Translate from the AR type to the GrapeParam types
416
+ f = f.to_s
417
+ db_type = (model.try(:columns_hash)||{})[f].try(:type)
418
+
419
+ # Look for an override class from the model, check Pg2Ruby, use the database type,
420
+ # or fail over to a String:
421
+ ( is_file_attachment?(model,f) && Rack::Multipart::UploadedFile ) ||
422
+ (model.try(:attribute_param_types)||{})[f] ||
423
+ Pg2Ruby[db_type] ||
424
+ begin db_type.to_s.camelize.constantize rescue nil end ||
425
+ String
426
+ end
427
+
428
+ def param_required?(model,f)
429
+ return false if skip_presence_validations.include? f
430
+ # Detect if the field is a required field for the create action
431
+ model.validators_on(f.to_sym).any?{|v| v.kind_of? ActiveRecord::Validations::PresenceValidator }
432
+ end
433
+
434
+ def add_destroy_param(dsl,model,reflection)
435
+ raise "#{model} does not accept nested attributes for #{reflection}" if !model.nested_attributes_options[reflection.to_sym]
436
+ # If destruction is allowed append the _destroy field
437
+ if model.nested_attributes_options[reflection.to_sym][:allow_destroy]
438
+ dsl.optional '_destroy', type: Integer
439
+ end
440
+ end
441
+
442
+ end
443
+ end
444
+
445
+ end
@@ -0,0 +1,71 @@
1
+ require 'grape-swagger'
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
24
+
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
30
+ 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)
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]')
59
+ end
60
+ parsed_params
61
+ end
62
+
63
+ alias_method_chain :parse_params, :camelized
64
+ end
65
+ end
66
+ doc
67
+ end
68
+ alias_method_chain :create_documentation_class, :camelized
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,3 @@
1
+ module IntrospectiveGrape
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,4 @@
1
+ module IntrospectiveGrape
2
+ autoload :API, 'introspective_grape/api'
3
+ autoload :CamelSnake, 'introspective_grape/camel_snake'
4
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :introspective_grape do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,4 @@
1
+
2
+ gem 'pundit'
3
+ gem 'paperclip', '3.4.2'
4
+ gem 'delayed_paperclip', git: 'https://github.com/buermann/delayed_paperclip.git'
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,17 @@
1
+ # Duck-type some helper class methods into our ActiveRecord models to
2
+ # allow us to configure API behaviors granularly, at the model level.
3
+ class ActiveRecord::Base
4
+ class << self
5
+ @@api_actions ||= [:index,:show,:create,:update,:destroy,nil]
6
+ def api_actions; @@api_actions; end
7
+
8
+ def exclude_actions(*args) # Do not define endpoints for these actions
9
+ raise "#{self.name} defines invalid exclude_actions: #{args-@@api_actions}" if (args.flatten-@@api_actions).present?
10
+ @exclude_actions = args.present? ? args.flatten : @exclude_actions || []
11
+ end
12
+
13
+ def default_includes(*args) # Eager load these associations.
14
+ @default_includes = args.present? ? args.flatten : @default_includes || []
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module ApiHelpers
2
+ include IntrospectiveGrape::CamelSnake
3
+ def warden
4
+ env['warden']
5
+ end
6
+
7
+ def current_user
8
+ warden.user || params[:api_key].present? && @user = User.find_by_authentication_token(params[:api_key])
9
+ end
10
+
11
+ def authorize!
12
+ unauthorized! unless current_user
13
+ end
14
+
15
+ # returns an 'unauthorized' response
16
+ def unauthorized!(error_type = nil)
17
+ respond_error!('unauthorized', error_type, 401)
18
+ end
19
+
20
+ # returns a error response with given type, message_key and status
21
+ def respond_error!(type, message_key, status = 500, other = {})
22
+ e = {
23
+ type: type,
24
+ status: status
25
+ }
26
+ e['message_key'] = message_key if message_key
27
+ e.merge!(other)
28
+ error!({ error: e }, status)
29
+ end
30
+
31
+ private
32
+
33
+ def safe_params(params)
34
+ ActionController::Parameters.new(params)
35
+ end
36
+ end