haveapi 0.27.3 → 0.28.0
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/Gemfile +1 -1
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/action.rb +125 -36
- data/lib/haveapi/actions/paginable.rb +3 -1
- data/lib/haveapi/authentication/basic/provider.rb +2 -0
- data/lib/haveapi/authentication/chain.rb +11 -7
- data/lib/haveapi/authentication/oauth2/config.rb +25 -3
- data/lib/haveapi/authentication/oauth2/provider.rb +92 -11
- data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +44 -3
- data/lib/haveapi/authentication/token/provider.rb +53 -15
- data/lib/haveapi/authorization.rb +42 -18
- data/lib/haveapi/client_examples/php_client.rb +1 -1
- data/lib/haveapi/client_examples/ruby_client.rb +1 -1
- data/lib/haveapi/context.rb +10 -4
- data/lib/haveapi/example.rb +15 -16
- data/lib/haveapi/extensions/action_exceptions.rb +6 -6
- data/lib/haveapi/model_adapters/active_record.rb +140 -68
- data/lib/haveapi/model_adapters/hash.rb +1 -1
- data/lib/haveapi/parameters/resource.rb +35 -3
- data/lib/haveapi/parameters/typed.rb +26 -7
- data/lib/haveapi/params.rb +27 -8
- data/lib/haveapi/resource.rb +4 -1
- data/lib/haveapi/resources/action_state.rb +8 -1
- data/lib/haveapi/route.rb +2 -2
- data/lib/haveapi/server.rb +137 -45
- data/lib/haveapi/validator.rb +2 -2
- data/lib/haveapi/validator_chain.rb +1 -0
- data/lib/haveapi/validators/confirmation.rb +1 -0
- data/lib/haveapi/validators/format.rb +6 -2
- data/lib/haveapi/validators/length.rb +2 -0
- data/lib/haveapi/validators/numericality.rb +2 -0
- data/lib/haveapi/validators/presence.rb +1 -1
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page/client_auth.erb +1 -1
- data/lib/haveapi/views/version_page/client_example.erb +3 -3
- data/lib/haveapi/views/version_page/client_init.erb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- data/lib/haveapi/views/version_sidebar.erb +4 -2
- data/spec/action/authorize_spec.rb +99 -0
- data/spec/action/runtime_spec.rb +426 -0
- data/spec/action_state_spec.rb +52 -0
- data/spec/authentication/basic_spec.rb +29 -0
- data/spec/authentication/oauth2_spec.rb +329 -0
- data/spec/authentication/token_spec.rb +195 -0
- data/spec/authentication/token_version_routes_spec.rb +164 -0
- data/spec/authorization_spec.rb +66 -0
- data/spec/documentation/auth_filtering_spec.rb +195 -1
- data/spec/documentation/current_user_html_escaping_spec.rb +47 -0
- data/spec/documentation/examples_spec.rb +97 -0
- data/spec/documentation/host_html_escaping_spec.rb +41 -0
- data/spec/documentation_spec.rb +13 -0
- data/spec/extensions/action_exceptions_spec.rb +30 -0
- data/spec/model_adapters/active_record_spec.rb +406 -1
- data/spec/parameters/typed_spec.rb +42 -0
- data/spec/params_spec.rb +41 -0
- data/spec/server/integration_spec.rb +90 -0
- data/spec/validator_chain_spec.rb +39 -0
- data/spec/validators/confirmation_spec.rb +14 -0
- data/spec/validators/format_spec.rb +7 -0
- data/spec/validators/length_spec.rb +6 -0
- data/spec/validators/numericality_spec.rb +7 -0
- data/spec/validators/presence_spec.rb +2 -0
- data/test_support/client_test_api.rb +28 -0
- metadata +8 -4
- data/shell.nix +0 -20
|
@@ -153,6 +153,8 @@ module HaveAPI::Authentication
|
|
|
153
153
|
t = token(request)
|
|
154
154
|
|
|
155
155
|
t && config.find_user_by_token(request, t)
|
|
156
|
+
rescue HaveAPI::AuthenticationError
|
|
157
|
+
nil
|
|
156
158
|
end
|
|
157
159
|
|
|
158
160
|
# Extract token from HTTP request
|
|
@@ -194,10 +196,7 @@ module HaveAPI::Authentication
|
|
|
194
196
|
end
|
|
195
197
|
|
|
196
198
|
def token_present?(value)
|
|
197
|
-
|
|
198
|
-
return false if value.respond_to?(:empty?) && value.empty?
|
|
199
|
-
|
|
200
|
-
true
|
|
199
|
+
value.is_a?(String) && !value.empty?
|
|
201
200
|
end
|
|
202
201
|
|
|
203
202
|
def token_resource
|
|
@@ -228,7 +227,8 @@ module HaveAPI::Authentication
|
|
|
228
227
|
END
|
|
229
228
|
integer :interval, label: 'Interval',
|
|
230
229
|
desc: 'How long will requested token be valid, in seconds.',
|
|
231
|
-
default: 60 * 5, fill: true
|
|
230
|
+
default: 60 * 5, fill: true,
|
|
231
|
+
number: { min: 1, max: 86_400 }
|
|
232
232
|
end
|
|
233
233
|
|
|
234
234
|
output(:hash) do
|
|
@@ -277,11 +277,30 @@ module HaveAPI::Authentication
|
|
|
277
277
|
|
|
278
278
|
def exec
|
|
279
279
|
provider = self.class.resource.token_instance
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
begin
|
|
281
|
+
token = provider.token(request)
|
|
282
|
+
user = provider.authenticate(request)
|
|
283
|
+
rescue HaveAPI::Authentication::TokenConflict => e
|
|
284
|
+
error!(e.message, {}, http_status: 400)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
unless user
|
|
288
|
+
error!(
|
|
289
|
+
'Action requires user to authenticate with a token',
|
|
290
|
+
{},
|
|
291
|
+
http_status: 401
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
begin
|
|
296
|
+
result = provider.config.class.revoke.handle.call(ActionRequest.new(
|
|
297
|
+
request:,
|
|
298
|
+
user:,
|
|
299
|
+
token:
|
|
300
|
+
), ActionResult.new)
|
|
301
|
+
rescue HaveAPI::AuthenticationError => e
|
|
302
|
+
error!(e.message)
|
|
303
|
+
end
|
|
285
304
|
|
|
286
305
|
if result.ok?
|
|
287
306
|
ok!
|
|
@@ -305,11 +324,30 @@ module HaveAPI::Authentication
|
|
|
305
324
|
|
|
306
325
|
def exec
|
|
307
326
|
provider = self.class.resource.token_instance
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
327
|
+
begin
|
|
328
|
+
token = provider.token(request)
|
|
329
|
+
user = provider.authenticate(request)
|
|
330
|
+
rescue HaveAPI::Authentication::TokenConflict => e
|
|
331
|
+
error!(e.message, {}, http_status: 400)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
unless user
|
|
335
|
+
error!(
|
|
336
|
+
'Action requires user to authenticate with a token',
|
|
337
|
+
{},
|
|
338
|
+
http_status: 401
|
|
339
|
+
)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
begin
|
|
343
|
+
result = provider.config.class.renew.handle.call(ActionRequest.new(
|
|
344
|
+
request:,
|
|
345
|
+
user:,
|
|
346
|
+
token:
|
|
347
|
+
), ActionResult.new)
|
|
348
|
+
rescue HaveAPI::AuthenticationError => e
|
|
349
|
+
error!(e.message)
|
|
350
|
+
end
|
|
313
351
|
|
|
314
352
|
if result.ok?
|
|
315
353
|
{ valid_to: result.valid_to }
|
|
@@ -30,7 +30,15 @@ module HaveAPI
|
|
|
30
30
|
# Apply restrictions on query which selects objects from database.
|
|
31
31
|
# Most common usage is restrict user to access only objects he owns.
|
|
32
32
|
def restrict(**kwargs)
|
|
33
|
-
|
|
33
|
+
normalized = normalize_hash_keys(kwargs)
|
|
34
|
+
|
|
35
|
+
normalized.each do |key, value|
|
|
36
|
+
@restrict.each do |restriction|
|
|
37
|
+
deny if restriction.has_key?(key) && restriction[key] != value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@restrict << normalized
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
# Restrict parameters client can set/change.
|
|
@@ -65,7 +73,11 @@ module HaveAPI
|
|
|
65
73
|
ret = {}
|
|
66
74
|
|
|
67
75
|
@restrict.each do |r|
|
|
68
|
-
|
|
76
|
+
r.each do |key, value|
|
|
77
|
+
deny if ret.has_key?(key) && ret[key] != value
|
|
78
|
+
|
|
79
|
+
ret[key] = value
|
|
80
|
+
end
|
|
69
81
|
end
|
|
70
82
|
|
|
71
83
|
ret
|
|
@@ -79,12 +91,16 @@ module HaveAPI
|
|
|
79
91
|
filter_inner(output, @output, params, format)
|
|
80
92
|
end
|
|
81
93
|
|
|
94
|
+
def permitted_input_names(params)
|
|
95
|
+
permitted_params(params, @input).map(&:name)
|
|
96
|
+
end
|
|
97
|
+
|
|
82
98
|
private
|
|
83
99
|
|
|
84
100
|
def filter_inner(allowed_params, direction, params, format)
|
|
85
101
|
allowed = {}
|
|
86
102
|
|
|
87
|
-
allowed_params.each do |p|
|
|
103
|
+
permitted_params(allowed_params, direction).each do |p|
|
|
88
104
|
if params.has_param?(p.name)
|
|
89
105
|
allowed[p.name] = format ? p.format_output(params[p.name]) : params[p.name]
|
|
90
106
|
|
|
@@ -93,29 +109,37 @@ module HaveAPI
|
|
|
93
109
|
end
|
|
94
110
|
end
|
|
95
111
|
|
|
96
|
-
|
|
112
|
+
allowed
|
|
113
|
+
end
|
|
97
114
|
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
def permitted_params(params, direction)
|
|
116
|
+
return params unless direction
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
ret
|
|
118
|
+
if direction[:whitelist]
|
|
119
|
+
whitelist = normalize_names(direction[:whitelist])
|
|
106
120
|
|
|
121
|
+
params.select { |p| whitelist.include?(p.name) }
|
|
107
122
|
elsif direction[:blacklist]
|
|
108
|
-
|
|
123
|
+
blacklist = normalize_names(direction[:blacklist])
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
params.reject { |p| blacklist.include?(p.name) }
|
|
126
|
+
else
|
|
127
|
+
params
|
|
128
|
+
end
|
|
129
|
+
end
|
|
113
130
|
|
|
114
|
-
|
|
131
|
+
def normalize_names(names)
|
|
132
|
+
names.map { |name| normalize_key(name) }
|
|
133
|
+
end
|
|
115
134
|
|
|
116
|
-
|
|
117
|
-
|
|
135
|
+
def normalize_hash_keys(hash)
|
|
136
|
+
hash.each_with_object({}) do |(key, value), ret|
|
|
137
|
+
ret[normalize_key(key)] = value
|
|
118
138
|
end
|
|
119
139
|
end
|
|
140
|
+
|
|
141
|
+
def normalize_key(key)
|
|
142
|
+
key.is_a?(String) ? key.to_sym : key
|
|
143
|
+
end
|
|
120
144
|
end
|
|
121
145
|
end
|
|
@@ -94,7 +94,7 @@ module HaveAPI::ClientExamples
|
|
|
94
94
|
out << "$reply = $api->#{resource_path.join('->')}->#{action_name}"
|
|
95
95
|
out << "(#{args.join(', ')});\n"
|
|
96
96
|
|
|
97
|
-
return
|
|
97
|
+
return out << response(sample) if sample[:status]
|
|
98
98
|
|
|
99
99
|
out << '// Throws exception \\HaveAPI\\Client\\Exception\\ActionFailed'
|
|
100
100
|
out
|
|
@@ -55,7 +55,7 @@ module HaveAPI::ClientExamples
|
|
|
55
55
|
out << "reply = client.#{resource_path.join('.')}.#{action_name}"
|
|
56
56
|
out << "(#{args.join(', ')})" unless args.empty?
|
|
57
57
|
|
|
58
|
-
return
|
|
58
|
+
return out << response(sample) if sample[:status]
|
|
59
59
|
|
|
60
60
|
out << "\n"
|
|
61
61
|
out << '# Raises exception HaveAPI::Client::ActionFailed'
|
data/lib/haveapi/context.rb
CHANGED
|
@@ -2,11 +2,13 @@ module HaveAPI
|
|
|
2
2
|
class Context
|
|
3
3
|
attr_accessor :server, :version, :request, :resource, :action, :path, :args,
|
|
4
4
|
:params, :current_user, :authorization, :endpoint, :resource_path,
|
|
5
|
-
:action_instance, :action_prepare, :layout, :doc
|
|
5
|
+
:action_instance, :action_prepare, :layout, :doc,
|
|
6
|
+
:auth_users_by_version
|
|
6
7
|
|
|
7
8
|
def initialize(server, version: nil, request: nil, resource: [], action: nil,
|
|
8
9
|
path: nil, args: nil, params: nil, user: nil,
|
|
9
|
-
authorization: nil, endpoint: nil, resource_path: [], doc: false
|
|
10
|
+
authorization: nil, endpoint: nil, resource_path: [], doc: false,
|
|
11
|
+
auth_users_by_version: nil)
|
|
10
12
|
@server = server
|
|
11
13
|
@version = version
|
|
12
14
|
@request = request
|
|
@@ -20,6 +22,7 @@ module HaveAPI
|
|
|
20
22
|
@endpoint = endpoint
|
|
21
23
|
@resource_path = resource_path
|
|
22
24
|
@doc = doc
|
|
25
|
+
@auth_users_by_version = auth_users_by_version
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def resolved_path
|
|
@@ -79,7 +82,7 @@ module HaveAPI
|
|
|
79
82
|
|
|
80
83
|
my_args = args.clone
|
|
81
84
|
|
|
82
|
-
path.scan(/\{([a-zA-
|
|
85
|
+
path.scan(/\{([a-zA-Z0-9\-_]+)\}/) do |match|
|
|
83
86
|
path_param = match.first
|
|
84
87
|
ret[path_param] = my_args.shift
|
|
85
88
|
end
|
|
@@ -94,7 +97,10 @@ module HaveAPI
|
|
|
94
97
|
private
|
|
95
98
|
|
|
96
99
|
def resolve_arg!(path, arg)
|
|
97
|
-
|
|
100
|
+
value = arg.to_s
|
|
101
|
+
raise HaveAPI::ValidationError, 'invalid path parameter encoding' unless value.valid_encoding?
|
|
102
|
+
|
|
103
|
+
path.sub!(/\{[a-zA-Z0-9\-_]+\}/, value)
|
|
98
104
|
end
|
|
99
105
|
end
|
|
100
106
|
end
|
data/lib/haveapi/example.rb
CHANGED
|
@@ -41,21 +41,17 @@ module HaveAPI
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def authorized?(context)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
else
|
|
48
|
-
true
|
|
49
|
-
end
|
|
44
|
+
return true unless @authorization
|
|
45
|
+
|
|
46
|
+
@authorization.call(context.current_user) ? true : false
|
|
50
47
|
end
|
|
51
48
|
|
|
52
49
|
def provided?
|
|
53
|
-
|
|
54
|
-
instance_variable_get(v)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
false
|
|
50
|
+
instance_variables.any? do |v|
|
|
51
|
+
value = instance_variable_get(v)
|
|
52
|
+
next false if v == :@title && value.to_s.empty?
|
|
53
|
+
|
|
54
|
+
!value.nil? && value != false
|
|
59
55
|
end
|
|
60
56
|
end
|
|
61
57
|
|
|
@@ -65,8 +61,8 @@ module HaveAPI
|
|
|
65
61
|
title: @title,
|
|
66
62
|
comment: @comment,
|
|
67
63
|
path_params: @path_params,
|
|
68
|
-
request: filter_input_params(context, @request),
|
|
69
|
-
response: filter_output_params(context, @response),
|
|
64
|
+
request: @request.nil? ? nil : filter_input_params(context, @request),
|
|
65
|
+
response: @response.nil? ? nil : filter_output_params(context, @response),
|
|
70
66
|
status: @status.nil? ? true : @status,
|
|
71
67
|
message: @message,
|
|
72
68
|
errors: @errors,
|
|
@@ -80,6 +76,8 @@ module HaveAPI
|
|
|
80
76
|
protected
|
|
81
77
|
|
|
82
78
|
def filter_input_params(context, input)
|
|
79
|
+
return nil if input.nil?
|
|
80
|
+
|
|
83
81
|
case context.action.input.layout
|
|
84
82
|
when :object, :hash
|
|
85
83
|
context.authorization.filter_input(
|
|
@@ -91,14 +89,15 @@ module HaveAPI
|
|
|
91
89
|
input.map do |obj|
|
|
92
90
|
context.authorization.filter_input(
|
|
93
91
|
context.action.input.params,
|
|
94
|
-
ModelAdapters::Hash.output(context, obj)
|
|
95
|
-
true
|
|
92
|
+
ModelAdapters::Hash.output(context, obj)
|
|
96
93
|
)
|
|
97
94
|
end
|
|
98
95
|
end
|
|
99
96
|
end
|
|
100
97
|
|
|
101
98
|
def filter_output_params(context, output)
|
|
99
|
+
return nil if output.nil?
|
|
100
|
+
|
|
102
101
|
case context.action.output.layout
|
|
103
102
|
when :object, :hash
|
|
104
103
|
context.authorization.filter_output(
|
|
@@ -5,12 +5,12 @@ module HaveAPI::Extensions
|
|
|
5
5
|
class << self
|
|
6
6
|
def enabled(server)
|
|
7
7
|
HaveAPI::Action.connect_hook(:exec_exception) do |ret, _context, e|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
if @exceptions
|
|
9
|
+
@exceptions.each do |handler|
|
|
10
|
+
if e.is_a?(handler[:klass])
|
|
11
|
+
ret = handler[:block].call(ret, e)
|
|
12
|
+
break
|
|
13
|
+
end
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -19,6 +19,8 @@ module HaveAPI::ModelAdapters
|
|
|
19
19
|
|
|
20
20
|
module Action
|
|
21
21
|
module InstanceMethods
|
|
22
|
+
MAX_INCLUDE_DEPTH = 16
|
|
23
|
+
|
|
22
24
|
# Helper method that sets correct ActiveRecord includes
|
|
23
25
|
# according to the meta includes sent by the user.
|
|
24
26
|
# `q` is the model or partial AR query. If not set,
|
|
@@ -77,6 +79,14 @@ module HaveAPI::ModelAdapters
|
|
|
77
79
|
paginable = input[parameter]
|
|
78
80
|
limit = input[:limit]
|
|
79
81
|
|
|
82
|
+
if limit && limit > HaveAPI::Actions::Paginable::MAX_LIMIT
|
|
83
|
+
error!(
|
|
84
|
+
"limit has to be maximally #{HaveAPI::Actions::Paginable::MAX_LIMIT}",
|
|
85
|
+
{},
|
|
86
|
+
http_status: 400
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
80
90
|
q = yield(q, paginable) if paginable
|
|
81
91
|
q = q.limit(limit) if limit
|
|
82
92
|
q
|
|
@@ -87,40 +97,54 @@ module HaveAPI::ModelAdapters
|
|
|
87
97
|
def ar_parse_includes(raw)
|
|
88
98
|
return @ar_parsed_includes if @ar_parsed_includes
|
|
89
99
|
|
|
90
|
-
@ar_parsed_includes =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if inc.is_a?(::Hash)
|
|
95
|
-
inc.each_key do |k|
|
|
96
|
-
next(false) unless self.class.model.reflections.has_key?(k.to_s)
|
|
97
|
-
end
|
|
100
|
+
@ar_parsed_includes = raw.filter_map do |assoc|
|
|
101
|
+
ar_parse_include_path(assoc, self.class.model)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
# Kept for callers that used the old parser directly.
|
|
106
|
+
def ar_inner_includes(includes)
|
|
107
|
+
includes.filter_map do |assoc|
|
|
108
|
+
parts = assoc.to_s.split('__').reject(&:empty?).map(&:to_sym)
|
|
109
|
+
next if parts.empty? || parts.size > MAX_INCLUDE_DEPTH
|
|
102
110
|
|
|
103
|
-
|
|
111
|
+
ar_include_tree(parts)
|
|
104
112
|
end
|
|
105
113
|
end
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
def ar_parse_include_path(path, model)
|
|
116
|
+
parts = path.to_s.split('__')
|
|
117
|
+
return if parts.empty? || parts.size > MAX_INCLUDE_DEPTH
|
|
118
|
+
|
|
119
|
+
current_model = model
|
|
120
|
+
symbols = []
|
|
121
|
+
i = 0
|
|
122
|
+
|
|
123
|
+
while i < parts.size
|
|
124
|
+
part = parts[i]
|
|
125
|
+
return if part.empty?
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
parts = assoc.split('__')
|
|
115
|
-
tmp[parts.first.to_sym] = ar_inner_includes([parts[1..].join('__')])
|
|
127
|
+
begin
|
|
128
|
+
reflection = current_model.reflections[part.to_s]
|
|
129
|
+
return unless reflection
|
|
116
130
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
symbols << part.to_sym
|
|
132
|
+
current_model = reflection.klass
|
|
133
|
+
rescue NameError
|
|
134
|
+
return
|
|
120
135
|
end
|
|
136
|
+
i += 1
|
|
121
137
|
end
|
|
122
138
|
|
|
123
|
-
|
|
139
|
+
ar_include_tree(symbols)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def ar_include_tree(parts)
|
|
143
|
+
ret = parts.last
|
|
144
|
+
(parts.size - 2).downto(0) do |i|
|
|
145
|
+
ret = { parts[i] => [ret] }
|
|
146
|
+
end
|
|
147
|
+
ret
|
|
124
148
|
end
|
|
125
149
|
|
|
126
150
|
# Default includes contain all associated resources specified
|
|
@@ -153,7 +177,9 @@ module HaveAPI::ModelAdapters
|
|
|
153
177
|
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
154
178
|
end
|
|
155
179
|
|
|
156
|
-
if raw.is_a?(
|
|
180
|
+
if raw.is_a?(Array) || raw.is_a?(Hash) || [true, false].include?(raw)
|
|
181
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
182
|
+
elsif raw.is_a?(String)
|
|
157
183
|
stripped = raw.strip
|
|
158
184
|
|
|
159
185
|
if stripped.empty?
|
|
@@ -163,9 +189,6 @@ module HaveAPI::ModelAdapters
|
|
|
163
189
|
end
|
|
164
190
|
|
|
165
191
|
raw = stripped
|
|
166
|
-
|
|
167
|
-
elsif [true, false].include?(raw)
|
|
168
|
-
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
169
192
|
end
|
|
170
193
|
|
|
171
194
|
value = if integer_pk?(model)
|
|
@@ -177,11 +200,17 @@ module HaveAPI::ModelAdapters
|
|
|
177
200
|
raw
|
|
178
201
|
end
|
|
179
202
|
|
|
180
|
-
if extra[:fetch]
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
ret = if extra[:fetch]
|
|
204
|
+
model.instance_exec(value, &extra[:fetch])
|
|
205
|
+
else
|
|
206
|
+
model.find(value)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if ret.nil? && !allow_null
|
|
210
|
+
raise HaveAPI::ValidationError, 'resource not found'
|
|
184
211
|
end
|
|
212
|
+
|
|
213
|
+
ret
|
|
185
214
|
rescue ::ActiveRecord::RecordNotFound
|
|
186
215
|
raise HaveAPI::ValidationError, 'resource not found'
|
|
187
216
|
rescue ArgumentError, TypeError
|
|
@@ -234,10 +263,20 @@ module HaveAPI::ModelAdapters
|
|
|
234
263
|
return unless %i[object object_list].include?(action.input.layout)
|
|
235
264
|
|
|
236
265
|
clean = proc do |raw|
|
|
237
|
-
if raw.is_a?(String)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
266
|
+
values = if raw.is_a?(String)
|
|
267
|
+
raw.strip.split(',')
|
|
268
|
+
elsif raw.is_a?(Array)
|
|
269
|
+
raw
|
|
270
|
+
else
|
|
271
|
+
raise HaveAPI::ValidationError, 'includes must be a string or array'
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
values.map do |value|
|
|
275
|
+
unless value.is_a?(String)
|
|
276
|
+
raise HaveAPI::ValidationError, 'includes must contain only strings'
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
value.strip
|
|
241
280
|
end
|
|
242
281
|
end
|
|
243
282
|
|
|
@@ -306,49 +345,82 @@ module HaveAPI::ModelAdapters
|
|
|
306
345
|
res_output = res_show.output
|
|
307
346
|
|
|
308
347
|
args = res_show.resolve_path_params(val)
|
|
348
|
+
resolve_assoc = includes_include?(param.name)
|
|
349
|
+
pass_includes = includes_pass_on_to(param.name) if resolve_assoc
|
|
309
350
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
351
|
+
with_association_context(res_show, args) do |show|
|
|
352
|
+
# Tell the child action it is being checked as a nested association.
|
|
353
|
+
show.flags[:inner_assoc] = true
|
|
313
354
|
|
|
314
|
-
|
|
355
|
+
return unauthorized_resource unless show.authorized?(@context.current_user)
|
|
356
|
+
return unauthorized_resource unless show_prepared?(show)
|
|
315
357
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
push_ins.version,
|
|
319
|
-
{},
|
|
320
|
-
nil,
|
|
321
|
-
@context
|
|
322
|
-
)
|
|
323
|
-
show.meta[:includes] = pass_includes
|
|
358
|
+
if resolve_assoc
|
|
359
|
+
show.meta[:includes] = pass_includes
|
|
324
360
|
|
|
325
|
-
|
|
326
|
-
# as a nested association, that it wasn't called directly by the user.
|
|
327
|
-
show.flags[:inner_assoc] = true
|
|
361
|
+
ret = show.safe_output(val)
|
|
328
362
|
|
|
329
|
-
|
|
363
|
+
raise "#{res_show} resolve failed" unless ret[0]
|
|
330
364
|
|
|
331
|
-
|
|
365
|
+
ret[1][res_show.output.namespace].update({
|
|
366
|
+
_meta: ret[1][:_meta].update(resolved: true)
|
|
367
|
+
})
|
|
332
368
|
|
|
333
|
-
|
|
334
|
-
|
|
369
|
+
else
|
|
370
|
+
{
|
|
371
|
+
param.value_id => val.send(res_output[param.value_id].db_name),
|
|
372
|
+
param.value_label => val.send(res_output[param.value_label].db_name),
|
|
373
|
+
_meta: {
|
|
374
|
+
path_params: args.is_a?(Array) ? args : [args],
|
|
375
|
+
resolved: false
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
335
381
|
|
|
336
|
-
|
|
382
|
+
def with_association_context(res_show, args)
|
|
383
|
+
push_cls = @context.action
|
|
384
|
+
push_ins = @context.action_instance
|
|
385
|
+
push_path = @context.path
|
|
386
|
+
@context.path = res_show.build_route('')
|
|
387
|
+
|
|
388
|
+
res_show.new(
|
|
389
|
+
push_ins.request,
|
|
390
|
+
push_ins.version,
|
|
391
|
+
res_show.path_params(@context.path, args),
|
|
392
|
+
nil,
|
|
393
|
+
@context
|
|
394
|
+
)
|
|
395
|
+
yield @context.action_instance
|
|
396
|
+
ensure
|
|
397
|
+
restore_context(push_cls, push_ins, push_path)
|
|
398
|
+
end
|
|
337
399
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
400
|
+
def restore_context(action, action_instance, path)
|
|
401
|
+
@context.action = action
|
|
402
|
+
@context.action_instance = action_instance
|
|
403
|
+
@context.path = path
|
|
404
|
+
end
|
|
341
405
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
resolved: false
|
|
349
|
-
}
|
|
350
|
-
}
|
|
406
|
+
def show_prepared?(show)
|
|
407
|
+
completed = Object.new
|
|
408
|
+
ret = catch(:return) do
|
|
409
|
+
show.validate!
|
|
410
|
+
show.prepare
|
|
411
|
+
completed
|
|
351
412
|
end
|
|
413
|
+
|
|
414
|
+
ret.equal?(completed)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def unauthorized_resource
|
|
418
|
+
{
|
|
419
|
+
_meta: {
|
|
420
|
+
resolved: false,
|
|
421
|
+
authorized: false
|
|
422
|
+
}
|
|
423
|
+
}
|
|
352
424
|
end
|
|
353
425
|
|
|
354
426
|
# Should an association with `name` be resolved?
|