haveapi 0.27.2 → 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 +150 -71
- data/lib/haveapi/model_adapters/hash.rb +1 -1
- data/lib/haveapi/parameters/resource.rb +50 -6
- data/lib/haveapi/parameters/typed.rb +40 -13
- data/lib/haveapi/params.rb +27 -8
- data/lib/haveapi/resource.rb +4 -1
- data/lib/haveapi/resources/action_state.rb +13 -5
- 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 +408 -3
- data/spec/parameters/typed_spec.rb +75 -7
- 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 +31 -3
- 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
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
parts = assoc.split('__')
|
|
115
|
-
tmp[parts.first.to_sym] = ar_inner_includes([parts[1..].join('__')])
|
|
119
|
+
current_model = model
|
|
120
|
+
symbols = []
|
|
121
|
+
i = 0
|
|
116
122
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
while i < parts.size
|
|
124
|
+
part = parts[i]
|
|
125
|
+
return if part.empty?
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
reflection = current_model.reflections[part.to_s]
|
|
129
|
+
return unless reflection
|
|
130
|
+
|
|
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
|
|
@@ -142,23 +166,29 @@ module HaveAPI::ModelAdapters
|
|
|
142
166
|
|
|
143
167
|
class Input < ::HaveAPI::ModelAdapter::Input
|
|
144
168
|
def self.clean(model, raw, extra)
|
|
145
|
-
return nil if raw.nil?
|
|
146
|
-
|
|
147
169
|
original = raw
|
|
170
|
+
allow_null = if extra
|
|
171
|
+
extra.has_key?(:nullable) ? extra[:nullable] : extra[:optional]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if raw.nil?
|
|
175
|
+
return nil if allow_null
|
|
176
|
+
|
|
177
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
178
|
+
end
|
|
148
179
|
|
|
149
|
-
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)
|
|
150
183
|
stripped = raw.strip
|
|
151
184
|
|
|
152
185
|
if stripped.empty?
|
|
153
|
-
return nil if
|
|
186
|
+
return nil if allow_null
|
|
154
187
|
|
|
155
188
|
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
156
189
|
end
|
|
157
190
|
|
|
158
191
|
raw = stripped
|
|
159
|
-
|
|
160
|
-
elsif [true, false].include?(raw)
|
|
161
|
-
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
162
192
|
end
|
|
163
193
|
|
|
164
194
|
value = if integer_pk?(model)
|
|
@@ -170,11 +200,17 @@ module HaveAPI::ModelAdapters
|
|
|
170
200
|
raw
|
|
171
201
|
end
|
|
172
202
|
|
|
173
|
-
if extra[:fetch]
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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'
|
|
177
211
|
end
|
|
212
|
+
|
|
213
|
+
ret
|
|
178
214
|
rescue ::ActiveRecord::RecordNotFound
|
|
179
215
|
raise HaveAPI::ValidationError, 'resource not found'
|
|
180
216
|
rescue ArgumentError, TypeError
|
|
@@ -227,10 +263,20 @@ module HaveAPI::ModelAdapters
|
|
|
227
263
|
return unless %i[object object_list].include?(action.input.layout)
|
|
228
264
|
|
|
229
265
|
clean = proc do |raw|
|
|
230
|
-
if raw.is_a?(String)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
234
280
|
end
|
|
235
281
|
end
|
|
236
282
|
|
|
@@ -299,49 +345,82 @@ module HaveAPI::ModelAdapters
|
|
|
299
345
|
res_output = res_show.output
|
|
300
346
|
|
|
301
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
|
|
302
350
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
306
354
|
|
|
307
|
-
|
|
355
|
+
return unauthorized_resource unless show.authorized?(@context.current_user)
|
|
356
|
+
return unauthorized_resource unless show_prepared?(show)
|
|
308
357
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
push_ins.version,
|
|
312
|
-
{},
|
|
313
|
-
nil,
|
|
314
|
-
@context
|
|
315
|
-
)
|
|
316
|
-
show.meta[:includes] = pass_includes
|
|
358
|
+
if resolve_assoc
|
|
359
|
+
show.meta[:includes] = pass_includes
|
|
317
360
|
|
|
318
|
-
|
|
319
|
-
# as a nested association, that it wasn't called directly by the user.
|
|
320
|
-
show.flags[:inner_assoc] = true
|
|
361
|
+
ret = show.safe_output(val)
|
|
321
362
|
|
|
322
|
-
|
|
363
|
+
raise "#{res_show} resolve failed" unless ret[0]
|
|
323
364
|
|
|
324
|
-
|
|
365
|
+
ret[1][res_show.output.namespace].update({
|
|
366
|
+
_meta: ret[1][:_meta].update(resolved: true)
|
|
367
|
+
})
|
|
325
368
|
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
328
381
|
|
|
329
|
-
|
|
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
|
|
330
399
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
400
|
+
def restore_context(action, action_instance, path)
|
|
401
|
+
@context.action = action
|
|
402
|
+
@context.action_instance = action_instance
|
|
403
|
+
@context.path = path
|
|
404
|
+
end
|
|
334
405
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
resolved: false
|
|
342
|
-
}
|
|
343
|
-
}
|
|
406
|
+
def show_prepared?(show)
|
|
407
|
+
completed = Object.new
|
|
408
|
+
ret = catch(:return) do
|
|
409
|
+
show.validate!
|
|
410
|
+
show.prepare
|
|
411
|
+
completed
|
|
344
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
|
+
}
|
|
345
424
|
end
|
|
346
425
|
|
|
347
426
|
# Should an association with `name` be resolved?
|