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
|
@@ -5,7 +5,7 @@ module HaveAPI::Parameters
|
|
|
5
5
|
|
|
6
6
|
def initialize(resource, name: nil, label: nil, desc: nil,
|
|
7
7
|
choices: nil, value_id: :id, value_label: :label, required: nil,
|
|
8
|
-
db_name: nil, fetch: nil)
|
|
8
|
+
db_name: nil, fetch: nil, nullable: nil)
|
|
9
9
|
@resource = resource
|
|
10
10
|
@resource_path = build_resource_path(resource)
|
|
11
11
|
@name = name || resource.resource_name.underscore.to_sym
|
|
@@ -15,6 +15,7 @@ module HaveAPI::Parameters
|
|
|
15
15
|
@value_id = value_id
|
|
16
16
|
@value_label = value_label
|
|
17
17
|
@required = required
|
|
18
|
+
@nullable = nullable
|
|
18
19
|
@db_name = db_name
|
|
19
20
|
@extra = {
|
|
20
21
|
fetch:
|
|
@@ -33,6 +34,10 @@ module HaveAPI::Parameters
|
|
|
33
34
|
!@required
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
def nullable?
|
|
38
|
+
@nullable == true && optional?
|
|
39
|
+
end
|
|
40
|
+
|
|
36
41
|
def show_action
|
|
37
42
|
@resource::Show
|
|
38
43
|
end
|
|
@@ -56,6 +61,7 @@ module HaveAPI::Parameters
|
|
|
56
61
|
|
|
57
62
|
{
|
|
58
63
|
required: required?,
|
|
64
|
+
nullable: nullable?,
|
|
59
65
|
label: @label,
|
|
60
66
|
description: @desc,
|
|
61
67
|
type: 'Resource',
|
|
@@ -91,17 +97,27 @@ module HaveAPI::Parameters
|
|
|
91
97
|
attrs.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
92
98
|
end
|
|
93
99
|
|
|
94
|
-
def clean(raw)
|
|
100
|
+
def clean(raw, context = nil)
|
|
101
|
+
if raw.nil?
|
|
102
|
+
return nil if nullable?
|
|
103
|
+
|
|
104
|
+
raise HaveAPI::ValidationError, 'cannot be null'
|
|
105
|
+
end
|
|
106
|
+
|
|
95
107
|
if raw.is_a?(String)
|
|
96
|
-
stripped = raw
|
|
97
|
-
return nil if stripped.empty? &&
|
|
108
|
+
stripped = strip_string(raw)
|
|
109
|
+
return nil if stripped.empty? && nullable?
|
|
98
110
|
end
|
|
99
111
|
|
|
100
|
-
extra = @extra.merge(optional: optional?)
|
|
112
|
+
extra = @extra.merge(optional: optional?, nullable: nullable?)
|
|
101
113
|
|
|
102
|
-
::HaveAPI::ModelAdapter.for(
|
|
114
|
+
ret = ::HaveAPI::ModelAdapter.for(
|
|
103
115
|
show_action.input.layout, @resource.model
|
|
104
116
|
).input_clean(@resource.model, raw, extra)
|
|
117
|
+
|
|
118
|
+
authorize_record!(ret, context)
|
|
119
|
+
|
|
120
|
+
ret
|
|
105
121
|
end
|
|
106
122
|
|
|
107
123
|
def validate(v, params)
|
|
@@ -114,6 +130,12 @@ module HaveAPI::Parameters
|
|
|
114
130
|
|
|
115
131
|
private
|
|
116
132
|
|
|
133
|
+
def strip_string(value)
|
|
134
|
+
value.strip
|
|
135
|
+
rescue ArgumentError, Encoding::CompatibilityError
|
|
136
|
+
raise HaveAPI::ValidationError, 'invalid string encoding'
|
|
137
|
+
end
|
|
138
|
+
|
|
117
139
|
def build_resource_path(r)
|
|
118
140
|
path = []
|
|
119
141
|
top_module = Kernel
|
|
@@ -132,5 +154,27 @@ module HaveAPI::Parameters
|
|
|
132
154
|
|
|
133
155
|
path
|
|
134
156
|
end
|
|
157
|
+
|
|
158
|
+
def authorize_record!(record, context)
|
|
159
|
+
return if record.nil? || context.nil?
|
|
160
|
+
return unless show_action.authorization
|
|
161
|
+
|
|
162
|
+
path = show_action.build_route('')
|
|
163
|
+
path_params = show_action.path_params(path, show_action.resolve_path_params(record))
|
|
164
|
+
child_context = HaveAPI::Context.new(
|
|
165
|
+
context.server,
|
|
166
|
+
version: context.version,
|
|
167
|
+
request: context.request,
|
|
168
|
+
action: show_action,
|
|
169
|
+
path:,
|
|
170
|
+
params: path_params,
|
|
171
|
+
user: context.current_user,
|
|
172
|
+
endpoint: context.endpoint
|
|
173
|
+
)
|
|
174
|
+
action = show_action.new(context.request, context.version, path_params, nil, child_context)
|
|
175
|
+
return if action.authorized?(context.current_user)
|
|
176
|
+
|
|
177
|
+
raise HaveAPI::ValidationError, 'resource not found'
|
|
178
|
+
end
|
|
135
179
|
end
|
|
136
180
|
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
require 'date'
|
|
2
|
+
require 'time'
|
|
2
3
|
|
|
3
4
|
module HaveAPI::Parameters
|
|
4
5
|
class Typed
|
|
5
|
-
ATTRIBUTES = %i[label desc type db_name default fill clean protected load_validators].freeze
|
|
6
|
+
ATTRIBUTES = %i[label desc type db_name default fill clean protected load_validators nullable].freeze
|
|
6
7
|
|
|
7
8
|
attr_reader :name, :label, :desc, :type, :default
|
|
8
9
|
|
|
@@ -36,6 +37,10 @@ module HaveAPI::Parameters
|
|
|
36
37
|
!required?
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def nullable?
|
|
41
|
+
@nullable == true && optional?
|
|
42
|
+
end
|
|
43
|
+
|
|
39
44
|
def fill?
|
|
40
45
|
@fill
|
|
41
46
|
end
|
|
@@ -47,6 +52,7 @@ module HaveAPI::Parameters
|
|
|
47
52
|
def describe(context)
|
|
48
53
|
{
|
|
49
54
|
required: required?,
|
|
55
|
+
nullable: nullable?,
|
|
50
56
|
label: @label,
|
|
51
57
|
description: @desc,
|
|
52
58
|
type: @type ? @type.to_s : String.to_s,
|
|
@@ -73,17 +79,20 @@ module HaveAPI::Parameters
|
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
def clean(raw)
|
|
76
|
-
return instance_exec(raw, &@clean) if @clean
|
|
82
|
+
return validate_cleaned_value(instance_exec(raw, &@clean)) if @clean
|
|
77
83
|
|
|
78
|
-
if raw.
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
if raw.nil?
|
|
85
|
+
return nil if nullable?
|
|
86
|
+
|
|
87
|
+
raise HaveAPI::ValidationError, 'cannot be null'
|
|
81
88
|
end
|
|
82
89
|
|
|
83
|
-
if raw.
|
|
84
|
-
|
|
90
|
+
if raw.is_a?(String)
|
|
91
|
+
stripped = strip_string(raw)
|
|
92
|
+
return nil if stripped.empty? && nullable?
|
|
93
|
+
end
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
if @type.nil?
|
|
87
96
|
nil
|
|
88
97
|
|
|
89
98
|
elsif @type == Integer
|
|
@@ -136,6 +145,20 @@ module HaveAPI::Parameters
|
|
|
136
145
|
|
|
137
146
|
private
|
|
138
147
|
|
|
148
|
+
def validate_cleaned_value(value)
|
|
149
|
+
if value.nil? && !nullable?
|
|
150
|
+
raise HaveAPI::ValidationError, 'cannot be null'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def strip_string(value)
|
|
157
|
+
value.strip
|
|
158
|
+
rescue ArgumentError, Encoding::CompatibilityError
|
|
159
|
+
raise HaveAPI::ValidationError, 'invalid string encoding'
|
|
160
|
+
end
|
|
161
|
+
|
|
139
162
|
def coerce_integer(raw)
|
|
140
163
|
case raw
|
|
141
164
|
when Integer
|
|
@@ -147,7 +170,7 @@ module HaveAPI::Parameters
|
|
|
147
170
|
|
|
148
171
|
raw.to_i
|
|
149
172
|
when String
|
|
150
|
-
s = raw
|
|
173
|
+
s = strip_string(raw)
|
|
151
174
|
|
|
152
175
|
if s.empty? || !s.match?(/\A[+-]?\d+\z/)
|
|
153
176
|
raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
|
|
@@ -164,7 +187,7 @@ module HaveAPI::Parameters
|
|
|
164
187
|
f = raw.to_f
|
|
165
188
|
|
|
166
189
|
elsif raw.is_a?(String)
|
|
167
|
-
s = raw
|
|
190
|
+
s = strip_string(raw)
|
|
168
191
|
raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}" if s.empty?
|
|
169
192
|
|
|
170
193
|
begin
|
|
@@ -191,7 +214,7 @@ module HaveAPI::Parameters
|
|
|
191
214
|
return true if raw == 1
|
|
192
215
|
|
|
193
216
|
elsif raw.is_a?(String)
|
|
194
|
-
s = raw
|
|
217
|
+
s = strip_string(raw)
|
|
195
218
|
raise HaveAPI::ValidationError, "not a valid boolean #{raw.inspect}" if s.empty?
|
|
196
219
|
|
|
197
220
|
return true if %w[true t yes y 1].include?(s.downcase)
|
|
@@ -202,12 +225,16 @@ module HaveAPI::Parameters
|
|
|
202
225
|
end
|
|
203
226
|
|
|
204
227
|
def coerce_datetime(raw)
|
|
205
|
-
|
|
228
|
+
unless raw.is_a?(String)
|
|
229
|
+
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if strip_string(raw).empty?
|
|
206
233
|
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
207
234
|
end
|
|
208
235
|
|
|
209
236
|
DateTime.iso8601(raw).to_time
|
|
210
|
-
rescue ArgumentError
|
|
237
|
+
rescue ArgumentError, TypeError
|
|
211
238
|
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
212
239
|
end
|
|
213
240
|
|
data/lib/haveapi/params.rb
CHANGED
|
@@ -134,12 +134,12 @@ module HaveAPI
|
|
|
134
134
|
|
|
135
135
|
if include
|
|
136
136
|
@include ||= []
|
|
137
|
-
@include.concat(include)
|
|
137
|
+
@include.concat(normalize_names(include))
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
if exclude
|
|
141
141
|
@exclude ||= []
|
|
142
|
-
@exclude.concat(exclude)
|
|
142
|
+
@exclude.concat(normalize_names(exclude))
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
instance_eval(&block)
|
|
@@ -209,10 +209,17 @@ module HaveAPI
|
|
|
209
209
|
# First step of validation. Check if input is in correct namespace
|
|
210
210
|
# and has a correct layout.
|
|
211
211
|
def check_layout(params)
|
|
212
|
-
|
|
212
|
+
value = namespace ? params[namespace] : params
|
|
213
|
+
|
|
214
|
+
if value.nil?
|
|
215
|
+
raise ValidationError.new('invalid input layout', {}) if any_required_params?
|
|
216
|
+
|
|
217
|
+
elsif !valid_layout?(value)
|
|
213
218
|
raise ValidationError.new('invalid input layout', {})
|
|
214
219
|
end
|
|
215
220
|
|
|
221
|
+
return unless namespace
|
|
222
|
+
|
|
216
223
|
case layout
|
|
217
224
|
when :object, :hash
|
|
218
225
|
params[namespace] ||= {}
|
|
@@ -224,12 +231,15 @@ module HaveAPI
|
|
|
224
231
|
|
|
225
232
|
# Third step of validation. Check if all required params are present,
|
|
226
233
|
# convert params to correct data types, set default values if necessary.
|
|
227
|
-
def validate(params)
|
|
234
|
+
def validate(params, context: nil, only: nil)
|
|
228
235
|
errors = {}
|
|
236
|
+
permitted = only && only.map(&:to_sym)
|
|
229
237
|
|
|
230
238
|
layout_aware(params) do |input|
|
|
231
239
|
# First run - coerce values to correct types
|
|
232
240
|
@params.each do |p|
|
|
241
|
+
next if permitted && !permitted.include?(p.name)
|
|
242
|
+
|
|
233
243
|
if p.required? && input[p.name].nil?
|
|
234
244
|
errors[p.name] = ['required parameter missing']
|
|
235
245
|
next
|
|
@@ -241,7 +251,11 @@ module HaveAPI
|
|
|
241
251
|
end
|
|
242
252
|
|
|
243
253
|
begin
|
|
244
|
-
cleaned = p.clean
|
|
254
|
+
cleaned = if p.method(:clean).arity.abs > 1
|
|
255
|
+
p.clean(input[p.name], context)
|
|
256
|
+
else
|
|
257
|
+
p.clean(input[p.name])
|
|
258
|
+
end
|
|
245
259
|
rescue ValidationError => e
|
|
246
260
|
errors[p.name] ||= []
|
|
247
261
|
errors[p.name] << e.message
|
|
@@ -253,6 +267,7 @@ module HaveAPI
|
|
|
253
267
|
|
|
254
268
|
# Second run - validate parameters
|
|
255
269
|
@params.each do |p|
|
|
270
|
+
next if permitted && !permitted.include?(p.name)
|
|
256
271
|
next if errors.has_key?(p.name)
|
|
257
272
|
next if input[p.name].nil?
|
|
258
273
|
|
|
@@ -305,13 +320,17 @@ module HaveAPI
|
|
|
305
320
|
kwargs
|
|
306
321
|
end
|
|
307
322
|
|
|
308
|
-
def
|
|
323
|
+
def normalize_names(names)
|
|
324
|
+
names.map { |v| v.is_a?(String) ? v.to_sym : v }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def valid_layout?(value)
|
|
309
328
|
case layout
|
|
310
329
|
when :object, :hash
|
|
311
|
-
|
|
330
|
+
value.is_a?(Hash)
|
|
312
331
|
|
|
313
332
|
when :object_list, :hash_list
|
|
314
|
-
|
|
333
|
+
value.is_a?(Array) && value.all?(Hash)
|
|
315
334
|
|
|
316
335
|
else
|
|
317
336
|
false
|
data/lib/haveapi/resource.rb
CHANGED
|
@@ -98,7 +98,10 @@ module HaveAPI
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
hash[:resources].each do |resource, children|
|
|
101
|
-
|
|
101
|
+
child = resource.describe(children, context)
|
|
102
|
+
next if child[:actions].empty? && child[:resources].empty?
|
|
103
|
+
|
|
104
|
+
ret[:resources][resource.resource_name.underscore] = child
|
|
102
105
|
end
|
|
103
106
|
|
|
104
107
|
context.resource_path = orig_resource_path
|
|
@@ -77,18 +77,22 @@ module HaveAPI::Resources
|
|
|
77
77
|
class Poll < HaveAPI::Action
|
|
78
78
|
include Mixin
|
|
79
79
|
|
|
80
|
+
MAX_TIMEOUT = 30
|
|
81
|
+
|
|
80
82
|
desc 'Returns when the action is completed or timeout occurs'
|
|
81
83
|
http_method :get
|
|
82
84
|
route '{%{resource}_id}/poll'
|
|
83
85
|
|
|
84
86
|
input(:hash) do
|
|
85
|
-
float :timeout, label: 'Timeout', desc: 'in seconds', default: 15, fill: true
|
|
87
|
+
float :timeout, label: 'Timeout', desc: 'in seconds', default: 15, fill: true,
|
|
88
|
+
number: { min: 0, max: MAX_TIMEOUT }
|
|
86
89
|
float :update_in, label: 'Progress',
|
|
87
90
|
desc: 'number of seconds after which the state is returned if the progress ' \
|
|
88
|
-
'has changed'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
integer :
|
|
91
|
+
'has changed',
|
|
92
|
+
nullable: true
|
|
93
|
+
bool :status, desc: 'status to check with if update_in is set', nullable: true
|
|
94
|
+
integer :current, desc: 'progress to check with if update_in is set', nullable: true
|
|
95
|
+
integer :total, desc: 'progress to check with if update_in is set', nullable: true
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
output(:hash) do
|
|
@@ -98,6 +102,10 @@ module HaveAPI::Resources
|
|
|
98
102
|
authorize { allow }
|
|
99
103
|
|
|
100
104
|
def exec
|
|
105
|
+
if input[:timeout] > MAX_TIMEOUT
|
|
106
|
+
error!("timeout has to be maximally #{MAX_TIMEOUT}", {}, http_status: 400)
|
|
107
|
+
end
|
|
108
|
+
|
|
101
109
|
t = Time.now
|
|
102
110
|
|
|
103
111
|
loop do
|
data/lib/haveapi/route.rb
CHANGED
|
@@ -3,8 +3,8 @@ module HaveAPI
|
|
|
3
3
|
attr_reader :path, :sinatra_path, :action, :resource_path
|
|
4
4
|
|
|
5
5
|
def initialize(path, action, resource_path)
|
|
6
|
-
@
|
|
7
|
-
@
|
|
6
|
+
@sinatra_path = path.gsub(/:([a-zA-Z0-9\-_]+)/, '{\1}')
|
|
7
|
+
@path = @sinatra_path
|
|
8
8
|
@action = action
|
|
9
9
|
@resource_path = resource_path
|
|
10
10
|
end
|