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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/haveapi.gemspec +1 -1
  4. data/lib/haveapi/action.rb +125 -36
  5. data/lib/haveapi/actions/paginable.rb +3 -1
  6. data/lib/haveapi/authentication/basic/provider.rb +2 -0
  7. data/lib/haveapi/authentication/chain.rb +11 -7
  8. data/lib/haveapi/authentication/oauth2/config.rb +25 -3
  9. data/lib/haveapi/authentication/oauth2/provider.rb +92 -11
  10. data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +44 -3
  11. data/lib/haveapi/authentication/token/provider.rb +53 -15
  12. data/lib/haveapi/authorization.rb +42 -18
  13. data/lib/haveapi/client_examples/php_client.rb +1 -1
  14. data/lib/haveapi/client_examples/ruby_client.rb +1 -1
  15. data/lib/haveapi/context.rb +10 -4
  16. data/lib/haveapi/example.rb +15 -16
  17. data/lib/haveapi/extensions/action_exceptions.rb +6 -6
  18. data/lib/haveapi/model_adapters/active_record.rb +140 -68
  19. data/lib/haveapi/model_adapters/hash.rb +1 -1
  20. data/lib/haveapi/parameters/resource.rb +35 -3
  21. data/lib/haveapi/parameters/typed.rb +26 -7
  22. data/lib/haveapi/params.rb +27 -8
  23. data/lib/haveapi/resource.rb +4 -1
  24. data/lib/haveapi/resources/action_state.rb +8 -1
  25. data/lib/haveapi/route.rb +2 -2
  26. data/lib/haveapi/server.rb +137 -45
  27. data/lib/haveapi/validator.rb +2 -2
  28. data/lib/haveapi/validator_chain.rb +1 -0
  29. data/lib/haveapi/validators/confirmation.rb +1 -0
  30. data/lib/haveapi/validators/format.rb +6 -2
  31. data/lib/haveapi/validators/length.rb +2 -0
  32. data/lib/haveapi/validators/numericality.rb +2 -0
  33. data/lib/haveapi/validators/presence.rb +1 -1
  34. data/lib/haveapi/version.rb +1 -1
  35. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  36. data/lib/haveapi/views/version_page/client_example.erb +3 -3
  37. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  38. data/lib/haveapi/views/version_page.erb +2 -2
  39. data/lib/haveapi/views/version_sidebar.erb +4 -2
  40. data/spec/action/authorize_spec.rb +99 -0
  41. data/spec/action/runtime_spec.rb +426 -0
  42. data/spec/action_state_spec.rb +52 -0
  43. data/spec/authentication/basic_spec.rb +29 -0
  44. data/spec/authentication/oauth2_spec.rb +329 -0
  45. data/spec/authentication/token_spec.rb +195 -0
  46. data/spec/authentication/token_version_routes_spec.rb +164 -0
  47. data/spec/authorization_spec.rb +66 -0
  48. data/spec/documentation/auth_filtering_spec.rb +195 -1
  49. data/spec/documentation/current_user_html_escaping_spec.rb +47 -0
  50. data/spec/documentation/examples_spec.rb +97 -0
  51. data/spec/documentation/host_html_escaping_spec.rb +41 -0
  52. data/spec/documentation_spec.rb +13 -0
  53. data/spec/extensions/action_exceptions_spec.rb +30 -0
  54. data/spec/model_adapters/active_record_spec.rb +406 -1
  55. data/spec/parameters/typed_spec.rb +42 -0
  56. data/spec/params_spec.rb +41 -0
  57. data/spec/server/integration_spec.rb +90 -0
  58. data/spec/validator_chain_spec.rb +39 -0
  59. data/spec/validators/confirmation_spec.rb +14 -0
  60. data/spec/validators/format_spec.rb +7 -0
  61. data/spec/validators/length_spec.rb +6 -0
  62. data/spec/validators/numericality_spec.rb +7 -0
  63. data/spec/validators/presence_spec.rb +2 -0
  64. data/test_support/client_test_api.rb +28 -0
  65. metadata +8 -4
  66. data/shell.nix +0 -20
