introspective_grape 0.4.3 → 0.5.4
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/.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
|