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.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +1164 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +58 -0
- data/Gemfile +5 -3
- data/README.md +70 -17
- data/introspective_grape.gemspec +8 -8
- data/lib/.DS_Store +0 -0
- data/lib/introspective_grape/api.rb +177 -216
- data/lib/introspective_grape/camel_snake.rb +28 -59
- data/lib/introspective_grape/filters.rb +66 -0
- data/lib/introspective_grape/formatter/camel_json.rb +14 -0
- data/lib/introspective_grape/helpers.rb +63 -0
- data/lib/introspective_grape/traversal.rb +54 -0
- data/lib/introspective_grape/version.rb +1 -1
- data/lib/introspective_grape.rb +11 -0
- data/spec/.DS_Store +0 -0
- data/spec/dummy/Gemfile +5 -3
- data/spec/dummy/app/api/.DS_Store +0 -0
- data/spec/dummy/app/api/api_helpers.rb +5 -6
- data/spec/dummy/app/api/dummy/chat_api.rb +1 -2
- data/spec/dummy/app/api/dummy/company_api.rb +16 -1
- data/spec/dummy/app/api/dummy/location_api.rb +3 -3
- data/spec/dummy/app/api/dummy/project_api.rb +1 -0
- data/spec/dummy/app/api/dummy/sessions.rb +4 -8
- data/spec/dummy/app/api/dummy/user_api.rb +3 -1
- data/spec/dummy/app/api/dummy_api.rb +6 -6
- data/spec/dummy/app/api/error_handlers.rb +2 -2
- data/spec/dummy/app/models/chat_user.rb +1 -1
- data/spec/dummy/app/models/image.rb +2 -2
- data/spec/dummy/app/models/role.rb +1 -1
- data/spec/dummy/app/models/user/chatter.rb +6 -6
- data/spec/dummy/app/models/user_project_job.rb +3 -3
- data/spec/dummy/config/application.rb +1 -1
- data/spec/dummy/db/migrate/20150824215701_create_images.rb +3 -3
- data/spec/dummy/db/schema.rb +1 -1
- data/spec/models/image_spec.rb +1 -1
- data/spec/models/role_spec.rb +5 -5
- data/spec/models/user_location_spec.rb +2 -2
- data/spec/models/user_project_job_spec.rb +1 -1
- data/spec/rails_helper.rb +3 -1
- data/spec/requests/company_api_spec.rb +28 -0
- data/spec/requests/location_api_spec.rb +19 -2
- data/spec/requests/project_api_spec.rb +34 -3
- data/spec/requests/sessions_api_spec.rb +1 -1
- data/spec/requests/user_api_spec.rb +24 -3
- data/spec/support/blueprints.rb +3 -3
- data/spec/support/location_helper.rb +26 -21
- data/spec/support/request_helpers.rb +1 -3
- metadata +58 -28
- 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
|
60
|
-
@params = Hashie::Mash.new
|
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,
|
106
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
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
|
-
|
251
|
-
|
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
|
-
|
256
|
-
|
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
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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 |
|
392
|
-
klass.generate_params(
|
393
|
-
klass.add_destroy_param(
|
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 |
|
401
|
-
klass.generate_params(
|
402
|
-
klass.add_destroy_param(
|
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
|
-
|
411
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
26
|
-
#
|
27
|
-
|
28
|
-
|
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
|
-
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
25
|
+
alias_method_chain :parse_params, :camelized
|
26
|
+
end
|
64
27
|
end
|
28
|
+
doc
|
65
29
|
end
|
66
|
-
|
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
|