introspective_grape 0.4.3 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +23 -0
- data/.github/workflows/security.yml +32 -0
- data/.github/workflows/test.yml +18 -0
- data/.rubocop.yml +84 -1173
- data/.ruby-version +1 -1
- data/CHANGELOG.md +35 -0
- data/Gemfile +3 -5
- data/README.md +94 -63
- data/Rakefile +1 -6
- data/introspective_grape.gemspec +63 -51
- data/lib/introspective_grape/api.rb +167 -136
- data/lib/introspective_grape/camel_snake.rb +5 -3
- data/lib/introspective_grape/create_helpers.rb +3 -5
- data/lib/introspective_grape/doc.rb +19 -5
- data/lib/introspective_grape/filters.rb +98 -83
- data/lib/introspective_grape/formatter/camel_json.rb +2 -3
- data/lib/introspective_grape/helpers.rb +55 -48
- data/lib/introspective_grape/snake_params.rb +1 -2
- data/lib/introspective_grape/traversal.rb +56 -54
- data/lib/introspective_grape/validators.rb +23 -23
- data/lib/introspective_grape/version.rb +3 -1
- data/spec/dummy/.ruby-version +1 -0
- data/spec/dummy/Gemfile +5 -4
- data/spec/dummy/app/api/api_helpers.rb +1 -1
- data/spec/dummy/app/api/dummy/company_api.rb +10 -1
- data/spec/dummy/app/api/dummy/project_api.rb +1 -0
- data/spec/dummy/app/api/dummy/sessions.rb +1 -1
- data/spec/dummy/app/api/dummy_api.rb +8 -2
- data/spec/dummy/app/assets/config/manifest.js +4 -0
- data/spec/dummy/app/models/user.rb +1 -1
- data/spec/dummy/config/database.yml +1 -1
- data/spec/rails_helper.rb +1 -1
- data/spec/requests/company_api_spec.rb +9 -0
- metadata +150 -42
- data/.coveralls.yml +0 -2
- data/.travis.yml +0 -40
- data/bin/rails +0 -12
- data/gemfiles/Gemfile.rails.5.0.0 +0 -14
- data/gemfiles/Gemfile.rails.5.0.1 +0 -14
- data/gemfiles/Gemfile.rails.5.1.0 +0 -14
- data/gemfiles/Gemfile.rails.5.2.0 +0 -14
- data/gemfiles/Gemfile.rails.master +0 -14
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'action_controller'
|
2
2
|
require 'kaminari'
|
3
|
+
#require 'byebug'
|
3
4
|
require 'grape-kaminari'
|
4
5
|
require 'introspective_grape/validators'
|
5
6
|
|
@@ -7,13 +8,16 @@ class IntrospectiveGrapeError < StandardError
|
|
7
8
|
end
|
8
9
|
|
9
10
|
module IntrospectiveGrape
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
10
12
|
class API < Grape::API::Instance
|
13
|
+
# rubocop:enable Metrics/ClassLength
|
11
14
|
extend IntrospectiveGrape::Helpers
|
12
15
|
extend IntrospectiveGrape::CreateHelpers
|
13
16
|
extend IntrospectiveGrape::Filters
|
14
17
|
extend IntrospectiveGrape::Traversal
|
15
18
|
extend IntrospectiveGrape::Doc
|
16
19
|
extend IntrospectiveGrape::SnakeParams
|
20
|
+
include Grape::Kaminari
|
17
21
|
|
18
22
|
# Allow files to be uploaded through ActionController:
|
19
23
|
ActionController::Parameters::PERMITTED_SCALAR_TYPES.push Rack::Multipart::UploadedFile, ActionController::Parameters
|
@@ -36,7 +40,7 @@ module IntrospectiveGrape
|
|
36
40
|
# types for the attributes specified in a hash:
|
37
41
|
#
|
38
42
|
# def self.grape_param_types
|
39
|
-
# { "<attribute name>" =>
|
43
|
+
# { "<attribute name>" => Grape::API::Boolean,
|
40
44
|
# "<attribute name>" => Integer,
|
41
45
|
# "<attribute name>" => String }
|
42
46
|
# end
|
@@ -48,30 +52,30 @@ module IntrospectiveGrape
|
|
48
52
|
#
|
49
53
|
|
50
54
|
class << self
|
51
|
-
PLURAL_REFLECTIONS = [
|
55
|
+
PLURAL_REFLECTIONS = [ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasManyReflection].freeze
|
52
56
|
# mapping of activerecord/postgres 'type's to ruby data classes, where they differ
|
53
|
-
|
57
|
+
PG2RUBY = { datetime: DateTime }.freeze
|
54
58
|
|
55
59
|
def inherited(child)
|
56
60
|
super(child)
|
57
61
|
child.before do
|
58
62
|
# Ensure that a user is logged in.
|
59
|
-
|
63
|
+
send(IntrospectiveGrape::API.authentication_method(self))
|
60
64
|
end
|
61
65
|
|
62
66
|
child.snake_params_before_validation if IntrospectiveGrape.config.camelize_parameters
|
63
67
|
end
|
64
68
|
|
65
69
|
# We will probably need before and after hooks eventually, but haven't yet...
|
66
|
-
#api_actions.each do |a|
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#end
|
70
|
+
# api_actions.each do |a|
|
71
|
+
# define_method "before_#{a}_hook" do |model, params| ; end
|
72
|
+
# define_method "after_#{a}_hook" do |model, params| ; end
|
73
|
+
# end
|
70
74
|
|
75
|
+
# rubocop:disable Metrics/AbcSize
|
71
76
|
def restful(model, strong_params=[], routes=[])
|
72
|
-
if model.respond_to?(:attribute_param_types)
|
73
|
-
|
74
|
-
end
|
77
|
+
raise IntrospectiveGrapeError.new("#{model.name}'s attribute_param_types class method needs to be changed to grape_param_types") if model.respond_to?(:attribute_param_types)
|
78
|
+
|
75
79
|
# Recursively define endpoints for the model and any nested models.
|
76
80
|
#
|
77
81
|
# model: the model class for the API
|
@@ -84,9 +88,9 @@ module IntrospectiveGrape
|
|
84
88
|
begin ActiveRecord::Migration.check_pending! rescue return end
|
85
89
|
|
86
90
|
# normalize the whitelist to symbols
|
87
|
-
strong_params.map!{|f| f.
|
91
|
+
strong_params.map! {|f| f.is_a?(String) ? f.to_sym : f }
|
88
92
|
# default to a flat representation of the model's attributes if left unspecified
|
89
|
-
strong_params = strong_params.blank? ? model.attribute_names.map(&:to_sym)-
|
93
|
+
strong_params = strong_params.blank? ? model.attribute_names.map(&:to_sym) - %i(id updated_at created_at) : strong_params
|
90
94
|
|
91
95
|
# The strong params will be the same for all routes, differing from the Grape params
|
92
96
|
# when routes are nested
|
@@ -109,14 +113,14 @@ module IntrospectiveGrape
|
|
109
113
|
# recursively define endpoints
|
110
114
|
model = routes.last.model || return
|
111
115
|
|
112
|
-
api_params.select{|a| a.
|
116
|
+
api_params.select {|a| a.is_a?(Hash) }.each do |nested|
|
113
117
|
# Recursively add RESTful nested routes for every nested model:
|
114
|
-
nested.each do |r,fields|
|
118
|
+
nested.each do |r, fields|
|
115
119
|
# Look at model.reflections to find the association's class name:
|
116
|
-
reflection_name = r.to_s.sub(/_attributes$/,'')
|
120
|
+
reflection_name = r.to_s.sub(/_attributes$/, '')
|
117
121
|
begin
|
118
122
|
relation = model.reflections[reflection_name].class_name.constantize
|
119
|
-
rescue
|
123
|
+
rescue StandardError
|
120
124
|
Rails.logger.fatal "Can't find associated model for #{r} on #{model}"
|
121
125
|
end
|
122
126
|
|
@@ -126,69 +130,18 @@ module IntrospectiveGrape
|
|
126
130
|
end
|
127
131
|
end
|
128
132
|
|
129
|
-
def convert_nested_params_hash(dsl, routes)
|
130
|
-
root = routes.first
|
131
|
-
klass = root.klass
|
132
|
-
dsl.after_validation do
|
133
|
-
# After Grape validates its parameters:
|
134
|
-
# 1) Find the root model instance for the API if its passed (implicitly either
|
135
|
-
# an update/destroy on the root node or it's a nested route
|
136
|
-
# 2) For nested endpoints convert the params hash into Rails-compliant nested
|
137
|
-
# attributes, to be passed to the root instance for update. This keeps
|
138
|
-
# behavior consistent between bulk and single record updates.
|
139
|
-
|
140
|
-
if params[root.key]
|
141
|
-
@model = root.model.includes( klass.default_includes(root.model) ).find(params[root.key])
|
142
|
-
end
|
143
|
-
|
144
|
-
if routes.size > 1
|
145
|
-
nested_attributes = klass.build_nested_attributes(routes[1..-1], params.except(root.key,:api_key) )
|
146
|
-
@params.merge!( nested_attributes ) if nested_attributes.kind_of?(Hash)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def define_restful_api(dsl, routes, model, api_params)
|
152
|
-
# declare index, show, update, create, and destroy methods:
|
153
|
-
API_ACTIONS.each do |action|
|
154
|
-
send("define_#{action}", dsl, routes, model, api_params) unless exclude_actions(model).include?(action)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def define_endpoints(routes,api_params)
|
159
|
-
# De-reference these as local variables from their class scope, or when we make
|
160
|
-
# calls to the API they will be whatever they were last set to by the recursive
|
161
|
-
# calls to "nest_routes".
|
162
|
-
routes = routes.clone
|
163
|
-
api_params = api_params.clone
|
164
|
-
|
165
|
-
model = routes.last.model || return
|
166
|
-
|
167
|
-
# We define the param keys for ID fields in camelcase for swagger's URL substitution,
|
168
|
-
# they'll come back in snake case in the params hash, the API as a whole is agnostic:
|
169
|
-
namespace = routes[0..-2].map{|p| "#{p.name.pluralize}/:#{p.swaggerKey}/" }.join + routes.last.name.pluralize
|
170
|
-
|
171
|
-
resource namespace do
|
172
|
-
convert_nested_params_hash(self, routes)
|
173
|
-
define_restful_api(self, routes, model, api_params)
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
133
|
def define_index(dsl, routes, model, api_params)
|
178
|
-
include Grape::Kaminari
|
179
134
|
root = routes.first
|
180
135
|
klass = routes.first.klass
|
181
136
|
name = routes.last.name.pluralize
|
182
137
|
simple_filters(klass, model, api_params)
|
183
138
|
|
184
139
|
dsl.desc "list #{name}" do
|
185
|
-
detail klass.index_documentation
|
140
|
+
detail klass.index_documentation(name)
|
186
141
|
end
|
187
142
|
dsl.params do
|
188
143
|
klass.declare_filter_params(self, klass, model, api_params)
|
189
|
-
|
190
|
-
if klass.pagination
|
191
|
-
paginate per_page: klass.pagination[:per_page]||25, max_per_page: klass.pagination[:max_per_page], offset: klass.pagination[:offset]||0
|
144
|
+
use :pagination, per_page: klass.pagination[:per_page]||25, max_per_page: klass.pagination[:max_per_page], offset: klass.pagination[:offset]||0 if klass.pagination
|
192
145
|
end
|
193
146
|
dsl.get '/' do
|
194
147
|
# Invoke the policy for the action, defined in the policy classes for the model:
|
@@ -196,10 +149,8 @@ module IntrospectiveGrape
|
|
196
149
|
|
197
150
|
# Nested route indexes need to be scoped by the API's top level policy class:
|
198
151
|
records = policy_scope( root.model.includes(klass.default_includes(root.model)) )
|
199
|
-
|
200
152
|
records = klass.apply_filter_params(klass, model, api_params, params, records)
|
201
|
-
|
202
|
-
records = records.map{|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
|
153
|
+
records = records.map {|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
|
203
154
|
|
204
155
|
# paginate the records using Kaminari
|
205
156
|
records = paginate(Kaminari.paginate_array(records)) if klass.pagination
|
@@ -208,12 +159,12 @@ module IntrospectiveGrape
|
|
208
159
|
end
|
209
160
|
|
210
161
|
def define_show(dsl, routes, model, _api_params)
|
211
|
-
name
|
162
|
+
name = routes.last.name.singularize
|
212
163
|
klass = routes.first.klass
|
213
164
|
dsl.desc "retrieve a #{name}" do
|
214
|
-
detail klass.show_documentation
|
165
|
+
detail klass.show_documentation(name)
|
215
166
|
end
|
216
|
-
dsl.get ":#{routes.last.
|
167
|
+
dsl.get ":#{routes.last.swagger_key}" do
|
217
168
|
authorize @model, :show?
|
218
169
|
present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
|
219
170
|
end
|
@@ -223,7 +174,7 @@ module IntrospectiveGrape
|
|
223
174
|
name = routes.last.name.singularize
|
224
175
|
klass = routes.first.klass
|
225
176
|
dsl.desc "create a #{name}" do
|
226
|
-
detail klass.create_documentation
|
177
|
+
detail klass.create_documentation(name)
|
227
178
|
end
|
228
179
|
dsl.params do
|
229
180
|
klass.generate_params(self, :create, model, api_params, true)
|
@@ -236,18 +187,18 @@ module IntrospectiveGrape
|
|
236
187
|
|
237
188
|
def define_update(dsl, routes, model, api_params)
|
238
189
|
klass = routes.first.klass
|
239
|
-
name
|
190
|
+
name = routes.last.name.singularize
|
240
191
|
dsl.desc "update a #{name}" do
|
241
|
-
detail klass.update_documentation
|
192
|
+
detail klass.update_documentation(name)
|
242
193
|
end
|
243
194
|
dsl.params do
|
244
195
|
klass.generate_params(self, :update, model, api_params, true)
|
245
196
|
end
|
246
|
-
dsl.put ":#{routes.last.
|
197
|
+
dsl.put ":#{routes.last.swagger_key}" do
|
247
198
|
authorize @model, :update?
|
248
199
|
|
249
200
|
@model.update_attributes!( safe_params(params).permit(klass.whitelist) )
|
250
|
-
|
201
|
+
|
251
202
|
if IntrospectiveGrape.config.skip_object_reload
|
252
203
|
present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
|
253
204
|
else
|
@@ -257,18 +208,66 @@ module IntrospectiveGrape
|
|
257
208
|
end
|
258
209
|
end
|
259
210
|
|
211
|
+
# rubocop:enable Metrics/AbcSize
|
260
212
|
def define_destroy(dsl, routes, _model, _api_params)
|
261
213
|
klass = routes.first.klass
|
262
214
|
name = routes.last.name.singularize
|
263
215
|
dsl.desc "destroy a #{name}" do
|
264
|
-
detail klass.destroy_documentation
|
216
|
+
detail klass.destroy_documentation(name)
|
265
217
|
end
|
266
|
-
dsl.delete ":#{routes.last.
|
218
|
+
dsl.delete ":#{routes.last.swagger_key}" do
|
267
219
|
authorize @model, :destroy?
|
268
220
|
present status: (klass.find_leaf(routes, @model, params).destroy ? true : false)
|
269
221
|
end
|
270
222
|
end
|
271
223
|
|
224
|
+
def convert_nested_params_hash(dsl, routes)
|
225
|
+
root = routes.first
|
226
|
+
klass = self
|
227
|
+
dsl.after_validation do
|
228
|
+
next unless params[root.key] # there was no one, nothing to see
|
229
|
+
|
230
|
+
# After Grape validates its parameters:
|
231
|
+
# 1) Find the root model instance for the API if its passed (implicitly either
|
232
|
+
# an update/destroy on the root node or it's a nested route
|
233
|
+
# 2) For nested endpoints convert the params hash into Rails-compliant nested
|
234
|
+
# attributes, to be passed to the root instance for update. This keeps
|
235
|
+
# behavior consistent between bulk and single record updates.
|
236
|
+
@model = root.model.includes( root.klass.default_includes(root.model) ).find(params[root.key])
|
237
|
+
@params.merge!( klass.merge_nested_params(routes, params) )
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def merge_nested_params(routes, params)
|
242
|
+
attr = params.reject {|k| [routes.first.key, :api_key].include?(k) }
|
243
|
+
build_nested_attributes(routes[1..-1], attr)
|
244
|
+
end
|
245
|
+
|
246
|
+
def define_restful_api(dsl, routes, model, api_params)
|
247
|
+
# declare index, show, update, create, and destroy methods:
|
248
|
+
API_ACTIONS.each do |action|
|
249
|
+
send("define_#{action}", dsl, routes, model, api_params) unless exclude_actions(model).include?(action)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def define_endpoints(routes, api_params)
|
254
|
+
# De-reference these as local variables from their class scope, or when we make
|
255
|
+
# calls to the API they will be whatever they were last set to by the recursive
|
256
|
+
# calls to "nest_routes".
|
257
|
+
routes = routes.clone
|
258
|
+
api_params = api_params.clone
|
259
|
+
model = routes.last.model || return
|
260
|
+
|
261
|
+
# We define the param keys for ID fields in camelcase for swagger's URL substitution,
|
262
|
+
# they'll come back in snake case in the params hash, the API as a whole is agnostic:
|
263
|
+
namespace = routes[0..-2].map {|p| "#{p.name.pluralize}/:#{p.swagger_key}/" }.join + routes.last.name.pluralize
|
264
|
+
|
265
|
+
klass = self # the 'resource' block changes the context to the Grape::API::Instance...
|
266
|
+
resource namespace do
|
267
|
+
klass.convert_nested_params_hash(self, routes)
|
268
|
+
klass.define_restful_api(self, routes, model, api_params)
|
269
|
+
end
|
270
|
+
end
|
272
271
|
|
273
272
|
def build_routes(routes, model, reflection_name=nil)
|
274
273
|
routes = routes.clone
|
@@ -278,21 +277,26 @@ module IntrospectiveGrape
|
|
278
277
|
#
|
279
278
|
# Constructs an array representation of the route's models and their associations,
|
280
279
|
# a /root/:rootId/branch/:branchId/leaf/:leafId path would have flat array like
|
281
|
-
# [root,branch,leaf] representing the path structure and its models, used to
|
280
|
+
# [root, branch, leaf] representing the path structure and its models, used to
|
282
281
|
# manipulate ActiveRecord relationships and params hashes and so on.
|
283
|
-
parent_model = routes.last
|
282
|
+
parent_model = routes.last&.model
|
284
283
|
return routes if model == parent_model
|
285
284
|
|
286
|
-
name
|
287
|
-
reflection
|
288
|
-
|
289
|
-
swaggerKey = IntrospectiveGrape.config.camelize_parameters ? "#{name.singularize.camelize(:lower)}Id" : "#{name.singularize}_id"
|
285
|
+
name = reflection_name || model.name.underscore
|
286
|
+
reflection = parent_model&.reflections&.fetch(reflection_name)
|
287
|
+
swagger_key = IntrospectiveGrape.config.camelize_parameters ? "#{name.singularize.camelize(:lower)}Id" : "#{name.singularize}_id"
|
290
288
|
|
291
|
-
routes.push OpenStruct.new(klass: self, name: name, param: "#{name}_attributes", model: model,
|
289
|
+
routes.push OpenStruct.new(klass: self, name: name, param: "#{name}_attributes", model: model,
|
290
|
+
many?: plural?(parent_model, reflection),
|
291
|
+
key: "#{name.singularize}_id".to_sym,
|
292
|
+
swagger_key: swagger_key, reflection: reflection)
|
292
293
|
end
|
293
294
|
|
295
|
+
def plural?(model, reflection)
|
296
|
+
(model && PLURAL_REFLECTIONS.include?(reflection.class))
|
297
|
+
end
|
294
298
|
|
295
|
-
def build_nested_attributes(routes,hash)
|
299
|
+
def build_nested_attributes(routes, hash)
|
296
300
|
# Recursively re-express the flat attributes hash from nested routes as nested
|
297
301
|
# attributes that can be used to perform an update on the root model.
|
298
302
|
|
@@ -313,13 +317,12 @@ module IntrospectiveGrape
|
|
313
317
|
end
|
314
318
|
end
|
315
319
|
|
316
|
-
|
317
|
-
|
318
320
|
def generate_params(dsl, action, model, fields, is_root_endpoint=false)
|
319
321
|
# We'll be doing a recursive walk (to handle nested attributes) down the
|
320
322
|
# whitelisted params, generating the Grape param definitions by introspecting
|
321
323
|
# on the model and its associations.
|
322
|
-
raise "Invalid action: #{action}" unless
|
324
|
+
raise "Invalid action: #{action}" unless %i(update create).include?(action)
|
325
|
+
|
323
326
|
# dsl : The Grape::Validations::ParamsScope object
|
324
327
|
# action: create or update
|
325
328
|
# model : The ActiveRecord model class
|
@@ -330,16 +333,16 @@ module IntrospectiveGrape
|
|
330
333
|
fields -= [:id] if is_root_endpoint
|
331
334
|
|
332
335
|
fields.each do |field|
|
333
|
-
if field.
|
334
|
-
generate_nested_params(dsl,action,model,field)
|
335
|
-
elsif
|
336
|
+
if field.is_a?(Hash)
|
337
|
+
generate_nested_params(dsl, action, model, field)
|
338
|
+
elsif action == :create && param_required?(model, field)
|
336
339
|
# All params are optional on an update, only require them during creation.
|
337
340
|
# Updating a record with new child models will have to rely on ActiveRecord
|
338
341
|
# validations:
|
339
|
-
dsl.requires field, { type: param_type(model,field) }.merge( validations(model, field) )
|
342
|
+
dsl.requires field, { type: param_type(model, field) }.merge( validations(model, field) )
|
340
343
|
else
|
341
|
-
#dsl.optional field, *options
|
342
|
-
dsl.optional field, { type: param_type(model,field) }.merge( validations(model, field) )
|
344
|
+
# dsl.optional field, *options
|
345
|
+
dsl.optional field, { type: param_type(model, field) }.merge( validations(model, field) )
|
343
346
|
end
|
344
347
|
end
|
345
348
|
end
|
@@ -348,25 +351,23 @@ module IntrospectiveGrape
|
|
348
351
|
(model.try(:grape_validations) || {}).with_indifferent_access[field] || {}
|
349
352
|
end
|
350
353
|
|
351
|
-
def generate_nested_params(dsl,action,model,fields)
|
354
|
+
def generate_nested_params(dsl, action, model, fields)
|
352
355
|
klass = self
|
353
|
-
fields.each do |r,v|
|
356
|
+
fields.each do |r, v|
|
354
357
|
# Look at model.reflections to find the association's class name:
|
355
|
-
reflection = r.to_s.sub(/_attributes$/,'') # the reflection name
|
356
|
-
relation
|
358
|
+
reflection = r.to_s.sub(/_attributes$/, '') # the reflection name
|
359
|
+
relation = find_relation(model, reflection)
|
357
360
|
|
358
|
-
if
|
361
|
+
if file_attachment?(model, r)
|
359
362
|
# Handle Carrierwave file upload fields
|
360
|
-
s =
|
361
|
-
if s.present?
|
362
|
-
|
363
|
-
end
|
364
|
-
elsif PLURAL_REFLECTIONS.include?( model.reflections[reflection].class )
|
363
|
+
s = %i(filename type name tempfile head) - v
|
364
|
+
Rails.logger.warn "Missing required file upload parameters #{s} for uploader field #{r}" if s.present?
|
365
|
+
elsif plural_reflection?(model, reflection)
|
365
366
|
# In case you need a refresher on how these work:
|
366
367
|
# http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
|
367
368
|
dsl.optional r, type: Array do |dl|
|
368
|
-
klass.generate_params(dl,action,relation,v)
|
369
|
-
klass.add_destroy_param(dl,model,reflection
|
369
|
+
klass.generate_params(dl, action, relation, v)
|
370
|
+
klass.add_destroy_param(dl, model, reflection, action)
|
370
371
|
end
|
371
372
|
else
|
372
373
|
# TODO: handle any remaining correctly. Presently defaults to a Hash
|
@@ -374,50 +375,80 @@ module IntrospectiveGrape
|
|
374
375
|
# ThroughReflection, HasOneReflection,
|
375
376
|
# HasAndBelongsToManyReflection, BelongsToReflection
|
376
377
|
dsl.optional r, type: Hash do |dl|
|
377
|
-
klass.generate_params(dl,action,relation,v)
|
378
|
-
klass.add_destroy_param(dl,model,reflection
|
378
|
+
klass.generate_params(dl, action, relation, v)
|
379
|
+
klass.add_destroy_param(dl, model, reflection, action)
|
379
380
|
end
|
380
381
|
end
|
381
382
|
end
|
382
383
|
end
|
383
384
|
|
384
|
-
def
|
385
|
-
|
385
|
+
def plural_reflection?(model, reflection)
|
386
|
+
PLURAL_REFLECTIONS.include?( model.reflections[reflection].class )
|
387
|
+
end
|
388
|
+
|
389
|
+
def find_relation(model, reflection)
|
390
|
+
begin
|
391
|
+
model.reflections[reflection].class_name.constantize
|
392
|
+
rescue StandardError
|
393
|
+
model
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
def file_attachment?(model, field)
|
398
|
+
(model.respond_to?(:uploaders) && model.uploaders[field.to_sym]) || # carrierwave
|
386
399
|
(model.respond_to?(:attachment_definitions) && model.attachment_definitions[field.to_sym]) ||
|
387
|
-
defined?(Paperclip::Attachment) && model.send(:new).try(field).
|
400
|
+
(defined?(Paperclip::Attachment) && model.send(:new).try(field).is_a?(Paperclip::Attachment))
|
388
401
|
end
|
389
402
|
|
390
|
-
def param_type(model,
|
403
|
+
def param_type(model, field)
|
391
404
|
# Translate from the AR type to the GrapeParam types
|
392
|
-
|
393
|
-
db_type = (model
|
405
|
+
field = field.to_s
|
406
|
+
db_type = (model&.try(:columns_hash) || {})[field]&.type
|
394
407
|
|
395
408
|
# Check if it's a file attachment, look for an override class from the model,
|
396
|
-
# check
|
397
|
-
|
398
|
-
(model
|
399
|
-
|
400
|
-
|
401
|
-
String
|
409
|
+
# check PG2RUBY, use the database type, or fail over to a String:
|
410
|
+
uploaded_file?(model, field) ||
|
411
|
+
check_model_for_type(model, field) ||
|
412
|
+
PG2RUBY[db_type] ||
|
413
|
+
db_type_constant(db_type) ||
|
414
|
+
String # default to String if nothing else works
|
415
|
+
end
|
416
|
+
|
417
|
+
def uploaded_file?(model, field)
|
418
|
+
file_attachment?(model, field) && Rack::Multipart::UploadedFile
|
402
419
|
end
|
403
420
|
|
404
|
-
def
|
421
|
+
def check_model_for_type(model, field)
|
422
|
+
(model.try(:grape_param_types) || {}).with_indifferent_access[field]
|
423
|
+
end
|
424
|
+
|
425
|
+
def db_type_constant(db_type)
|
426
|
+
begin
|
427
|
+
db_type.to_s.camelize.constantize
|
428
|
+
rescue StandardError
|
429
|
+
nil
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def param_required?(model, field)
|
405
434
|
# Detect if the field is a required field for the create action
|
406
|
-
return false if skip_presence_validations.include?(
|
435
|
+
return false if skip_presence_validations.include?(field)
|
407
436
|
|
408
|
-
validated_field =
|
437
|
+
validated_field = field.match?(/_id/) ? field.to_s.sub(/_id\z/, '').to_sym : field.to_sym
|
409
438
|
|
410
|
-
model.validators_on(validated_field).any? {|v| v.
|
439
|
+
model.validators_on(validated_field).any? {|v| v.is_a? ActiveRecord::Validations::PresenceValidator }
|
411
440
|
end
|
412
441
|
|
413
|
-
def add_destroy_param(dsl,model,reflection)
|
414
|
-
|
442
|
+
def add_destroy_param(dsl, model, reflection, action)
|
443
|
+
return if action == :create
|
444
|
+
|
445
|
+
raise "#{model} does not accept nested attributes for #{reflection}" unless model.nested_attributes_options[reflection.to_sym]
|
446
|
+
|
447
|
+
return unless model.nested_attributes_options[reflection.to_sym][:allow_destroy]
|
448
|
+
|
415
449
|
# If destruction is allowed append the _destroy field
|
416
|
-
|
417
|
-
dsl.optional '_destroy', type: Integer
|
418
|
-
end
|
450
|
+
dsl.optional '_destroy', type: Integer
|
419
451
|
end
|
420
|
-
|
421
452
|
end
|
422
453
|
end
|
423
454
|
end
|
@@ -8,10 +8,10 @@ if IntrospectiveGrape.config.camelize_parameters
|
|
8
8
|
module ParseParamsWithCamelized
|
9
9
|
def parse_params(params, path, method, _options = {})
|
10
10
|
parsed_params = parse_params_without_camelized(params, path, method)
|
11
|
-
parsed_params.
|
11
|
+
parsed_params.each do |param|
|
12
12
|
param[:name] = param[:name]
|
13
|
-
|
14
|
-
|
13
|
+
.camelize(:lower)
|
14
|
+
.gsub(/Destroy/, '_destroy')
|
15
15
|
end
|
16
16
|
super(params, path, method, _options = {})
|
17
17
|
end
|
@@ -19,6 +19,7 @@ if IntrospectiveGrape.config.camelize_parameters
|
|
19
19
|
|
20
20
|
module CreateCamelizedDocumentationClass
|
21
21
|
private
|
22
|
+
|
22
23
|
def create_documentation_class
|
23
24
|
doc = super
|
24
25
|
doc.class_eval do
|
@@ -29,6 +30,7 @@ if IntrospectiveGrape.config.camelize_parameters
|
|
29
30
|
end
|
30
31
|
|
31
32
|
Grape::API::Instance.singleton_class.send(:prepend, CreateCamelizedDocumentationClass)
|
33
|
+
|
32
34
|
else
|
33
35
|
module CallWithCamelized
|
34
36
|
def call(*args)
|
@@ -1,18 +1,17 @@
|
|
1
1
|
module IntrospectiveGrape
|
2
2
|
module CreateHelpers
|
3
|
-
|
4
3
|
def add_new_records_to_root_record(dsl, routes, params, model)
|
5
4
|
dsl.send(:authorize, model, :create?)
|
6
5
|
ActiveRecord::Base.transaction do
|
7
6
|
old = find_leaves(routes, model, params)
|
8
|
-
model.update_attributes( dsl.send(:safe_params,params).permit(whitelist) )
|
7
|
+
model.update_attributes( dsl.send(:safe_params, params).permit(whitelist) )
|
9
8
|
new = find_leaves(routes, model, params)
|
10
|
-
old.respond_to?(:size) ? new-old : new
|
9
|
+
old.respond_to?(:size) ? new - old : new
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
14
13
|
def create_new_record(dsl, routes, params)
|
15
|
-
model = routes.first.model.new( dsl.send(:safe_params,params).permit(whitelist) )
|
14
|
+
model = routes.first.model.new( dsl.send(:safe_params, params).permit(whitelist) )
|
16
15
|
dsl.send(:authorize, model, :create?)
|
17
16
|
model.save!
|
18
17
|
|
@@ -22,6 +21,5 @@ module IntrospectiveGrape
|
|
22
21
|
|
23
22
|
find_leaves(routes, model, params)
|
24
23
|
end
|
25
|
-
|
26
24
|
end
|
27
25
|
end
|
@@ -1,9 +1,23 @@
|
|
1
1
|
module IntrospectiveGrape
|
2
2
|
module Doc
|
3
|
-
def index_documentation
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def
|
3
|
+
def index_documentation(name=nil)
|
4
|
+
"returns list of all #{name}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def show_documentation(name=nil)
|
8
|
+
"returns details on a #{name}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_documentation(name=nil)
|
12
|
+
"creates a new #{name} record"
|
13
|
+
end
|
14
|
+
|
15
|
+
def update_documentation(name=nil)
|
16
|
+
"updates the details of a #{name}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def destroy_documentation(name=nil)
|
20
|
+
"destroys the details of a #{name}"
|
21
|
+
end
|
8
22
|
end
|
9
23
|
end
|