openapi_first 1.4.3 → 2.0.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/README.md +105 -28
  4. data/lib/openapi_first/body_parser.rb +8 -11
  5. data/lib/openapi_first/builder.rb +81 -0
  6. data/lib/openapi_first/configuration.rb +24 -3
  7. data/lib/openapi_first/definition.rb +44 -100
  8. data/lib/openapi_first/error_response.rb +2 -2
  9. data/lib/openapi_first/error_responses/default.rb +73 -0
  10. data/lib/openapi_first/error_responses/jsonapi.rb +59 -0
  11. data/lib/openapi_first/errors.rb +26 -4
  12. data/lib/openapi_first/failure.rb +29 -26
  13. data/lib/openapi_first/json_refs.rb +1 -3
  14. data/lib/openapi_first/middlewares/request_validation.rb +2 -2
  15. data/lib/openapi_first/middlewares/response_validation.rb +4 -3
  16. data/lib/openapi_first/request.rb +92 -0
  17. data/lib/openapi_first/request_parser.rb +35 -0
  18. data/lib/openapi_first/request_validator.rb +25 -0
  19. data/lib/openapi_first/response.rb +57 -0
  20. data/lib/openapi_first/response_parser.rb +49 -0
  21. data/lib/openapi_first/response_validator.rb +27 -0
  22. data/lib/openapi_first/router/find_content.rb +17 -0
  23. data/lib/openapi_first/router/find_response.rb +45 -0
  24. data/lib/openapi_first/{definition → router}/path_template.rb +9 -1
  25. data/lib/openapi_first/router.rb +100 -0
  26. data/lib/openapi_first/schema/validation_error.rb +16 -10
  27. data/lib/openapi_first/schema/validation_result.rb +8 -6
  28. data/lib/openapi_first/schema.rb +4 -8
  29. data/lib/openapi_first/test/methods.rb +21 -0
  30. data/lib/openapi_first/test.rb +19 -0
  31. data/lib/openapi_first/validated_request.rb +81 -0
  32. data/lib/openapi_first/validated_response.rb +33 -0
  33. data/lib/openapi_first/validators/request_body.rb +39 -0
  34. data/lib/openapi_first/validators/request_parameters.rb +61 -0
  35. data/lib/openapi_first/validators/response_body.rb +30 -0
  36. data/lib/openapi_first/validators/response_headers.rb +25 -0
  37. data/lib/openapi_first/version.rb +1 -1
  38. data/lib/openapi_first.rb +40 -21
  39. metadata +25 -20
  40. data/lib/openapi_first/definition/operation.rb +0 -197
  41. data/lib/openapi_first/definition/path_item.rb +0 -40
  42. data/lib/openapi_first/definition/request_body.rb +0 -46
  43. data/lib/openapi_first/definition/response.rb +0 -32
  44. data/lib/openapi_first/definition/responses.rb +0 -87
  45. data/lib/openapi_first/plugins/default/error_response.rb +0 -74
  46. data/lib/openapi_first/plugins/default.rb +0 -11
  47. data/lib/openapi_first/plugins/jsonapi/error_response.rb +0 -60
  48. data/lib/openapi_first/plugins/jsonapi.rb +0 -11
  49. data/lib/openapi_first/plugins.rb +0 -25
  50. data/lib/openapi_first/request_validation/request_body_validator.rb +0 -41
  51. data/lib/openapi_first/request_validation/validator.rb +0 -82
  52. data/lib/openapi_first/response_validation/validator.rb +0 -98
  53. data/lib/openapi_first/runtime_request.rb +0 -166
  54. data/lib/openapi_first/runtime_response.rb +0 -124
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../failure'
4
-
5
- module OpenapiFirst
6
- module RequestValidation
7
- class RequestBodyValidator # :nodoc:
8
- def initialize(operation)
9
- @operation = operation
10
- end
11
-
12
- def validate!(parsed_request_body, request_content_type)
13
- request_body = operation.request_body
14
- schema = request_body.schema_for(request_content_type)
15
- unless schema
16
- Failure.fail!(:unsupported_media_type,
17
- message: "Unsupported Media Type '#{request_content_type}'")
18
- end
19
-
20
- if request_body.required? && parsed_request_body.nil?
21
- Failure.fail!(:invalid_body,
22
- message: 'Request body is not defined')
23
- end
24
-
25
- validate_body!(parsed_request_body, schema)
26
- end
27
-
28
- private
29
-
30
- attr_reader :operation
31
-
32
- def validate_body!(parsed_request_body, schema)
33
- request_body_schema = schema
34
- return unless request_body_schema
35
-
36
- validation = request_body_schema.validate(parsed_request_body)
37
- Failure.fail!(:invalid_body, errors: validation.errors) if validation.error?
38
- end
39
- end
40
- end
41
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../failure'
4
- require_relative 'request_body_validator'
5
-
6
- module OpenapiFirst
7
- module RequestValidation
8
- # Validates a RuntimeRequest against an Operation.
9
- class Validator
10
- def initialize(operation)
11
- @operation = operation
12
- end
13
-
14
- def validate(runtime_request)
15
- catch Failure::FAILURE do
16
- validate_defined(runtime_request)
17
- validate_parameters!(runtime_request)
18
- validate_request_body!(runtime_request)
19
- nil
20
- end
21
- end
22
-
23
- private
24
-
25
- attr_reader :operation, :raw_path_params
26
-
27
- def validate_defined(request)
28
- return if request.known?
29
- return Failure.fail!(:not_found) unless request.known_path?
30
-
31
- Failure.fail!(:method_not_allowed) unless request.known_request_method?
32
- end
33
-
34
- def validate_parameters!(request)
35
- validate_query_params!(request)
36
- validate_path_params!(request)
37
- validate_cookie_params!(request)
38
- validate_header_params!(request)
39
- end
40
-
41
- def validate_path_params!(request)
42
- schema = operation.path_parameters_schema
43
- return unless schema
44
-
45
- validation = schema.validate(request.path_parameters)
46
- Failure.fail!(:invalid_path, errors: validation.errors) if validation.error?
47
- end
48
-
49
- def validate_query_params!(request)
50
- schema = operation.query_parameters_schema
51
- return unless schema
52
-
53
- validation = schema.validate(request.query)
54
- Failure.fail!(:invalid_query, errors: validation.errors) if validation.error?
55
- end
56
-
57
- def validate_cookie_params!(request)
58
- schema = operation.cookie_parameters_schema
59
- return unless schema
60
-
61
- validation = schema.validate(request.cookies)
62
- Failure.fail!(:invalid_cookie, errors: validation.errors) if validation.error?
63
- end
64
-
65
- def validate_header_params!(request)
66
- schema = operation.header_parameters_schema
67
- return unless schema
68
-
69
- validation = schema.validate(request.headers)
70
- Failure.fail!(:invalid_header, errors: validation.errors) if validation.error?
71
- end
72
-
73
- def validate_request_body!(request)
74
- return unless operation.request_body
75
-
76
- RequestBodyValidator.new(operation).validate!(request.body, request.content_type)
77
- rescue ParseError => e
78
- Failure.fail!(:invalid_body, message: e.message)
79
- end
80
- end
81
- end
82
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../failure'
4
-
5
- module OpenapiFirst
6
- module ResponseValidation
7
- # Validates a RuntimeResponse against an Operation.
8
- class Validator
9
- def initialize(operation)
10
- @operation = operation
11
- end
12
-
13
- def validate(runtime_response)
14
- return unless operation
15
-
16
- catch Failure::FAILURE do
17
- validate_defined(runtime_response)
18
- response_definition = runtime_response.response_definition
19
- validate_response_body(response_definition.content_schema, runtime_response)
20
- validate_response_headers(response_definition.headers, runtime_response.headers)
21
- nil
22
- end
23
- end
24
-
25
- private
26
-
27
- attr_reader :operation
28
-
29
- def validate_defined(runtime_response)
30
- return if runtime_response.known?
31
-
32
- unless runtime_response.known_status?
33
- message = "Response status '#{runtime_response.status}' not found for '#{runtime_response.name}'"
34
- Failure.fail!(:response_not_found, message:)
35
- end
36
-
37
- content_type = runtime_response.content_type
38
- if content_type.nil? || content_type.empty?
39
- message = "Content-Type for '#{runtime_response.name}' must not be empty"
40
- Failure.fail!(:invalid_response_header, message:)
41
- end
42
-
43
- message = "Content-Type '#{content_type}' is not defined for '#{runtime_response.name}'"
44
- Failure.fail!(:invalid_response_header, message:)
45
- end
46
-
47
- def validate_response_body(schema, runtime_response)
48
- return unless schema
49
-
50
- begin
51
- parsed_body = runtime_response.body
52
- rescue ParseError => e
53
- Failure.fail!(:invalid_response_body, message: e.message)
54
- end
55
-
56
- validation = schema.validate(parsed_body)
57
- Failure.fail!(:invalid_response_body, errors: validation.errors) if validation.error?
58
- end
59
-
60
- def validate_response_headers(response_header_definitions, unpacked_headers)
61
- return unless response_header_definitions
62
-
63
- response_header_definitions.each do |name, definition|
64
- next if name == 'Content-Type'
65
-
66
- validate_response_header(name, definition, unpacked_headers, openapi_version: operation.openapi_version)
67
- end
68
- end
69
-
70
- def validate_response_header(name, definition, unpacked_headers, openapi_version:)
71
- unless unpacked_headers.key?(name)
72
- if definition['required']
73
- Failure.fail!(:invalid_response_header,
74
- message: "Required response header '#{name}' is missing")
75
- end
76
-
77
- return
78
- end
79
-
80
- return unless definition.key?('schema')
81
-
82
- validation = Schema.new(definition['schema'], openapi_version:)
83
- value = unpacked_headers[name]
84
- validation_result = validation.validate(value)
85
- return unless validation_result.error?
86
-
87
- Failure.fail!(:invalid_response_header,
88
- errors: validation_result.errors)
89
- end
90
-
91
- def load_json(string)
92
- MultiJson.load(string)
93
- rescue MultiJson::ParseError
94
- string
95
- end
96
- end
97
- end
98
- end
@@ -1,166 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'forwardable'
4
- require 'openapi_parameters'
5
- require_relative 'runtime_response'
6
- require_relative 'body_parser'
7
- require_relative 'request_validation/validator'
8
-
9
- module OpenapiFirst
10
- # RuntimeRequest represents how an incoming request (Rack::Request) matches a request definition.
11
- class RuntimeRequest
12
- extend Forwardable
13
-
14
- def initialize(request:, path_item:, operation:, path_params:)
15
- @request = request
16
- @path_item = path_item
17
- @operation = operation
18
- @original_path_params = path_params
19
- @validated = false
20
- end
21
-
22
- def_delegators :@request, :content_type, :media_type, :path
23
- def_delegators :@operation, :operation_id, :request_method
24
- def_delegator :@path_item, :path, :path_definition
25
-
26
- # Returns the path_item object.
27
- # @return [PathItem, nil] The path_item object or nil if this request path is not known.
28
- attr_reader :path_item
29
-
30
- # Returns the operation object.
31
- # @return [Operation, nil] The operation object or nil if this request method is not known.
32
- attr_reader :operation
33
-
34
- # Returns the error object if validation failed.
35
- # @return [Failure, nil]
36
- attr_accessor :error
37
-
38
- # Checks if the request is valid.
39
- # @return [Boolean] true if the request is valid, false otherwise.
40
- def valid?
41
- validate unless validated?
42
- error.nil?
43
- end
44
-
45
- # Checks if the path and request method are known.
46
- # @return [Boolean] true if the path and request method are known, false otherwise.
47
- def known?
48
- known_path? && known_request_method?
49
- end
50
-
51
- # Checks if the path is known.
52
- # @return [Boolean] true if the path is known, false otherwise.
53
- def known_path?
54
- !!path_item
55
- end
56
-
57
- # Checks if the request method is known.
58
- # @return [Boolean] true if the request method is known, false otherwise.
59
- def known_request_method?
60
- !!operation
61
- end
62
-
63
- # Returns the merged path and query parameters.
64
- # @return [Hash] The merged path and query parameters.
65
- def params
66
- @params ||= query.merge(path_parameters)
67
- end
68
-
69
- # Returns the parsed path parameters of the request.
70
- # @return [Hash]
71
- def path_parameters
72
- return {} unless operation.path_parameters
73
-
74
- @path_parameters ||=
75
- OpenapiParameters::Path.new(operation.path_parameters).unpack(@original_path_params) || {}
76
- end
77
-
78
- # Returns the parsed query parameters.
79
- # This only includes parameters that are defined in the API description.
80
- # @note This method is aliased as query_parameters.
81
- # @return [Hash]
82
- def query
83
- return {} unless operation.query_parameters
84
-
85
- @query ||=
86
- OpenapiParameters::Query.new(operation.query_parameters).unpack(request.env[Rack::QUERY_STRING]) || {}
87
- end
88
-
89
- alias query_parameters query
90
-
91
- # Returns the parsed header parameters.
92
- # This only includes parameters that are defined in the API description.
93
- # @return [Hash]
94
- def headers
95
- return {} unless operation.header_parameters
96
-
97
- @headers ||= OpenapiParameters::Header.new(operation.header_parameters).unpack_env(request.env) || {}
98
- end
99
-
100
- # Returns the parsed cookie parameters.
101
- # This only includes parameters that are defined in the API description.
102
- # @return [Hash]
103
- def cookies
104
- return {} unless operation.cookie_parameters
105
-
106
- @cookies ||=
107
- OpenapiParameters::Cookie.new(operation.cookie_parameters).unpack(request.env[Rack::HTTP_COOKIE]) || {}
108
- end
109
-
110
- # Returns the parsed request body.
111
- # This returns the whole request body with default values applied as defined in the API description.
112
- # This does not remove any fields that are not defined in the API description.
113
- # @return [Hash, Array, String, nil] The parsed body of the request.
114
- def body
115
- @body ||= BodyParser.new.parse(request, request.media_type)
116
- end
117
-
118
- alias parsed_body body
119
-
120
- # Validates the request.
121
- # @return [Failure, nil] The Failure object if validation failed.
122
- # @deprecated Please use {Definition#validate_request} instead
123
- def validate
124
- warn '[DEPRECATION] `validate` is deprecated. Please use ' \
125
- "`OpenapiFirst.load('openapi.yaml').validate_request(rack_request)` instead."
126
- @error = RequestValidation::Validator.new(operation).validate(self)
127
- end
128
-
129
- # Validates the request and raises an error if validation fails.
130
- def validate!
131
- warn '[DEPRECATION] `validate!` is deprecated. Please use ' \
132
- "`OpenapiFirst.load('openapi.yaml').validate_request(rack_request, raise_error: true)` instead."
133
- error = validate
134
- error&.raise!
135
- end
136
-
137
- # Validates the response.
138
- # @param rack_response [Rack::Response] The rack response object.
139
- # @param raise_error [Boolean] Whether to raise an error if validation fails.
140
- # @return [RuntimeResponse] The validated response object.
141
- def validate_response(rack_response, raise_error: false)
142
- warn '[DEPRECATION] `validate_response!` is deprecated. Please use ' \
143
- "`OpenapiFirst.load('openapi.yaml').validate_response(request, response, raise_error: false)` instead."
144
- validated = response(rack_response).tap(&:validate)
145
- validated.error&.raise! if raise_error
146
- validated
147
- end
148
-
149
- # Creates a new RuntimeResponse object.
150
- # @param rack_response [Rack::Response] The rack response object.
151
- # @return [RuntimeResponse] The RuntimeResponse object.
152
- def response(rack_response)
153
- warn '[DEPRECATION] `response` is deprecated. Please use ' \
154
- "`OpenapiFirst.load('openapi.yaml').validate_response(request, response, raise_error: false)` instead."
155
- RuntimeResponse.new(operation, rack_response)
156
- end
157
-
158
- private
159
-
160
- def validated?
161
- defined?(@error)
162
- end
163
-
164
- attr_reader :request
165
- end
166
- end
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'forwardable'
4
- require_relative 'body_parser'
5
- require_relative 'response_validation/validator'
6
-
7
- module OpenapiFirst
8
- # Represents a response returned by the Rack application and how it relates to the API description.
9
- class RuntimeResponse
10
- extend Forwardable
11
-
12
- def initialize(operation, rack_response)
13
- @operation = operation
14
- @rack_response = rack_response
15
- end
16
-
17
- # @return [Failure, nil] Error object if validation failed.
18
- attr_accessor :error
19
-
20
- # @visibility private
21
- attr_accessor :operation
22
-
23
- # @attr_reader [Integer] status The HTTP status code of this response.
24
- # @attr_reader [String] content_type The content_type of the Rack::Response.
25
- def_delegators :@rack_response, :status, :content_type
26
-
27
- # @return [String] name The name of the operation. Used for generating error messages.
28
- def name
29
- "#{@operation.name} response status: #{status}"
30
- end
31
-
32
- # Checks if the response is valid. Runs the validation unless it has been run before.
33
- # @return [Boolean]
34
- def valid?
35
- validate unless validated?
36
- @error.nil?
37
- end
38
-
39
- # Checks if the response is defined in the API description.
40
- # @return [Boolean] Returns true if the response is known, false otherwise.
41
- def known?
42
- !!response_definition
43
- end
44
-
45
- # Checks if the response status is defined in the API description.
46
- # @return [Boolean] Returns true if the response status is known, false otherwise.
47
- def known_status?
48
- @operation.response_status_defined?(status)
49
- end
50
-
51
- # Returns the description of the response definition if available.
52
- # @return [String, nil] Returns the description of the response, or nil if not available.
53
- def description
54
- response_definition&.description
55
- end
56
-
57
- # Returns the parsed (JSON) body of the response.
58
- # @return [Hash, String] Returns the body of the response.
59
- def body
60
- @body ||= content_type =~ /json/i ? load_json(original_body) : original_body
61
- end
62
-
63
- # Returns the headers of the response as defined in the API description.
64
- # This only returns the headers that are defined in the API description.
65
- # @return [Hash] Returns the headers of the response.
66
- def headers
67
- @headers ||= unpack_response_headers
68
- end
69
-
70
- # Validates the response.
71
- # @return [Failure, nil] Returns the validation error, or nil if the response is valid.
72
- # @deprecated Please use {Definition#validate_response} instead
73
- def validate
74
- warn '[DEPRECATION] `validate` is deprecated. ' \
75
- "Please use `OpenapiFirst.load('openapi.yaml').validate_response(rack_request, rack_response)` instead."
76
- @error = ResponseValidation::Validator.new(@operation).validate(self)
77
- end
78
-
79
- # Validates the response and raises an error if invalid.
80
- # @raise [ResponseNotFoundError, ResponseInvalidError] Raises an error if the response is invalid.
81
- def validate!
82
- error = validate
83
- error&.raise!
84
- end
85
-
86
- # Returns the response definition associated with the response.
87
- # @return [Definition::Response, nil] Returns the response definition, or nil if not found.
88
- def response_definition
89
- @response_definition ||= @operation.response_for(status, content_type)
90
- end
91
-
92
- private
93
-
94
- def validated?
95
- defined?(@error)
96
- end
97
-
98
- # Usually the body responds to #each, but when using manual response validation without the middleware
99
- # in Rails request specs the body is a String. So this code handles both cases.
100
- def original_body
101
- buffered_body = String.new
102
- if @rack_response.body.respond_to?(:each)
103
- @rack_response.body.each { |chunk| buffered_body.to_s << chunk }
104
- return buffered_body
105
- end
106
- @rack_response.body
107
- end
108
-
109
- def load_json(string)
110
- MultiJson.load(string)
111
- rescue MultiJson::ParseError
112
- raise ParseError, 'Failed to parse response body as JSON'
113
- end
114
-
115
- def unpack_response_headers
116
- return {} if response_definition&.headers.nil?
117
-
118
- headers_as_parameters = response_definition.headers.map do |name, definition|
119
- definition.merge('name' => name, 'in' => 'header')
120
- end
121
- OpenapiParameters::Header.new(headers_as_parameters).unpack(@rack_response.headers)
122
- end
123
- end
124
- end