@@ -97,7 +97,7 @@ module HaveAPI::Parameters
97
97
  attrs.each { |k, v| instance_variable_set("@#{k}", v) }
98
98
  end
99
99
 
100
- def clean(raw)
100
+ def clean(raw, context = nil)
101
101
  if raw.nil?
102
102
  return nil if nullable?
103
103
 
@@ -105,15 +105,19 @@ module HaveAPI::Parameters
105
105
  end
106
106
 
107
107
  if raw.is_a?(String)
108
- stripped = raw.strip
108
+ stripped = strip_string(raw)
109
109
  return nil if stripped.empty? && nullable?
110
110
  end
111
111
 
112
112
  extra = @extra.merge(optional: optional?, nullable: nullable?)
113
113
 
114
- ::HaveAPI::ModelAdapter.for(
114
+ ret = ::HaveAPI::ModelAdapter.for(
115
115
  show_action.input.layout, @resource.model
116
116
  ).input_clean(@resource.model, raw, extra)
117
+
118
+ authorize_record!(ret, context)
119
+
120
+ ret
117
121
  end
118
122
 
119
123
  def validate(v, params)
@@ -126,6 +130,12 @@ module HaveAPI::Parameters
126
130
 
127
131
  private
128
132
 
133
+ def strip_string(value)
134
+ value.strip
135
+ rescue ArgumentError, Encoding::CompatibilityError
136
+ raise HaveAPI::ValidationError, 'invalid string encoding'
137
+ end
138
+
129
139
  def build_resource_path(r)
130
140
  path = []
131
141
  top_module = Kernel
@@ -144,5 +154,27 @@ module HaveAPI::Parameters
144
154
 
145
155
  path
146
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
147
179
  end
148
180
  end
@@ -1,4 +1,5 @@
1
1
  require 'date'
2
+ require 'time'
2
3
 
3
4
  module HaveAPI::Parameters
4
5
  class Typed
@@ -78,7 +79,7 @@ module HaveAPI::Parameters
78
79
  end
79
80
 
80
81
  def clean(raw)
81
- return instance_exec(raw, &@clean) if @clean
82
+ return validate_cleaned_value(instance_exec(raw, &@clean)) if @clean
82
83
 
83
84
  if raw.nil?
84
85
  return nil if nullable?
@@ -87,7 +88,7 @@ module HaveAPI::Parameters
87
88
  end
88
89
 
89
90
  if raw.is_a?(String)
90
- stripped = raw.strip
91
+ stripped = strip_string(raw)
91
92
  return nil if stripped.empty? && nullable?
92
93
  end
93
94
 
@@ -144,6 +145,20 @@ module HaveAPI::Parameters
144
145
 
145
146
  private
146
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
+
147
162
  def coerce_integer(raw)
148
163
  case raw
149
164
  when Integer
@@ -155,7 +170,7 @@ module HaveAPI::Parameters
155
170
 
156
171
  raw.to_i
157
172
  when String
158
- s = raw.strip
173
+ s = strip_string(raw)
159
174
 
160
175
  if s.empty? || !s.match?(/\A[+-]?\d+\z/)
161
176
  raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
@@ -172,7 +187,7 @@ module HaveAPI::Parameters
172
187
  f = raw.to_f
173
188
 
174
189
  elsif raw.is_a?(String)
175
- s = raw.strip
190
+ s = strip_string(raw)
176
191
  raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}" if s.empty?
177
192
 
178
193
  begin
@@ -199,7 +214,7 @@ module HaveAPI::Parameters
199
214
  return true if raw == 1
200
215
 
201
216
  elsif raw.is_a?(String)
202
- s = raw.strip
217
+ s = strip_string(raw)
203
218
  raise HaveAPI::ValidationError, "not a valid boolean #{raw.inspect}" if s.empty?
