openapi_first 1.0.0.beta1 → 1.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +8 -20
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +23 -0
  5. data/Gemfile +4 -1
  6. data/Gemfile.lock +39 -51
  7. data/README.md +27 -25
  8. data/benchmarks/Gemfile.lock +28 -33
  9. data/benchmarks/apps/openapi_first_with_hanami_api.ru +1 -2
  10. data/benchmarks/apps/openapi_first_with_plain_rack.ru +32 -0
  11. data/benchmarks/apps/openapi_first_with_response_validation.ru +14 -11
  12. data/benchmarks/apps/openapi_first_with_sinatra.ru +29 -0
  13. data/lib/openapi_first/body_parser_middleware.rb +1 -1
  14. data/lib/openapi_first/config.rb +19 -0
  15. data/lib/openapi_first/default_error_response.rb +47 -0
  16. data/lib/openapi_first/definition.rb +8 -1
  17. data/lib/openapi_first/error_response.rb +31 -0
  18. data/lib/openapi_first/errors.rb +3 -40
  19. data/lib/openapi_first/operation.rb +33 -14
  20. data/lib/openapi_first/operation_schemas.rb +52 -0
  21. data/lib/openapi_first/plugins.rb +17 -0
  22. data/lib/openapi_first/request_body_validator.rb +41 -0
  23. data/lib/openapi_first/request_validation.rb +66 -84
  24. data/lib/openapi_first/response_validation.rb +38 -7
  25. data/lib/openapi_first/response_validator.rb +1 -1
  26. data/lib/openapi_first/router.rb +15 -14
  27. data/lib/openapi_first/schema_validation.rb +22 -21
  28. data/lib/openapi_first/string_keyed_hash.rb +20 -0
  29. data/lib/openapi_first/validation_result.rb +15 -0
  30. data/lib/openapi_first/version.rb +1 -1
  31. data/lib/openapi_first.rb +30 -21
  32. data/openapi_first.gemspec +4 -12
  33. metadata +20 -117
  34. data/benchmarks/apps/openapi_first.ru +0 -22
  35. data/lib/openapi_first/utils.rb +0 -35
  36. data/lib/openapi_first/validation_format.rb +0 -55
  37. /data/benchmarks/apps/{committee.ru → committee_with_hanami_api.ru} +0 -0
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class DefaultErrorResponse < ErrorResponse
5
+ OpenapiFirst::Plugins.register_error_response(:default, self)
6
+
7
+ def body
8
+ MultiJson.dump({ errors: serialized_errors })
9
+ end
10
+
11
+ def serialized_errors
12
+ return default_errors unless validation_output
13
+
14
+ key = pointer_key
15
+ [
16
+ {
17
+ source: { key => pointer(validation_output['instanceLocation']) },
18
+ title: validation_output['error']
19
+ }
20
+ ]
21
+ end
22
+
23
+ def default_errors
24
+ [{
25
+ status: status.to_s,
26
+ title:
27
+ }]
28
+ end
29
+
30
+ def pointer_key
31
+ case location
32
+ when :request_body
33
+ :pointer
34
+ when :query, :path
35
+ :parameter
36
+ else
37
+ location
38
+ end
39
+ end
40
+
41
+ def pointer(data_pointer)
42
+ return data_pointer if location == :request_body
43
+
44
+ data_pointer.delete_prefix('/')
45
+ end
46
+ end
47
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative 'operation'
4
4
 
5
5
  module OpenapiFirst
6
+ # Represents an OpenAPI API Description document
6
7
  class Definition
7
8
  attr_reader :filepath, :operations
8
9
 
@@ -11,9 +12,15 @@ module OpenapiFirst
11
12
  methods = %w[get head post put patch delete trace options]
12
13
  @operations = resolved['paths'].flat_map do |path, path_item|
13
14
  path_item.slice(*methods).map do |request_method, _operation_object|
14
- Operation.new(path, request_method, path_item)
15
+ Operation.new(path, request_method, path_item, openapi_version: detect_version(resolved))
15
16
  end
16
17
  end
17
18
  end
19
+
20
+ private
21
+
22
+ def detect_version(resolved)
23
+ (resolved['openapi'] || resolved['swagger'])[0..2]
24
+ end
18
25
  end
