introspective_grape 0.0.4 → 0.1.9
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.
- 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
|