204
219
 
205
220
  return true if %w[true t yes y 1].include?(s.downcase)
@@ -210,12 +225,16 @@ module HaveAPI::Parameters
210
225
  end
211
226
 
212
227
  def coerce_datetime(raw)
213
- if raw.is_a?(String) && raw.strip.empty?
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?
214
233
  raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
215
234
  end
216
235
 
217
236
  DateTime.iso8601(raw).to_time
218
- rescue ArgumentError
237
+ rescue ArgumentError, TypeError
219
238
  raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
220
239
  end
221
240
 
@@ -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
- if (params[namespace].nil? || !valid_layout?(params)) && any_required_params?
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(input[p.name])
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 valid_layout?(params)
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
- params[namespace].is_a?(Hash)
330
+ value.is_a?(Hash)
312
331
 
313
332
  when :object_list, :hash_list
314
- params[namespace].is_a?(Array)
333
+ value.is_a?(Array) && value.all?(Hash)
315
334
 
316
335
  else
317
336
  false
@@ -98,7 +98,10 @@ module HaveAPI
98
98
  end
99
99
 
100
100
  hash[:resources].each do |resource, children|
101
- ret[:resources][resource.resource_name.underscore] = resource.describe(children, context)
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,12 +77,15 @@ 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
91
  'has changed',
@@ -99,6 +102,10 @@ module HaveAPI::Resources
99
102
  authorize { allow }
100
103
 
101
104
  def exec
105
+ if input[:timeout] > MAX_TIMEOUT
106
+ error!("timeout has to be maximally #{MAX_TIMEOUT}", {}, http_status: 400)
107
+ end
108
+
102
109
  t = Time.now
103
110
 
104
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
- @path = path
7
- @sinatra_path = path.gsub(/:([a-zA-Z\-_]+)/, '{\1}')
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
@@ -49,8 +49,12 @@ module HaveAPI
49
49
  return if @formatter
50
50
 
51
51
  @formatter = OutputFormatter.new
52
-
53
- unless @formatter.supports?(request.accept)
52
+ accept = request.accept
53
+ rescue ArgumentError, EncodingError
54
+ @formatter.supports?([])
55
+ report_error(400, {}, 'Bad Accept header')
56
+ else
57
+ unless @formatter.supports?(accept)
54
58
  @halted = true
55
59
  halt 406, "Not Acceptable\n"
56
60
  end
@@ -79,6 +83,19 @@ module HaveAPI
79
83
  @current_user
80
84
  end
81
85
 
86
+ def authenticated_versions
87
+ settings.api_server.versions.each_with_object({}) do |v, ret|
88
+ ret[v] = settings.api_server.send(:do_authenticate, v, request)
89
+ rescue HaveAPI::Authentication::TokenConflict => e
90
+ unless @formatter
91
+ @formatter = OutputFormatter.new
92
+ @formatter.supports?([])
93
+ end
94
+
95
+ report_error(400, {}, e.message)
96
+ end
97
+ end
98
+
82
99
  def access_control
83
100
  return unless request.env['HTTP_ORIGIN'] && request.env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
84
101
 
@@ -110,6 +127,10 @@ module HaveAPI
110
127
 
111
128
  def report_error(code, headers, msg)
112
129
  @halted = true
130
+ unless @formatter
131
+ @formatter = OutputFormatter.new
132
+ @formatter.supports?([])
133
+ end
113
134
 
114
135
  content_type @formatter.content_type, charset: 'utf-8'
115
136
  halt code, headers, @formatter.format(false, nil, msg, version: false)
@@ -172,10 +193,14 @@ module HaveAPI
172
193
  return ret if validators.nil?
173
194
 
174
195
  validators.each do |name, opts|
175
- ret += "<h5>#{name.to_s.capitalize}</h5>"
196
+ ret += "<h5>#{escape_html(name.to_s.capitalize)}</h5>"
176
197
  ret += '<dl>'
