introspective_grape 0.4.1 → 0.5.2
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 +30 -0
- data/Gemfile +3 -5
- data/README.md +94 -63
- data/Rakefile +1 -6
- data/introspective_grape.gemspec +63 -53
- data/lib/introspective_grape/api.rb +166 -137
- 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 +153 -45
- 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,19 +187,17 @@ 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
|
-
|
251
|
-
default_includes = routes.first.klass.default_includes(routes.first.model)
|
252
201
|
|
253
202
|
if IntrospectiveGrape.config.skip_object_reload
|
254
203
|
present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
|
@@ -259,18 +208,66 @@ module IntrospectiveGrape
|
|
259
208
|
end
|
260
209
|
end
|
261
210
|
|
211
|
+
# rubocop:enable Metrics/AbcSize
|
262
212
|
def define_destroy(dsl, routes, _model, _api_params)
|
263
213
|
klass = routes.first.klass
|
264
214
|
name = routes.last.name.singularize
|
265
215
|
dsl.desc "destroy a #{name}" do
|
266
|
-
detail klass.destroy_documentation
|
216
|
+
detail klass.destroy_documentation(name)
|
267
217
|
end
|
268
|
-
dsl.delete ":#{routes.last.
|
218
|
+
dsl.delete ":#{routes.last.swagger_key}" do
|
269
219
|
authorize @model, :destroy?
|
270
220
|
present status: (klass.find_leaf(routes, @model, params).destroy ? true : false)
|
271
221
|
end
|
272
222
|
end
|
273
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
|
274
271
|
|
275
272
|
def build_routes(routes, model, reflection_name=nil)
|
276
273
|
routes = routes.clone
|
@@ -280,21 +277,26 @@ module IntrospectiveGrape
|
|
280
277
|
#
|
281
278
|
# Constructs an array representation of the route's models and their associations,
|
282
279
|
# a /root/:rootId/branch/:branchId/leaf/:leafId path would have flat array like
|
283
|
-
# [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
|
284
281
|
# manipulate ActiveRecord relationships and params hashes and so on.
|
285
|
-
parent_model = routes.last
|
282
|
+
parent_model = routes.last&.model
|
286
283
|
return routes if model == parent_model
|
287
284
|
|
288
|
-
name
|
289
|
-
reflection
|
290
|
-
|
291
|
-
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"
|
292
288
|
|
293
|
-
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)
|
294
293
|
end
|
295
294
|
|
295
|
+
def plural?(model, reflection)
|
296
|
+
(model && PLURAL_REFLECTIONS.include?(reflection.class))
|
297
|
+
end
|
296
298
|
|
297
|
-
def build_nested_attributes(routes,hash)
|
299
|
+
def build_nested_attributes(routes, hash)
|
298
300
|
# Recursively re-express the flat attributes hash from nested routes as nested
|
299
301
|
# attributes that can be used to perform an update on the root model.
|
300
302
|
|
@@ -315,13 +317,12 @@ module IntrospectiveGrape
|
|
315
317
|
end
|
316
318
|
end
|
317
319
|
|
318
|
-
|
319
|
-
|
320
320
|
def generate_params(dsl, action, model, fields, is_root_endpoint=false)
|
321
321
|
# We'll be doing a recursive walk (to handle nested attributes) down the
|
322
322
|
# whitelisted params, generating the Grape param definitions by introspecting
|
323
323
|
# on the model and its associations.
|
324
|
-
raise "Invalid action: #{action}" unless
|
324
|
+
raise "Invalid action: #{action}" unless %i(update create).include?(action)
|
325
|
+
|
325
326
|
# dsl : The Grape::Validations::ParamsScope object
|
326
327
|
# action: create or update
|
327
328
|
# model : The ActiveRecord model class
|
@@ -332,16 +333,16 @@ module IntrospectiveGrape
|
|
332
333
|
fields -= [:id] if is_root_endpoint
|
333
334
|
|
334
335
|
fields.each do |field|
|
335
|
-
if field.
|
336
|
-
generate_nested_params(dsl,action,model,field)
|
337
|
-
elsif
|
336
|
+
if field.is_a?(Hash)
|
337
|
+
generate_nested_params(dsl, action, model, field)
|
338
|
+
elsif action == :create && param_required?(model, field)
|
338
339
|
# All params are optional on an update, only require them during creation.
|
339
340
|
# Updating a record with new child models will have to rely on ActiveRecord
|
340
341
|
# validations:
|
341
|
-
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) )
|
342
343
|
else
|
343
|
-
#dsl.optional field, *options
|
344
|
-
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) )
|
345
346
|
end
|
346
347
|
end
|
347
348
|
end
|
@@ -350,25 +351,23 @@ module IntrospectiveGrape
|
|
350
351
|
(model.try(:grape_validations) || {}).with_indifferent_access[field] || {}
|
351
352
|
end
|
352
353
|
|
353
|
-
def generate_nested_params(dsl,action,model,fields)
|
354
|
+
def generate_nested_params(dsl, action, model, fields)
|
354
355
|
klass = self
|
355
|
-
fields.each do |r,v|
|
356
|
+
fields.each do |r, v|
|
356
357
|
# Look at model.reflections to find the association's class name:
|
357
|
-
reflection = r.to_s.sub(/_attributes$/,'') # the reflection name
|
358
|
-
relation
|
358
|
+
reflection = r.to_s.sub(/_attributes$/, '') # the reflection name
|
359
|
+
relation = find_relation(model, reflection)
|
359
360
|
|
360
|
-
if
|
361
|
+
if file_attachment?(model, r)
|
361
362
|
# Handle Carrierwave file upload fields
|
362
|
-
s =
|
363
|
-
if s.present?
|
364
|
-
|
365
|
-
end
|
366
|
-
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)
|
367
366
|
# In case you need a refresher on how these work:
|
368
367
|
# http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
|
369
368
|
dsl.optional r, type: Array do |dl|
|
370
|
-
klass.generate_params(dl,action,relation,v)
|
371
|
-
klass.add_destroy_param(dl,model,reflection
|
369
|
+
klass.generate_params(dl, action, relation, v)
|
370
|
+
klass.add_destroy_param(dl, model, reflection, action)
|
372
371
|
end
|
373
372
|
else
|
374
373
|
# TODO: handle any remaining correctly. Presently defaults to a Hash
|
@@ -376,50 +375,80 @@ module IntrospectiveGrape
|
|
376
375
|
# ThroughReflection, HasOneReflection,
|
377
376
|
# HasAndBelongsToManyReflection, BelongsToReflection
|
378
377
|
dsl.optional r, type: Hash do |dl|
|
379
|
-
klass.generate_params(dl,action,relation,v)
|
380
|
-
klass.add_destroy_param(dl,model,reflection
|
378
|
+
klass.generate_params(dl, action, relation, v)
|
379
|
+
klass.add_destroy_param(dl, model, reflection, action)
|
381
380
|
end
|
382
381
|
end
|
383
382
|
end
|
384
383
|
end
|
385
384
|
|
386
|
-
def
|
387
|
-
|
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
|
388
399
|
(model.respond_to?(:attachment_definitions) && model.attachment_definitions[field.to_sym]) ||
|
389
|
-
defined?(Paperclip::Attachment) && model.send(:new).try(field).
|
400
|
+
(defined?(Paperclip::Attachment) && model.send(:new).try(field).is_a?(Paperclip::Attachment))
|
390
401
|
end
|
391
402
|
|
392
|
-
def param_type(model,
|
403
|
+
def param_type(model, field)
|
393
404
|
# Translate from the AR type to the GrapeParam types
|
394
|
-
|
395
|
-
db_type = (model
|
405
|
+
field = field.to_s
|
406
|
+
db_type = (model&.columns_hash || {})[field]&.type
|
396
407
|
|
397
408
|
# Check if it's a file attachment, look for an override class from the model,
|
398
|
-
# check
|
399
|
-
|
400
|
-
(model
|
401
|
-
|
402
|
-
|
403
|
-
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
|
404
415
|
end
|
405
416
|
|
406
|
-
def
|
417
|
+
def uploaded_file?(model, field)
|
418
|
+
file_attachment?(model, field) && Rack::Multipart::UploadedFile
|
419
|
+
end
|
420
|
+
|
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)
|
407
434
|
# Detect if the field is a required field for the create action
|
408
|
-
return false if skip_presence_validations.include?(
|
435
|
+
return false if skip_presence_validations.include?(field)
|
409
436
|
|
410
|
-
validated_field =
|
437
|
+
validated_field = field.match?(/_id/) ? field.to_s.sub(/_id\z/, '').to_sym : field.to_sym
|
411
438
|
|
412
|
-
model.validators_on(validated_field).any? {|v| v.
|
439
|
+
model.validators_on(validated_field).any? {|v| v.is_a? ActiveRecord::Validations::PresenceValidator }
|
413
440
|
end
|
414
441
|
|
415
|
-
def add_destroy_param(dsl,model,reflection)
|
416
|
-
|
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
|
+
|
417
449
|
# If destruction is allowed append the _destroy field
|
418
|
-
|
419
|
-
dsl.optional '_destroy', type: Integer
|
420
|
-
end
|
450
|
+
dsl.optional '_destroy', type: Integer
|
421
451
|
end
|
422
|
-
|
423
452
|
end
|
424
453
|
end
|
425
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
|