19
26
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ # This is the base class for error responses
5
+ class ErrorResponse
6
+ ## @param status [Integer] The HTTP status code.
7
+ ## @param title [String] The title of the error. Usually the name of the HTTP status code.
8
+ ## @param location [Symbol] The location of the error (:request_body, :query, :header, :cookie, :path).
9
+ ## @param validation_result [ValidationResult]
10
+ def initialize(status:, location:, title:, validation_result:)
11
+ @status = status
12
+ @title = title
13
+ @location = location
14
+ @validation_output = validation_result&.output
15
+ @schema = validation_result&.schema
16
+ @data = validation_result&.data
17
+ end
18
+
19
+ attr_reader :status, :location, :title, :schema, :data, :validation_output
20
+
21
+ def render
22
+ Rack::Response.new(body, status, Rack::CONTENT_TYPE => content_type).finish
23
+ end
24
+
25
+ def content_type = 'application/json'
26
+
27
+ def body
28
+ raise NotImplementedError
29
+ end
30
+ end
31
+ end
@@ -5,15 +5,6 @@ module OpenapiFirst
5
5
 
6
6
  class NotFoundError < Error; end
7
7
 
8
- class HandlerNotFoundError < Error; end
9
-
10
- class NotImplementedError < Error
11
- def initialize(message)
12
- warn 'NotImplementedError is deprecated. Handle HandlerNotFoundError instead'
13
- super
14
- end
15
- end
16
-
17
8
  class ResponseInvalid < Error; end
18
9
 
19
10
  class ResponseCodeNotFoundError < ResponseInvalid; end
@@ -22,37 +13,9 @@ module OpenapiFirst
22
13
 
23
14
  class ResponseBodyInvalidError < ResponseInvalid; end
24
15
 
16
+ class ResponseHeaderInvalidError < ResponseInvalid; end
17
+
25
18
  class BodyParsingError < Error; end
26
19
 
27
- class RequestInvalidError < Error
28
- def initialize(serialized_errors)
29
- message = error_message(serialized_errors)
30
- super message
31
- end
32
-
33
- private
34
-
35
- def error_message(errors)
36
- errors.map do |error|
37
- [human_source(error), human_error(error)].compact.join(' ')
38
- end.join(', ')
39
- end
40
-
41
- def human_source(error)
42
- return unless error[:source]
43
-
44
- source_key = error[:source].keys.first
45
- source = {
46
- pointer: 'Request body invalid:',
47
- parameter: 'Query parameter invalid:'
48
- }.fetch(source_key, source_key)
49
- name = error[:source].values.first
50
- source += " #{name}" unless name.nil? || name.empty?
51
- source
52
- end
53
-
54
- def human_error(error)
55
- error[:title]
56
- end
57
- end
20
+ class RequestInvalidError < Error; end
58
21
  end
@@ -3,10 +3,10 @@
3
3
  require 'forwardable'
4
4
  require 'set'
5
5
  require_relative 'schema_validation'
6
- require_relative 'utils'
6
+ require_relative 'operation_schemas'
7
7
 
8
8
  module OpenapiFirst
9
- class Operation
9
+ class Operation # rubocop:disable Metrics/ClassLength
10
10
  extend Forwardable
11
11
  def_delegators :operation_object,
12
12
  :[],
@@ -15,12 +15,13 @@ module OpenapiFirst
15
15
  WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
16
16
  private_constant :WRITE_METHODS
17
17
 
18
- attr_reader :path, :method
18
+ attr_reader :path, :method, :openapi_version
19
19
 
20
- def initialize(path, request_method, path_item_object)
20
+ def initialize(path, request_method, path_item_object, openapi_version:)
21
21
  @path = path
22
22
  @method = request_method
23
23
  @path_item_object = path_item_object
24
+ @openapi_version = openapi_version
24
25
  end
25
26
 
26
27
  def operation_id
@@ -39,7 +40,7 @@ module OpenapiFirst
39
40
  operation_object['requestBody']
40
41
  end
41
42
 
42
- def response_schema_for(status, content_type)
43
+ def response_body_schema(status, content_type)
43
44
  content = response_for(status)['content']
44
45
  return if content.nil? || content.empty?
45
46
 
@@ -52,16 +53,18 @@ module OpenapiFirst
52
53
  raise ResponseContentTypeNotFoundError, message
53
54
  end
54
55
  schema = media_type['schema']