177
- opts.each do |k, v|
178
- ret += "<dt>#{k}</dt><dd>#{escape_html(v.to_s)}</dd>"
198
+ if opts.respond_to?(:each_pair)
199
+ opts.each_pair do |k, v|
200
+ ret += "<dt>#{escape_html(k)}</dt><dd>#{escape_html(v.to_s)}</dd>"
201
+ end
202
+ else
203
+ ret += "<dt>description</dt><dd>#{escape_html(opts.to_s)}</dd>"
179
204
  end
180
205
  ret += '</dl>'
181
206
  end
@@ -270,12 +295,13 @@ module HaveAPI
270
295
 
271
296
  # Mount root
272
297
  @sinatra.get @root do
273
- authenticated?(settings.api_server.default_version)
298
+ auth_users_by_version = authenticated_versions
274
299
 
275
300
  @api = settings.api_server.describe(Context.new(
276
301
  settings.api_server,
277
- user: current_user,
278
- params:
302
+ user: auth_users_by_version[settings.api_server.default_version],
303
+ params:,
304
+ auth_users_by_version:
279
305
  ))
280
306
 
281
307
  content_type 'text/html'
@@ -285,7 +311,6 @@ module HaveAPI
285
311
  @sinatra.options @root do
286
312
  setup_formatter
287
313
  access_control
288
- authenticated?(settings.api_server.default_version)
289
314
  ret = nil
290
315
 
291
316
  ret = case params[:describe]
@@ -296,20 +321,26 @@ module HaveAPI
296
321
  }
297
322
 
298
323
  when 'default'
324
+ auth_users_by_version = authenticated_versions
325
+
299
326
  settings.api_server.describe_version(Context.new(
300
327
  settings.api_server,
301
328
  version: settings.api_server.default_version,
302
- user: current_user,
329
+ user: auth_users_by_version[settings.api_server.default_version],
303
330
  doc: true,
304
- params:
331
+ params:,
332
+ auth_users_by_version:
305
333
  ))
306
334
 
307
335
  else
336
+ auth_users_by_version = authenticated_versions
337
+
308
338
  settings.api_server.describe(Context.new(
309
339
  settings.api_server,
310
- user: current_user,
340
+ user: auth_users_by_version[settings.api_server.default_version],
311
341
  doc: true,
312
- params:
342
+ params:,
343
+ auth_users_by_version:
313
344
  ))
314
345
  end
315
346
 
@@ -340,7 +371,7 @@ module HaveAPI
340
371
  end
341
372
  end
342
373
 
