openapi_first 1.4.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -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 +35 -24
  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,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- # Plugin System adapted from
5
- # Polished Ruby Programming by Jeremy Evans
6
- # https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewBook?id=0
7
- # @!visibility private
8
- module Plugins
9
- PLUGINS = {} # rubocop:disable Style/MutableConstant
10
- private_constant :PLUGINS
11
-
12
- def register(name, klass)
13
- PLUGINS[name.to_sym] = klass
14
- end
15
-
16
- def plugin(name)
17
- require "openapi_first/plugins/#{name}"
18
- PLUGINS.fetch(name.to_sym)
19
- end
20
-
21
- def find_plugin(name)
22
- PLUGINS.fetch(name)
23
- end
24
- end
25
- end
@@ -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