55
- SchemaValidation.new(schema, write: false) if schema
56
+ return unless schema
57
+
58
+ SchemaValidation.new(schema, write: false, openapi_version:)
56
59
  end
57
60
 
58
61
  def request_body_schema(request_content_type)
59
- content = operation_object.dig('requestBody', 'content')
60
- media_type = find_content_for_content_type(content, request_content_type)
61
- schema = media_type&.fetch('schema', nil)
62
- return unless schema
63
-
64
- SchemaValidation.new(schema, write: write?)
62
+ (@request_body_schema ||= {})[request_content_type] ||= begin
63
+ content = operation_object.dig('requestBody', 'content')
64
+ media_type = find_content_for_content_type(content, request_content_type)
65
+ schema = media_type&.fetch('schema', nil)
66
+ SchemaValidation.new(schema, write: write?, openapi_version:) if schema
67
+ end
65
68
  end
66
69
 
67
70
  def response_for(status)
@@ -73,12 +76,12 @@ module OpenapiFirst
73
76
  end
74
77
 
75
78
  def name
76
- "#{method.upcase} #{path} (#{operation_id})"
79
+ @name ||= "#{method.upcase} #{path} (#{operation_id})"
77
80
  end
78
81
 
79
82
  def valid_request_content_type?(request_content_type)
80
83
  content = operation_object.dig('requestBody', 'content')
81
- return unless content
84
+ return false unless content
82
85
 
83
86
  !!find_content_for_content_type(content, request_content_type)
84
87
  end
@@ -91,6 +94,17 @@ module OpenapiFirst
91
94
  @path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
92
95
  end
93
96
 
