model-api 0.8.3
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 +7 -0
- data/README.md +2 -0
- data/lib/model-api.rb +38 -0
- data/lib/model-api/base_controller.rb +1352 -0
- data/lib/model-api/bypass_parse_middleware.rb +32 -0
- data/lib/model-api/hash_metadata.rb +48 -0
- data/lib/model-api/model.rb +49 -0
- data/lib/model-api/not_found_exception.rb +10 -0
- data/lib/model-api/open_api_extensions.rb +287 -0
- data/lib/model-api/renderer.rb +504 -0
- data/lib/model-api/simple_metadata.rb +33 -0
- data/lib/model-api/suppress_login_redirect_middleware.rb +38 -0
- data/lib/model-api/unauthorized_exception.rb +4 -0
- data/lib/model-api/utils.rb +392 -0
- data/model-api.gemspec +24 -0
- metadata +114 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module ModelApi
|
2
|
+
class SimpleMetadata
|
3
|
+
class << self
|
4
|
+
def process_metadata(type, obj, args)
|
5
|
+
instance_var = :"@api_#{type}_metadata"
|
6
|
+
metadata = obj.instance_variable_get(instance_var) || {}
|
7
|
+
if args.present?
|
8
|
+
if args.size == 1 && args[0].is_a?(Hash)
|
9
|
+
metadata.merge!(args[0].symbolize_keys)
|
10
|
+
elsif args.size == 1 && args[0].is_a?(Array)
|
11
|
+
metadata.merge!(Hash[args[0].map { |key| [key.to_sym, {}] }])
|
12
|
+
else
|
13
|
+
metadata.merge!(Hash[args.map { |key| [key.to_sym, {}] }])
|
14
|
+
end
|
15
|
+
obj.instance_variable_set(instance_var, metadata)
|
16
|
+
end
|
17
|
+
metadata.dup
|
18
|
+
end
|
19
|
+
|
20
|
+
def merge_superclass_metadata(type, sc, metadata, opts = {})
|
21
|
+
metadata_def_method = :"api_#{type}"
|
22
|
+
if sc == ActiveRecord::Base || !sc.respond_to?(:"api_#{type}")
|
23
|
+
metadata
|
24
|
+
elsif (exclude_keys = opts[:exclude_keys]).is_a?(Array)
|
25
|
+
(sc.send(metadata_def_method) || {}).reject { |k, _v| exclude_keys.include?(k) }
|
26
|
+
.merge(metadata)
|
27
|
+
else
|
28
|
+
(sc.send(metadata_def_method) || {}).merge(metadata)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ModelApi
|
2
|
+
class SuppressLoginRedirectMiddleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
@api_root = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
unless @api_roots.present?
|
10
|
+
options = Rails.application.config.class.class_variable_get(:@@options)
|
11
|
+
options ||= {}
|
12
|
+
@api_roots = options[:api_middleware_root_paths] || ['api']
|
13
|
+
@api_roots = [@api_roots] unless @api_roots.is_a?(Array)
|
14
|
+
@api_roots = @api_roots.map { |path| path.starts_with?('/') ? path : "/#{path}" }
|
15
|
+
end
|
16
|
+
response = @app.call(env)
|
17
|
+
if response[0].to_i == 302
|
18
|
+
@api_roots.each do |path|
|
19
|
+
next unless env['REQUEST_PATH'].to_s.starts_with?(path) &&
|
20
|
+
(loc = response[1].find { |a| a[0] == 'Location' }).present? &&
|
21
|
+
loc[1].to_s.ends_with?('/users/sign_in')
|
22
|
+
|
23
|
+
# Mimic headers returned from API endpoint 404's for security reasons.
|
24
|
+
response_headers = ModelApi::Utils.common_http_headers.merge(
|
25
|
+
'Content-Type' => 'application/json',
|
26
|
+
'X-Content-Type-Options' => 'nosniff',
|
27
|
+
'X-Frame-Options' => 'SAMEORIGIN',
|
28
|
+
'X-Request-Id' => SecureRandom.uuid,
|
29
|
+
'X-UA-Compatible' => 'chrome=1',
|
30
|
+
'X-XSS-Protection' => '1; mode=block'
|
31
|
+
)
|
32
|
+
return [404, response_headers, [ModelApi::Utils.not_found_response_body]]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,392 @@
|
|
1
|
+
module ModelApi
|
2
|
+
class Utils
|
3
|
+
API_OPERATIONS = [:index, :show, :create, :update, :patch, :destroy, :other, :filter, :sort]
|
4
|
+
CAMELCASE_CONVERSION = true
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def route_name(request)
|
8
|
+
Rails.application.routes.router.recognize(request) do |route, _matches, _parameters|
|
9
|
+
return route.name
|
10
|
+
end
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def api_attrs(obj_or_class)
|
15
|
+
return nil if obj_or_class.nil?
|
16
|
+
klass = obj_or_class.is_a?(Class) ? obj_or_class : obj_or_class.class
|
17
|
+
return expand_metadata(klass.api_attributes) if klass.respond_to?(:api_attributes)
|
18
|
+
Hash[klass.column_names.map(&:to_sym).map { |attr| [attr, { attribute: attr }] }]
|
19
|
+
end
|
20
|
+
|
21
|
+
def filtered_attrs(obj_or_class, operation, opts = {})
|
22
|
+
return nil if obj_or_class.nil?
|
23
|
+
klass = obj_or_class.is_a?(Class) ? obj_or_class : obj_or_class.class
|
24
|
+
filtered_metadata(api_attrs(klass), klass, operation, opts)
|
25
|
+
end
|
26
|
+
|
27
|
+
def filtered_ext_attrs(metadata, operation = nil, opts = {})
|
28
|
+
if operation.is_a?(Hash) && opts.blank?
|
29
|
+
opts = operation
|
30
|
+
operation = opts[:operation] || :show
|
31
|
+
end
|
32
|
+
if metadata.is_a?(ActiveRecord::Base) || (metadata.is_a?(Class) &&
|
33
|
+
metadata < ActiveRecord::Base)
|
34
|
+
metadata = filtered_attrs(metadata, operation, opts)
|
35
|
+
end
|
36
|
+
return metadata unless metadata.is_a?(Hash) && metadata.present?
|
37
|
+
if [:filter, :sort].include?(operation)
|
38
|
+
return Hash[metadata.map { |a, m| [ext_query_attr(a, m), m] }]
|
39
|
+
end
|
40
|
+
Hash[metadata.map { |a, m| [ext_attr(a, m), m] }]
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_request_body(request)
|
44
|
+
request_body = request.body.read.to_s.strip
|
45
|
+
parsed_request_body = nil
|
46
|
+
if request.env['API_CONTENT_TYPE'] == :xml ||
|
47
|
+
request_body.start_with?('<')
|
48
|
+
parsed_request_body = Hash.from_xml(request_body) rescue nil
|
49
|
+
detected_format = :xml
|
50
|
+
end
|
51
|
+
unless parsed_request_body.present?
|
52
|
+
parsed_request_body = JSON.parse(request_body) rescue nil
|
53
|
+
detected_format = :json
|
54
|
+
end
|
55
|
+
[parsed_request_body, detected_format]
|
56
|
+
end
|
57
|
+
|
58
|
+
def ext_attr(attr, attr_metadata = {})
|
59
|
+
sym = attr.is_a?(Symbol)
|
60
|
+
ext_attr = attr_metadata[:alias] || attr
|
61
|
+
ext_attr = ext_attr.to_s.camelize(:lower) if CAMELCASE_CONVERSION
|
62
|
+
sym ? ext_attr.to_sym : ext_attr.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def ext_query_attr(attr, attr_metadata = {})
|
66
|
+
sym = attr.is_a?(Symbol)
|
67
|
+
ext_attr = attr_metadata[:alias] || attr
|
68
|
+
ext_attr = ext_attr.to_s.underscore
|
69
|
+
sym ? ext_attr.to_sym : ext_attr.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def ext_value(value, opts = {})
|
73
|
+
return value unless CAMELCASE_CONVERSION
|
74
|
+
if value.is_a?(Hash)
|
75
|
+
ext_hash(value, opts)
|
76
|
+
elsif value.respond_to?(:map)
|
77
|
+
value.map { |v| ext_value(v, opts) }
|
78
|
+
else
|
79
|
+
value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def internal_value(value, opts = {})
|
84
|
+
return value unless CAMELCASE_CONVERSION
|
85
|
+
if value.is_a?(Hash)
|
86
|
+
internal_hash(value, opts)
|
87
|
+
elsif value.respond_to?(:map)
|
88
|
+
value.map { |v| internal_value(v, opts) }
|
89
|
+
else
|
90
|
+
value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def api_links(obj_or_class)
|
95
|
+
klass = obj_or_class.is_a?(Class) ? obj_or_class : obj_or_class.class
|
96
|
+
return {} unless klass.respond_to?(:api_links)
|
97
|
+
klass.api_links.dup
|
98
|
+
end
|
99
|
+
|
100
|
+
def filtered_links(obj_or_class, operation, opts = {})
|
101
|
+
return {} if obj_or_class.nil? || eval_bool(obj_or_class, opts[:exclude_api_links], opts)
|
102
|
+
klass = obj_or_class.is_a?(Class) ? obj_or_class : obj_or_class.class
|
103
|
+
filtered_metadata(api_links(klass), klass, operation, opts)
|
104
|
+
end
|
105
|
+
|
106
|
+
def eval_can(criteria, context, action_type, controller)
|
107
|
+
return false unless criteria.present? && context.present? &&
|
108
|
+
action_type.present? && controller.respond_to?(:can?)
|
109
|
+
controller.can?(criteria, context)
|
110
|
+
end
|
111
|
+
|
112
|
+
def eval_bool(obj, expr, opts = {})
|
113
|
+
if expr.is_a?(Hash) && opts.blank?
|
114
|
+
obj = nil
|
115
|
+
opts = expr
|
116
|
+
end
|
117
|
+
if expr.respond_to?(:call)
|
118
|
+
return invoke_callback(expr, *([obj, opts].compact)) ? true : false
|
119
|
+
end
|
120
|
+
expr ? true : false
|
121
|
+
end
|
122
|
+
|
123
|
+
def transform_value(value, transform_method_or_proc, opts = {})
|
124
|
+
return value unless transform_method_or_proc.present?
|
125
|
+
if (transform_method_or_proc.is_a?(String) || transform_method_or_proc.is_a?(Symbol)) &&
|
126
|
+
value.respond_to?(transform_method_or_proc.to_sym)
|
127
|
+
return value.send(transform_method_or_proc.to_sym)
|
128
|
+
end
|
129
|
+
value = value.symbolize_keys if value.is_a?(Hash)
|
130
|
+
return value unless transform_method_or_proc.respond_to?(:call)
|
131
|
+
invoke_callback(transform_method_or_proc, value, opts.freeze)
|
132
|
+
end
|
133
|
+
|
134
|
+
def http_status_code(status)
|
135
|
+
unless status.is_a?(Symbol)
|
136
|
+
status_num = status.to_s.to_i
|
137
|
+
if status_num.to_s == status.to_s
|
138
|
+
unless Rack::Utils::HTTP_STATUS_CODES.include?(status_num)
|
139
|
+
fail "Invalid / unrecognized HTTP status code: #{status_num}"
|
140
|
+
end
|
141
|
+
return status_num
|
142
|
+
end
|
143
|
+
status = status.to_s.to_sym
|
144
|
+
end
|
145
|
+
status_code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status]
|
146
|
+
fail "Invalid / unrecognized HTTP status: #{status}" unless status_code.present?
|
147
|
+
status_code
|
148
|
+
end
|
149
|
+
|
150
|
+
def http_status(status)
|
151
|
+
unless status.is_a?(Fixnum)
|
152
|
+
status_num = status.to_s.to_i
|
153
|
+
if status_num.to_s != status.to_s
|
154
|
+
status_sym = status.to_s.to_sym
|
155
|
+
unless Rack::Utils::SYMBOL_TO_STATUS_CODE.include?(status_sym)
|
156
|
+
fail "Invalid / unrecognized HTTP status: #{status_sym}"
|
157
|
+
end
|
158
|
+
return status_sym
|
159
|
+
end
|
160
|
+
status = status_num
|
161
|
+
end
|
162
|
+
status_string = Rack::Utils::HTTP_STATUS_CODES[status]
|
163
|
+
fail "Invalid / unrecognized HTTP status code: #{status}" unless status_string.present?
|
164
|
+
status_string.downcase.gsub(/\s|-/, '_').to_sym
|
165
|
+
end
|
166
|
+
|
167
|
+
def response_successful?(response_status)
|
168
|
+
http_status_code(response_status) < 400
|
169
|
+
end
|
170
|
+
|
171
|
+
def assoc_opts(assoc, attr_metadata, opts)
|
172
|
+
contextual_metadata_opts(attr_metadata, opts.merge(association: assoc))
|
173
|
+
end
|
174
|
+
|
175
|
+
# Build options to generate metadata for a special context, e.g. for an object nested inside
|
176
|
+
# of a parent object.
|
177
|
+
def contextual_metadata_opts(attr_metadata, opts = {})
|
178
|
+
context_opts = opts
|
179
|
+
if (obj_metadata = attr_metadata[:attributes]).present?
|
180
|
+
context_opts = context_opts.merge(metadata: obj_metadata)
|
181
|
+
end
|
182
|
+
except_attrs = attr_metadata[:except_attrs] || []
|
183
|
+
if (assoc = opts[:association]).present? &&
|
184
|
+
![:belongs_to, :has_and_belongs_to_many].include?(assoc.macro) &&
|
185
|
+
!assoc.through_reflection.present?
|
186
|
+
except_attrs << assoc.foreign_key.to_sym
|
187
|
+
end
|
188
|
+
if except_attrs.present?
|
189
|
+
context_opts = context_opts.merge(except: (context_opts[:except] || []) +
|
190
|
+
except_attrs.compact.map(&:to_sym).uniq)
|
191
|
+
end
|
192
|
+
context_opts
|
193
|
+
end
|
194
|
+
|
195
|
+
def set_open_api_type_and_format(properties, type_name)
|
196
|
+
open_api_type, open_api_format = OpenApi::Utils.open_api_type_and_format(type_name)
|
197
|
+
if open_api_type.nil?
|
198
|
+
open_api_type = :string
|
199
|
+
open_api_format = type_name
|
200
|
+
end
|
201
|
+
if open_api_type.present?
|
202
|
+
properties[:type] = open_api_type
|
203
|
+
properties[:format] = open_api_format if open_api_format.present?
|
204
|
+
end
|
205
|
+
properties
|
206
|
+
end
|
207
|
+
|
208
|
+
def model_metadata(klass)
|
209
|
+
return klass.api_model if klass.respond_to?(:api_model)
|
210
|
+
{}
|
211
|
+
end
|
212
|
+
|
213
|
+
def model_name(klass)
|
214
|
+
model_alias = (model_metadata(klass) || {})[:alias]
|
215
|
+
ActiveModel::Name.new(klass, nil, model_alias.present? ? model_alias.to_s : nil)
|
216
|
+
end
|
217
|
+
|
218
|
+
def format_value(value, attr_metadata, opts)
|
219
|
+
ModelApi::Utils.transform_value(value, attr_metadata[:render], opts)
|
220
|
+
rescue Exception => e
|
221
|
+
Rails.logger.warn 'Error encountered formatting API output ' \
|
222
|
+
"(\"#{e.message}\") for value: \"#{value}\"" \
|
223
|
+
' ... rendering unformatted value instead.'
|
224
|
+
value
|
225
|
+
end
|
226
|
+
|
227
|
+
def not_found_response_body(opts = {})
|
228
|
+
response =
|
229
|
+
{
|
230
|
+
successful: false,
|
231
|
+
status: :not_found,
|
232
|
+
status_code: http_status_code(:not_found),
|
233
|
+
errors: [{
|
234
|
+
error: opts[:error] || 'No resource found',
|
235
|
+
message: opts[:message] || 'No resource found at the path ' \
|
236
|
+
'provided or matching the criteria specified'
|
237
|
+
}]
|
238
|
+
}
|
239
|
+
response.to_json(opts)
|
240
|
+
end
|
241
|
+
|
242
|
+
def invoke_callback(callback, *params)
|
243
|
+
return nil unless callback.respond_to?(:call)
|
244
|
+
callback.send(*(([:call] + params)[0..callback.parameters.size]))
|
245
|
+
end
|
246
|
+
|
247
|
+
def common_http_headers
|
248
|
+
{
|
249
|
+
'Cache-Control' => 'no-cache, no-store, max-age=0, must-revalidate',
|
250
|
+
'Pragma' => 'no-cache',
|
251
|
+
'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT'
|
252
|
+
}
|
253
|
+
end
|
254
|
+
|
255
|
+
# Transforms request and response to match conventions (i.e. using camelcase attrs if
|
256
|
+
# configured, and the standard response envelope)
|
257
|
+
def translate_external_api_filter(controller, opts = {}, &block)
|
258
|
+
request = controller.request
|
259
|
+
json, _format = parse_request_body(request)
|
260
|
+
json = internal_value(json)
|
261
|
+
json.each { |k, v| request.parameters[k] = request.POST[k] = v } if json.is_a?(Hash)
|
262
|
+
|
263
|
+
block.call
|
264
|
+
|
265
|
+
obj = controller.response_body.first if controller.response_body.is_a?(Array)
|
266
|
+
obj = (JSON.parse(obj) rescue nil) if obj.present?
|
267
|
+
opts = opts.merge(generate_body_only: true)
|
268
|
+
controller.response_body = [ModelApi::Renderer.render(controller,
|
269
|
+
ModelApi::Utils.ext_value(obj), opts)]
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
|
274
|
+
def action_filter(klass, filter_value, test_value, opts = {})
|
275
|
+
filter_type = opts[:filter_type] || :operation
|
276
|
+
if test_value.is_a?(Array)
|
277
|
+
return test_value.map(&:to_sym).include?(filter_value)
|
278
|
+
elsif test_value.is_a?(Hash)
|
279
|
+
test_value = test_value[filter_value]
|
280
|
+
end
|
281
|
+
if test_value.respond_to?(:call)
|
282
|
+
return ModelApi::Utils.invoke_callback(test_value, klass,
|
283
|
+
opts.merge(filter_type => filter_value).freeze)
|
284
|
+
end
|
285
|
+
filter_value == test_value
|
286
|
+
end
|
287
|
+
|
288
|
+
def filtered_metadata(metadata, klass, operation, opts = {})
|
289
|
+
if (only = opts[:only]).present?
|
290
|
+
metadata.select! { |k, _v| only.include?(k) }
|
291
|
+
end
|
292
|
+
if (except = opts[:except]).present?
|
293
|
+
metadata.reject! { |k, _v| except.include?(k) }
|
294
|
+
end
|
295
|
+
if (metadata_overrides = opts[:metadata]).present?
|
296
|
+
metadata = merge_metadata_overrides(metadata, metadata_overrides)
|
297
|
+
end
|
298
|
+
metadata.select! do |_attr, attr_metadata|
|
299
|
+
include_item?(attr_metadata, klass, operation, opts)
|
300
|
+
end
|
301
|
+
metadata
|
302
|
+
end
|
303
|
+
|
304
|
+
def include_item?(metadata, obj, operation, opts = {})
|
305
|
+
return false unless metadata.is_a?(Hash)
|
306
|
+
return false unless include_item_meets_admin_criteria?(metadata, obj, opts)
|
307
|
+
# Stop here re: filter/sort params, as following checks involve payloads/responses only
|
308
|
+
return eval_bool(obj, metadata[:filter], opts) if operation == :filter
|
309
|
+
return eval_bool(obj, metadata[:sort], opts) if operation == :sort
|
310
|
+
return false unless include_item_meets_read_write_criteria?(metadata, obj, operation, opts)
|
311
|
+
return false unless include_item_meets_incl_excl_criteria?(metadata, obj, operation, opts)
|
312
|
+
true
|
313
|
+
end
|
314
|
+
|
315
|
+
def merge_metadata_overrides(metadata, metadata_overrides)
|
316
|
+
if metadata_overrides.is_a?(Hash)
|
317
|
+
Hash[
|
318
|
+
expand_metadata(metadata_overrides).map do |key, item_metadata|
|
319
|
+
[key, item_metadata.reverse_merge(metadata[key] || {}).reverse_merge(key: key)]
|
320
|
+
end
|
321
|
+
]
|
322
|
+
elsif metadata_overrides.is_a?(Array)
|
323
|
+
metadata.select { |key, _m| metadata_overrides.include?(key) }
|
324
|
+
else
|
325
|
+
metadata
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def expand_metadata(metadata)
|
330
|
+
Hash[metadata.map { |k, v| [k, v.is_a?(Hash) ? v : { value: v, read_only: true }] }]
|
331
|
+
end
|
332
|
+
|
333
|
+
def ext_hash(hash, opts = {})
|
334
|
+
return hash unless CAMELCASE_CONVERSION
|
335
|
+
Hash[hash.map do |key, value|
|
336
|
+
sym = key.is_a?(Symbol)
|
337
|
+
key = key.to_s.camelize(:lower)
|
338
|
+
value = ext_value(value, opts)
|
339
|
+
[sym ? key.to_sym : key, value]
|
340
|
+
end]
|
341
|
+
end
|
342
|
+
|
343
|
+
def internal_hash(hash, opts = {})
|
344
|
+
return hash unless CAMELCASE_CONVERSION
|
345
|
+
Hash[hash.map do |key, value|
|
346
|
+
sym = key.is_a?(Symbol)
|
347
|
+
key = key.to_s.underscore
|
348
|
+
value = internal_value(value, opts)
|
349
|
+
[sym ? key.to_sym : key, value]
|
350
|
+
end]
|
351
|
+
end
|
352
|
+
|
353
|
+
def include_item_meets_admin_criteria?(metadata, obj, opts = {})
|
354
|
+
if eval_bool(obj, metadata[:admin_only], opts)
|
355
|
+
if opts.include?(:admin)
|
356
|
+
return false unless opts[:admin]
|
357
|
+
else
|
358
|
+
return false unless opts[:user].try(:admin_api_user?)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
return false if eval_bool(obj, metadata[:admin_content], opts) && !opts[:admin_content]
|
362
|
+
true
|
363
|
+
end
|
364
|
+
|
365
|
+
def include_item_meets_read_write_criteria?(metadata, obj, operation, opts = {})
|
366
|
+
if [:create, :update, :patch].include?(operation)
|
367
|
+
return false if eval_bool(obj, metadata[:read_only], opts)
|
368
|
+
else
|
369
|
+
return false if eval_bool(obj, metadata[:write_only], opts)
|
370
|
+
end
|
371
|
+
true
|
372
|
+
end
|
373
|
+
|
374
|
+
def include_item_meets_incl_excl_criteria?(metadata, obj, operation, opts = {})
|
375
|
+
if (only = metadata[:only]).present?
|
376
|
+
return false unless action_filter(obj, operation, only, opts)
|
377
|
+
end
|
378
|
+
if (except = metadata[:except]).present?
|
379
|
+
return false if action_filter(obj, operation, except, opts)
|
380
|
+
end
|
381
|
+
action = opts[:action]
|
382
|
+
if (only = metadata[:only_actions]).present?
|
383
|
+
return false unless action_filter(obj, action, only, opts.merge(filter_type: :action))
|
384
|
+
end
|
385
|
+
if (except = metadata[:except_actions]).present?
|
386
|
+
return false if action_filter(obj, action, except, opts.merge(filter_type: :action))
|
387
|
+
end
|
388
|
+
true
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|