343
- @sinatra.get %r{#{@root}doc/([^\.]+)(\.md)?} do |f, _|
374
+ @sinatra.get %r{#{@root}doc/([^.]+)(\.md)?} do |f, _|
344
375
  content_type 'text/html'
345
376
  erb :doc_layout, layout: :main_layout do
346
377
  begin
@@ -349,7 +380,11 @@ module HaveAPI
349
380
  halt 404
350
381
  end
351
382
 
352
- @sidebar = erb :"doc_sidebars/#{f}"
383
+ begin
384
+ @sidebar = erb :"doc_sidebars/#{f}"
385
+ rescue Errno::ENOENT
386
+ @sidebar = ''
387
+ end
353
388
  end
354
389
  end
355
390
 
@@ -481,35 +516,45 @@ module HaveAPI
481
516
  @sinatra.method(route.http_method).call(route.sinatra_path) do
482
517
  setup_formatter
483
518
 
484
- if route.action.auth
519
+ if route.action.auth || settings.api_server.action_state_auth_required?(route)
485
520
  authenticate!(v)
486
521
  else
487
522
  authenticated?(v)
488
523
  end
489
524
 
490
- begin
491
- body = request.body.read
525
+ raw_body = request.body.read
526
+ body_method = !%i[get head options].include?(route.http_method.to_sym)
492
527
 
493
- body = if body.empty?
494
- nil
495
- else
496
- JSON.parse(body, symbolize_names: true)
497
- end
498
- rescue StandardError => e
528
+ if body_method && !raw_body.empty? && !settings.api_server.send(:json_content_type?, request)
529
+ report_error(415, {}, 'Unsupported Content-Type')
530
+ end
531
+
532
+ begin
533
+ body = raw_body.empty? ? nil : JSON.parse(raw_body, symbolize_names: true)
534
+ rescue JSON::ParserError
499
535
  report_error(400, {}, 'Bad JSON syntax')
500
536
  end
501
537
 
502
- action = route.action.new(request, v, params, body, Context.new(
503
- settings.api_server,
504
- version: v,
505
- request: self,
506
- action: route.action,
507
- path: route.path,
508
- params:,
509
- user: current_user,
510
- endpoint: true,
511
- resource_path: route.resource_path
512
- ))
538
+ if !raw_body.empty? && !body.is_a?(Hash)
539
+ report_error(400, {}, 'JSON body must be an object')
540
+ end
541
+
542
+ action_params = body_method ? settings.api_server.send(:path_params, route, params) : params
543
+ context_params = body ? action_params.merge(body) : action_params
544
+
545
+ context = Context.new(
546
+ settings.api_server,
547
+ version: v,
548
+ request: self,
549
+ action: route.action,
550
+ path: route.path,
551
+ params: context_params,
552
+ user: current_user,
553
+ endpoint: true,
554
+ resource_path: route.resource_path
555
+ )
556
+
557
+ action = route.action.new(request, v, action_params, body, context)
513
558
 
514
559
  unless action.authorized?(current_user)
515
560
  report_error(403, {}, 'Access denied. Insufficient permissions.')
@@ -537,7 +582,7 @@ module HaveAPI
537
582
 
538
583
  pass if params[:method] && params[:method] != route_method
539
584
 
540
- if route.action.auth
585
+ if route.action.auth || settings.api_server.action_state_auth_required?(route)
541
586
  authenticate!(v)
542
587
  else
543
588
  authenticated?(v)
@@ -563,6 +608,8 @@ module HaveAPI
563
608
  unless desc
564
609
  report_error(403, {}, 'Access denied. Insufficient permissions.')
565
610
  end
611
+ rescue ValidationError => e
612
+ report_error(400, e.to_hash, e.message)
566
613
  rescue StandardError => e
567
614
  tmp = settings.api_server.call_hooks_for(:description_exception, args: [ctx, e])
568
615
  report_error(
@@ -577,19 +624,28 @@ module HaveAPI
577
624
  end
578
625
 
579
626
  def describe(context)
580
- context.version = @default_version
627
+ original_user = context.current_user
628
+ auth_users_by_version = context.auth_users_by_version
629
+ authenticated_description = auth_users_by_version&.values&.any?
581
630
 
582
- ret = {
583
- default_version: @default_version,
584
- versions: { default: describe_version(context) }
585
- }
631
+ ret = { default_version: @default_version, versions: {} }
632
+
633
+ context.version = @default_version
634
+ context.current_user = auth_users_by_version ? auth_users_by_version[@default_version] : original_user
635
+ ret[:versions][:default] = describe_version(context) unless authenticated_description && context.current_user.nil?
586
636
 
587
637
  @versions.each do |v|
638
+ user = auth_users_by_version ? auth_users_by_version[v] : original_user
639
+ next if authenticated_description && user.nil?
640
+
588
641
  context.version = v
642
+ context.current_user = user
589
643
  ret[:versions][v] = describe_version(context)
590
644
  end
591
645
 
592
646
  ret
647
+ ensure
648
+ context.current_user = original_user
593
649
  end
594
650
 
595
651
  def describe_version(context)
@@ -618,6 +674,12 @@ module HaveAPI
618
674
  r.describe(hash, context)
619
675
  end
620
676
 
677
+ def action_state_auth_required?(route)
678
+ return false if @auth_chain.empty?
679
+
680
+ route.action.resource == HaveAPI::Resources::ActionState
681
+ end
682
+
621
683
  def version_prefix(v)
622
684
  "#{@root}v#{v}/"
623
685
  end
@@ -625,15 +687,45 @@ module HaveAPI
625
687
  # @param v [String] API version
626
688
  # @param provider [Authentication::Base]
627
689
  # @param prefix [String]
628
- def add_auth_routes(v, provider, prefix: '')
629
- provider.register_routes(@sinatra, "#{@root}_auth/#{prefix}")
690
+ def add_auth_routes(v, provider, prefix: '', global: false)
691
+ provider.register_routes(@sinatra, auth_prefix(v, prefix, global:))
630
692
  end
631
693
 
632
- def add_auth_module(v, name, mod, prefix: '')
694
+ def add_auth_module(v, name, mod, prefix: '', global: false)
633
695
  @routes[v] ||= { authentication: { name => { resources: {} } } }
634
696
 
635
697
  HaveAPI.get_version_resources(mod, v).each do |r|
636
- mount_resource("#{@root}_auth/#{prefix}/", v, r, @routes[v][:authentication][name][:resources])
698
+ mount_resource("#{auth_prefix(v, prefix, global:)}/", v, r, @routes[v][:authentication][name][:resources])
699
+ end
700
+ end
701
+
702
+ def auth_prefix(v, prefix, global:)
703
+ root = global ? "#{@root}_auth" : "#{version_prefix(v)}_auth"
704
+ "#{root}/#{prefix}"
705
+ end
706
+
707
+ def json_content_type?(request)
708
+ media_type = if request.respond_to?(:media_type)
709
+ request.media_type
710
+ else
711
+ request.content_type.to_s.split(';').first
712
+ end
713
+
714
+ media_type == 'application/json' || media_type.to_s.end_with?('+json')
715
+ end
716
+
717
+ def path_params(route, params)
718
+ route.action.path_param_names(route.path).each_with_object({}) do |name, ret|
719
+ value = if params.has_key?(name.to_sym)
720
+ params[name.to_sym]
721
+ elsif params.has_key?(name)
722
+ params[name]
723
+ end
724
+
725
+ next if value.nil?
726
+
727
+ ret[name] = value
728
+ ret[name.to_sym] = value
637
729
  end
638
730
  end
639
731
 
@@ -89,9 +89,9 @@ module HaveAPI
89
89
  # may use this information as it will.
90
90
  def validate(v, params)
91
91
  @params = params
92
- ret = valid?(v)
92
+ valid?(v)
93
+ ensure
93
94
  @params = nil
94
- ret
95
95
  end
96
96
 
97
97
  protected
@@ -69,6 +69,7 @@ module HaveAPI
69
69
  ret = []
70
70
 
71
71
  @validators.each do |validator|
72
+ validator = validator.clone
72
73
  next if validator.validate(value, params)
73
74
 
74
75
  ret << format(validator.message, value:)
@@ -20,6 +20,7 @@ module HaveAPI
20
20
 
21
21
  def setup
22
22
  @param = simple? ? take : take(:param)
23
+ @param = @param.to_sym if @param.is_a?(::String)
23
24
  @equal = take(:equal, true)
24
25
  @message = take(
25
26
  :message,
@@ -33,11 +33,15 @@ module HaveAPI
33
33
  end
34
34
 
35
35
  def valid?(v)
36
+ return false unless v.respond_to?(:to_str)
37
+
38
+ matched = @rx.match?(v.to_str)
39
+
36
40
  if @match
37
- @rx.match(v) ? true : false
41
+ matched
38
42
 
39
43
  else
40
- @rx.match(v) ? false : true
44
+ !matched
41
45
  end
42
46
  end
43
47
  end
@@ -62,6 +62,8 @@ module HaveAPI
62
62
  end
63
63
 
64
64
  def valid?(v)
65
+ return false unless v.respond_to?(:length)
66
+
65
67
  len = v.length
66
68
 
67
69
  return len == @equals if @equals