97
+ IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
98
+ private_constant :IGNORED_HEADERS
99
+
100
+ def header_parameters
101
+ @header_parameters ||= all_parameters.filter { |p| p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name']) }
102
+ end
103
+
104
+ def cookie_parameters
105
+ @cookie_parameters ||= all_parameters.filter { |p| p['in'] == 'cookie' }
106
+ end
107
+
94
108
  def all_parameters
95
109
  @all_parameters ||= begin
96
110
  parameters = @path_item_object['parameters']&.dup || []
@@ -100,6 +114,11 @@ module OpenapiFirst
100
114
  end
101
115
  end
102
116
 
117
+ # visibility: private
118
+ def schemas
119
+ @schemas ||= OperationSchemas.new(self)
120
+ end
121
+
103
122
  private
104
123
 
105
124
  def response_by_code(status)
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openapi_parameters/parameter'
4
+ require_relative 'schema_validation'
5
+
6
+ module OpenapiFirst
7
+ # This class is basically a cache for JSON Schemas of parameters
8
+ class OperationSchemas
9
+ # @operation [OpenapiFirst::Operation]
10
+ def initialize(operation)
11
+ @operation = operation
12
+ end
13
+
14
+ attr_reader :operation
15
+
16
+ # Return JSON Schema of for all query parameters
17
+ def query_parameters_schema
18
+ @query_parameters_schema ||= build_json_schema(operation.query_parameters)
19
+ end
20
+
21
+ # Return JSON Schema of for all path parameters
22
+ def path_parameters_schema
23
+ @path_parameters_schema ||= build_json_schema(operation.path_parameters)
24
+ end
25
+
26
+ def header_parameters_schema
27
+ @header_parameters_schema ||= build_json_schema(operation.header_parameters)
28
+ end
29
+
30
+ def cookie_parameters_schema
31
+ @cookie_parameters_schema ||= build_json_schema(operation.cookie_parameters)
32
+ end
33
+
34
+ private
35
+
36
+ # Build JSON Schema for given parameter definitions
37
+ # @parameter_defs [Array<Hash>] Parameter definitions
38
+ def build_json_schema(parameter_defs)
39
+ init_schema = {
40
+ 'type' => 'object',
41
+ 'properties' => {},
42
+ 'required' => []
43
+ }
44
+ schema = parameter_defs.each_with_object(init_schema) do |parameter_def, result|
45
+ parameter = OpenapiParameters::Parameter.new(parameter_def)
46
+ result['properties'][parameter.name] = parameter.schema if parameter.schema
47
+ result['required'] << parameter.name if parameter.required?
48
+ end
49
+ SchemaValidation.new(schema, openapi_version: operation.openapi_version)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Plugins
5
+ ERROR_RESPONSES = {} # rubocop:disable Style/MutableConstant
6
+
7
+ def self.register_error_response(name, klass)
8
+ ERROR_RESPONSES[name] = klass
9
+ end
10
+
11
+ def self.find_error_response(name)
12
+ return name if name.is_a?(Class)
13
+
14
+ ERROR_RESPONSES.fetch(name)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class RequestBodyValidator
5
+ def initialize(operation, env)
6
+ @operation = operation
7
+ @env = env
8
+ @parsed_request_body = env[REQUEST_BODY]
9
+ end
10
+
11
+ def validate!
12
+ content_type = Rack::Request.new(@env).content_type
13
+ validate_request_content_type!(@operation, content_type)
14
+ validate_request_body!(@operation, @parsed_request_body, content_type)
15
+ end
16
+
17
+ private
18
+
19
+ def validate_request_content_type!(operation, content_type)
20
+ operation.valid_request_content_type?(content_type) || OpenapiFirst.error!(415)
21
+ end
22
+
23
+ def validate_request_body!(operation, body, content_type)
24
+ validate_request_body_presence!(body, operation)
25
+ return if content_type.nil?
26
+
27
+ schema = operation&.request_body_schema(content_type)
28
+ return unless schema
29
+
30
+ validation_result = schema.validate(body)
31
+ OpenapiFirst.error!(400, :request_body, validation_result:) if validation_result.error?
32
+ body
33
+ end
34
+
35
+ def validate_request_body_presence!(body, operation)
36
+ return unless operation.request_body['required'] && body.nil?
37
+
38
+ OpenapiFirst.error!(400, :request_body, title: 'Request body is required')
39
+ end
40
+ end
41
+ end
@@ -3,7 +3,9 @@
3
3
  require 'rack'
4
4
  require 'multi_json'
5
5
  require_relative 'use_router'
6
- require_relative 'validation_format'
6
+ require_relative 'error_response'
7
+ require_relative 'request_body_validator'
8
+ require_relative 'string_keyed_hash'
7
9
  require 'openapi_parameters'
8
10
 
9
11
  module OpenapiFirst
@@ -13,118 +15,98 @@ module OpenapiFirst
13
15
  def initialize(app, options = {})
14
16
  @app = app
15
17
  @raise = options.fetch(:raise_error, false)
18
+ @error_response_class =
19
+ Plugins.find_error_response(options.fetch(:error_response, Config.default_options.error_response))
16
20
  end
17
21
 
18
- def call(env) # rubocop:disable Metrics/AbcSize
22
+ def call(env)
19
23
  operation = env[OPERATION]
20
24
  return @app.call(env) unless operation
21
25
 
22
- error = catch(:error) do
23
- query_params = OpenapiParameters::Query.new(operation.query_parameters).unpack(env['QUERY_STRING'])
24
- validate_query_parameters!(operation, query_params)
25
- env[PARAMS].merge!(query_params)
26
-
27
- return @app.call(env) unless operation.request_body
28
-
29
- content_type = Rack::Request.new(env).content_type
30
- validate_request_content_type!(operation, content_type)
31
- parsed_request_body = env[REQUEST_BODY]
32
- validate_request_body!(operation, parsed_request_body, content_type)
33
- nil
34
- end
26
+ error = validate_request(operation, env)
35
27
  if error
36
- raise RequestInvalidError, error[:errors] if @raise
28
+ location, title = error.values_at(:location, :title)
29
+ raise RequestInvalidError, error_message(title, location) if @raise
37
30
 
38
- return validation_error_response(error[:status], error[:errors])
31
+ return error_response(error).render
39
32
  end
40
33
  @app.call(env)
41
34
  end
42
35
 
43
36
  private
44
37
 
45
- def validate_request_body!(operation, body, content_type)
46
- validate_request_body_presence!(body, operation)
47
- return if content_type.nil?
38
+ def error_message(title, location)
39
+ return title unless location
48
40
 
49
- schema = operation&.request_body_schema(content_type)
50
- return unless schema
51
-
52
- errors = schema.validate(body)
53
- throw_error(400, serialize_request_body_errors(errors)) if errors.any?
54
- body
55
- end
56
-
57
- def validate_request_content_type!(operation, content_type)
58
- operation.valid_request_content_type?(content_type) || throw_error(415)
41
+ "#{TOPICS.fetch(location)} #{title}"
59
42
  end
60
43
 
61
- def validate_request_body_presence!(body, operation)
62
- return unless operation.request_body['required'] && body.nil?
63
-
64
- throw_error(415, 'Request body is required')
44
+ TOPICS = {
45
+ request_body: 'Request body invalid:',
46
+ query: 'Query parameter invalid:',
47
+ header: 'Header parameter invalid:',
48
+ path: 'Path segment invalid:',
49
+ cookie: 'Cookie value invalid:'
50
+ }.freeze
51
+ private_constant :TOPICS
52
+
53
+ def error_response(error_object)
54
+ @error_response_class.new(**error_object)
65
55
  end
66
56
 
67
- def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
68
- {
69
- status: status.to_s,
70
- title: title
71
- }
57
+ def validate_request(operation, env)
58
+ catch(:error) do
59
+ env[PARAMS] = {}
60
+ validate_query_params!(operation, env)
61
+ validate_path_params!(operation, env)
62
+ validate_cookie_params!(operation, env)
63
+ validate_header_params!(operation, env)
64
+ RequestBodyValidator.new(operation, env).validate! if operation.request_body
65
+ nil
66
+ end
72
67
  end
73
68
 
74
- def throw_error(status, errors = [default_error(status)])
75
- throw :error, {
76
- status: status,
77
- errors: errors
78
- }
79
- end
69
+ def validate_path_params!(operation, env)
70
+ path_parameters = operation.path_parameters
71
+ return if path_parameters.empty?
80
72
 
81
- def validation_error_response(status, errors)
82
- Rack::Response.new(
83
- MultiJson.dump(errors: errors),
84
- status,
85
- Rack::CONTENT_TYPE => 'application/vnd.api+json'
86
- ).finish
73
+ hashy = StringKeyedHash.new(env[Router::RAW_PATH_PARAMS])
74
+ unpacked_path_params = OpenapiParameters::Path.new(path_parameters).unpack(hashy)
75
+ validation_result = operation.schemas.path_parameters_schema.validate(unpacked_path_params)
76
+ OpenapiFirst.error!(400, :path, validation_result:) if validation_result.error?
77
+ env[PATH_PARAMS] = unpacked_path_params
78
+ env[PARAMS].merge!(unpacked_path_params)
87
79
  end
88
80
 
89
- def serialize_request_body_errors(validation_errors)
90
- validation_errors.map do |error|
91
- {
92
- source: {
93
- pointer: error['data_pointer']
94
- }
95
- }.update(ValidationFormat.error_details(error))
96
- end
97
- end
81
+ def validate_query_params!(operation, env)
82
+ query_parameters = operation.query_parameters
83
+ return if operation.query_parameters.empty?
98
84
 
99
- def build_json_schema(parameter_defs)
100
- init_schema = {
101
- 'type' => 'object',
102
- 'properties' => {},
103
- 'required' => []
104
- }
105
- parameter_defs.each_with_object(init_schema) do |parameter_def, schema|
106
- parameter = OpenapiParameters::Parameter.new(parameter_def)
107
- schema['properties'][parameter.name] = parameter.schema if parameter.schema
108
- schema['required'] << parameter.name if parameter.required?
109
- end
85
+ unpacked_query_params = OpenapiParameters::Query.new(query_parameters).unpack(env['QUERY_STRING'])
86
+ validation_result = operation.schemas.query_parameters_schema.validate(unpacked_query_params)
87
+ OpenapiFirst.error!(400, :query, validation_result:) if validation_result.error?
88
+ env[QUERY_PARAMS] = unpacked_query_params
89
+ env[PARAMS].merge!(unpacked_query_params)
110
90
  end
111
91
 
112
- def validate_query_parameters!(operation, params)
113
- parameter_defs = operation.query_parameters
114
- return unless parameter_defs&.any?
92
+ def validate_cookie_params!(operation, env)
93
+ cookie_parameters = operation.cookie_parameters
94
+ return unless cookie_parameters&.any?
115
95
 
116
- json_schema = build_json_schema(parameter_defs)
117
- errors = SchemaValidation.new(json_schema).validate(params)
118
- throw_error(400, serialize_parameter_errors(errors)) if errors.any?
96
+ unpacked_params = OpenapiParameters::Cookie.new(cookie_parameters).unpack(env['HTTP_COOKIE'])
97
+ validation_result = operation.schemas.cookie_parameters_schema.validate(unpacked_params)
98
+ OpenapiFirst.error!(400, :cookie, validation_result:) if validation_result.error?
99
+ env[COOKIE_PARAMS] = unpacked_params
119
100
  end
120
101
 
121
- def serialize_parameter_errors(validation_errors)
122
- validation_errors.map do |error|
123
- pointer = error['data_pointer'][1..].to_s
124
- {
125
- source: { parameter: pointer }
126
- }.update(ValidationFormat.error_details(error))
127
- end
102
+ def validate_header_params!(operation, env)
103
+ header_parameters = operation.header_parameters
104
+ return if header_parameters.empty?
105
+
106
+ unpacked_header_params = OpenapiParameters::Header.new(header_parameters).unpack_env(env)
107
+ validation_result = operation.schemas.header_parameters_schema.validate(unpacked_header_params)
108
+ OpenapiFirst.error!(400, :header, validation_result:) if validation_result.error?
109
+ env[HEADER_PARAMS] = unpacked_header_params
128
110
  end
129
111
  end
130
112
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'multi_json'
4
4
  require_relative 'use_router'
5
- require_relative 'validation_format'
6
5
 
7
6
  module OpenapiFirst
8
7
  class ResponseValidation
@@ -26,8 +25,9 @@ module OpenapiFirst
26
25
  return validate_status_only(operation, status) if status == 204
27
26
 
28
27
  content_type = headers[Rack::CONTENT_TYPE]
29
- response_schema = operation.response_schema_for(status, content_type)
28
+ response_schema = operation.response_body_schema(status, content_type)
30
29
  validate_response_body(response_schema, body) if response_schema
30
+ validate_response_headers(operation, status, headers)
31
31
  end
32
32
 
33
33
  private
@@ -40,14 +40,45 @@ module OpenapiFirst
40
40
  full_body = +''
41
41
  response.each { |chunk| full_body << chunk }
42
42
  data = full_body.empty? ? {} : load_json(full_body)
43
- errors = schema.validate(data)
44
- errors = errors.to_a.map! do |error|
45
- format_error(error)
43
+ validation = schema.validate(data)
44
+ raise ResponseBodyInvalidError, validation.message if validation.error?
45
+ end
46
+
47
+ def validate_response_headers(operation, status, response_headers)
48
+ response_header_definitions = operation.response_for(status)&.dig('headers')
49
+ return unless response_header_definitions
50
+
51
+ unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
52
+ response_header_definitions.each do |name, definition|
53
+ next if name == 'Content-Type'
54
+
55
+ validate_response_header(name, definition, unpacked_headers, openapi_version: operation.openapi_version)
56
+ end
57
+ end
58
+
59
+ def validate_response_header(name, definition, unpacked_headers, openapi_version:)
60
+ unless unpacked_headers.key?(name)
61
+ raise ResponseHeaderInvalidError, "Required response header '#{name}' is missing" if definition['required']
62
+
63
+ return
64
+ end
65
+
66
+ return unless definition.key?('schema')
67
+
68
+ validation = SchemaValidation.new(definition['schema'], openapi_version:)
69
+ value = unpacked_headers[name]
70
+ validation_result = validation.validate(value)
71
+ raise ResponseHeaderInvalidError, validation_result.message if validation_result.error?
72
+ end
73
+
74
+ def unpack_response_headers(response_header_definitions, response_headers)
75
+ headers_as_parameters = response_header_definitions.map do |name, definition|
76
+ definition.merge('name' => name)
46
77
  end
47
- raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
78
+ OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
48
79
  end
49
80
 
50
- def format_error(error)
81
+ def format_response_error(error)
51
82
  return "Write-only field appears in response: #{error['data_pointer']}" if error['type'] == 'writeOnly'
52
83
 
53
84
  JSONSchemer::Errors.pretty(error)
@@ -7,7 +7,7 @@ module OpenapiFirst
7
7
  class ResponseValidator
8
8
  def initialize(spec)
9
9
  @spec = spec
10
- @router = Router.new(->(_env) {}, spec: spec, raise_error: true)
10
+ @router = Router.new(->(_env) {}, spec:, raise_error: true)
11
11
  @response_validation = ResponseValidation.new(->(response) { response.to_a })
12
12
  end
13
13