openapi_first 1.1.1 → 1.3.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/lib/openapi_first/body_parser.rb +3 -1
- data/lib/openapi_first/configuration.rb +3 -1
- data/lib/openapi_first/definition/operation.rb +65 -5
- data/lib/openapi_first/definition/path_item.rb +1 -0
- data/lib/openapi_first/definition/request_body.rb +1 -0
- data/lib/openapi_first/definition/response.rb +7 -0
- data/lib/openapi_first/definition.rb +43 -3
- data/lib/openapi_first/error_response.rb +1 -1
- data/lib/openapi_first/errors.rb +6 -0
- data/lib/openapi_first/failure.rb +28 -4
- data/lib/openapi_first/middlewares/request_validation.rb +2 -5
- data/lib/openapi_first/middlewares/response_validation.rb +1 -4
- data/lib/openapi_first/plugins/default/error_response.rb +4 -4
- data/lib/openapi_first/plugins/default.rb +1 -1
- data/lib/openapi_first/plugins/jsonapi/error_response.rb +3 -2
- data/lib/openapi_first/plugins/jsonapi.rb +1 -1
- data/lib/openapi_first/plugins.rb +1 -0
- data/lib/openapi_first/request_validation/request_body_validator.rb +1 -1
- data/lib/openapi_first/request_validation/validator.rb +1 -0
- data/lib/openapi_first/response_validation/validator.rb +1 -0
- data/lib/openapi_first/runtime_request.rb +63 -3
- data/lib/openapi_first/runtime_response.rb +43 -4
- data/lib/openapi_first/schema/validation_error.rb +2 -0
- data/lib/openapi_first/schema/validation_result.rb +2 -7
- data/lib/openapi_first/schema.rb +1 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +21 -3
- metadata +6 -17
- data/.github/CODEOWNERS +0 -1
- data/.github/workflows/ruby.yml +0 -13
- data/.gitignore +0 -11
- data/CHANGELOG.md +0 -274
- data/Gemfile +0 -18
- data/Gemfile.lock +0 -170
- data/Gemfile.rack2 +0 -15
- data/Gemfile.rack2.lock +0 -99
- data/LICENSE.txt +0 -21
- data/README.md +0 -225
- data/openapi_first.gemspec +0 -47
@@ -16,31 +16,59 @@ module OpenapiFirst
|
|
16
16
|
@path_item = path_item
|
17
17
|
@operation = operation
|
18
18
|
@original_path_params = path_params
|
19
|
+
@error = nil
|
20
|
+
@validated = false
|
19
21
|
end
|
20
22
|
|
21
23
|
def_delegators :@request, :content_type, :media_type, :path
|
22
24
|
def_delegators :@operation, :operation_id, :request_method
|
23
25
|
def_delegator :@path_item, :path, :path_definition
|
24
26
|
|
27
|
+
# Returns the path_item object.
|
28
|
+
# @return [PathItem, nil] The path_item object or nil if this request path is not known.
|
25
29
|
attr_reader :path_item
|
26
30
|
|
31
|
+
# Returns the operation object.
|
32
|
+
# @return [Operation, nil] The operation object or nil if this request method is not known.
|
33
|
+
attr_reader :operation
|
34
|
+
|
35
|
+
# Returns the error object if validation failed.
|
36
|
+
# @return [Failure, nil]
|
37
|
+
attr_reader :error
|
38
|
+
|
39
|
+
# Checks if the request is valid.
|
40
|
+
# @return [Boolean] true if the request is valid, false otherwise.
|
41
|
+
def valid?
|
42
|
+
validate unless @validated
|
43
|
+
error.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks if the path and request method are known.
|
47
|
+
# @return [Boolean] true if the path and request method are known, false otherwise.
|
27
48
|
def known?
|
28
49
|
known_path? && known_request_method?
|
29
50
|
end
|
30
51
|
|
52
|
+
# Checks if the path is known.
|
53
|
+
# @return [Boolean] true if the path is known, false otherwise.
|
31
54
|
def known_path?
|
32
55
|
!!path_item
|
33
56
|
end
|
34
57
|
|
58
|
+
# Checks if the request method is known.
|
59
|
+
# @return [Boolean] true if the request method is known, false otherwise.
|
35
60
|
def known_request_method?
|
36
61
|
!!operation
|
37
62
|
end
|
38
63
|
|
39
|
-
#
|
64
|
+
# Returns the merged path and query parameters.
|
65
|
+
# @return [Hash] The merged path and query parameters.
|
40
66
|
def params
|
41
67
|
@params ||= query.merge(path_parameters)
|
42
68
|
end
|
43
69
|
|
70
|
+
# Returns the parsed path parameters of the request.
|
71
|
+
# @return [Hash]
|
44
72
|
def path_parameters
|
45
73
|
return {} unless operation.path_parameters
|
46
74
|
|
@@ -48,6 +76,10 @@ module OpenapiFirst
|
|
48
76
|
OpenapiParameters::Path.new(operation.path_parameters).unpack(@original_path_params) || {}
|
49
77
|
end
|
50
78
|
|
79
|
+
# Returns the parsed query parameters.
|
80
|
+
# This only includes parameters that are defined in the API description.
|
81
|
+
# @note This method is aliased as query_parameters.
|
82
|
+
# @return [Hash]
|
51
83
|
def query
|
52
84
|
return {} unless operation.query_parameters
|
53
85
|
|
@@ -57,12 +89,18 @@ module OpenapiFirst
|
|
57
89
|
|
58
90
|
alias query_parameters query
|
59
91
|
|
92
|
+
# Returns the parsed header parameters.
|
93
|
+
# This only includes parameters that are defined in the API description.
|
94
|
+
# @return [Hash]
|
60
95
|
def headers
|
61
96
|
return {} unless operation.header_parameters
|
62
97
|
|
63
98
|
@headers ||= OpenapiParameters::Header.new(operation.header_parameters).unpack_env(request.env) || {}
|
64
99
|
end
|
65
100
|
|
101
|
+
# Returns the parsed cookie parameters.
|
102
|
+
# This only includes parameters that are defined in the API description.
|
103
|
+
# @return [Hash]
|
66
104
|
def cookies
|
67
105
|
return {} unless operation.cookie_parameters
|
68
106
|
|
@@ -70,26 +108,48 @@ module OpenapiFirst
|
|
70
108
|
OpenapiParameters::Cookie.new(operation.cookie_parameters).unpack(request.env[Rack::HTTP_COOKIE]) || {}
|
71
109
|
end
|
72
110
|
|
111
|
+
# Returns the parsed request body.
|
112
|
+
# This returns the whole request body with default values applied as defined in the API description.
|
113
|
+
# This does not remove any fields that are not defined in the API description.
|
114
|
+
# @return [Hash, Array, String, nil] The parsed body of the request.
|
73
115
|
def body
|
74
116
|
@body ||= BodyParser.new.parse(request, request.media_type)
|
75
117
|
end
|
118
|
+
|
76
119
|
alias parsed_body body
|
77
120
|
|
121
|
+
# Validates the request.
|
122
|
+
# @return [Failure, nil] The Failure object if validation failed.
|
78
123
|
def validate
|
79
|
-
|
124
|
+
@validated = true
|
125
|
+
@error = RequestValidation::Validator.new(operation).validate(self)
|
80
126
|
end
|
81
127
|
|
128
|
+
# Validates the request and raises an error if validation fails.
|
82
129
|
def validate!
|
83
130
|
error = validate
|
84
131
|
error&.raise!
|
85
132
|
end
|
86
133
|
|
134
|
+
# Validates the response.
|
135
|
+
# @param rack_response [Rack::Response] The rack response object.
|
136
|
+
# @param raise_error [Boolean] Whether to raise an error if validation fails.
|
137
|
+
# @return [RuntimeResponse] The validated response object.
|
138
|
+
def validate_response(rack_response, raise_error: false)
|
139
|
+
validated = response(rack_response).tap(&:validate)
|
140
|
+
validated.error&.raise! if raise_error
|
141
|
+
validated
|
142
|
+
end
|
143
|
+
|
144
|
+
# Creates a new RuntimeResponse object.
|
145
|
+
# @param rack_response [Rack::Response] The rack response object.
|
146
|
+
# @return [RuntimeResponse] The RuntimeResponse object.
|
87
147
|
def response(rack_response)
|
88
148
|
RuntimeResponse.new(operation, rack_response)
|
89
149
|
end
|
90
150
|
|
91
151
|
private
|
92
152
|
|
93
|
-
attr_reader :request
|
153
|
+
attr_reader :request
|
94
154
|
end
|
95
155
|
end
|
@@ -5,56 +5,95 @@ require_relative 'body_parser'
|
|
5
5
|
require_relative 'response_validation/validator'
|
6
6
|
|
7
7
|
module OpenapiFirst
|
8
|
+
# Represents a response returned by the Rack application and how it relates to the API description.
|
8
9
|
class RuntimeResponse
|
9
10
|
extend Forwardable
|
10
11
|
|
11
12
|
def initialize(operation, rack_response)
|
12
13
|
@operation = operation
|
13
14
|
@rack_response = rack_response
|
15
|
+
@error = nil
|
14
16
|
end
|
15
17
|
|
18
|
+
# @return [Failure, nil] Error object if validation failed.
|
19
|
+
attr_reader :error
|
20
|
+
|
21
|
+
# @attr_reader [Integer] status The HTTP status code of this response.
|
22
|
+
# @attr_reader [String] content_type The content_type of the Rack::Response.
|
16
23
|
def_delegators :@rack_response, :status, :content_type
|
17
|
-
def_delegators :@operation, :name
|
18
24
|
|
25
|
+
# @attr_reader [String] name The name of the operation. Used for generating error messages.
|
26
|
+
def_delegators :@operation, :name # @visibility private
|
27
|
+
|
28
|
+
# Checks if the response is valid. Runs the validation unless it has been run before.
|
29
|
+
# @return [Boolean]
|
30
|
+
def valid?
|
31
|
+
validate unless @validated
|
32
|
+
@error.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks if the response is defined in the API description.
|
36
|
+
# @return [Boolean] Returns true if the response is known, false otherwise.
|
19
37
|
def known?
|
20
38
|
!!response_definition
|
21
39
|
end
|
22
40
|
|
41
|
+
# Checks if the response status is defined in the API description.
|
42
|
+
# @return [Boolean] Returns true if the response status is known, false otherwise.
|
23
43
|
def known_status?
|
24
44
|
@operation.response_status_defined?(status)
|
25
45
|
end
|
26
46
|
|
47
|
+
# Returns the description of the response definition if available.
|
48
|
+
# @return [String, nil] Returns the description of the response, or nil if not available.
|
27
49
|
def description
|
28
50
|
response_definition&.description
|
29
51
|
end
|
30
52
|
|
53
|
+
# Returns the parsed (JSON) body of the response.
|
54
|
+
# @return [Hash, String] Returns the body of the response.
|
31
55
|
def body
|
32
56
|
@body ||= content_type =~ /json/i ? load_json(original_body) : original_body
|
33
57
|
end
|
34
58
|
|
59
|
+
# Returns the headers of the response as defined in the API description.
|
60
|
+
# This only returns the headers that are defined in the API description.
|
61
|
+
# @return [Hash] Returns the headers of the response.
|
35
62
|
def headers
|
36
63
|
@headers ||= unpack_response_headers
|
37
64
|
end
|
38
65
|
|
66
|
+
# Validates the response.
|
67
|
+
# @return [Failure, nil] Returns the validation error, or nil if the response is valid.
|
39
68
|
def validate
|
40
|
-
|
69
|
+
@validated = true
|
70
|
+
@error = ResponseValidation::Validator.new(@operation).validate(self)
|
41
71
|
end
|
42
72
|
|
73
|
+
# Validates the response and raises an error if invalid.
|
74
|
+
# @raise [ResponseNotFoundError, ResponseInvalidError] Raises an error if the response is invalid.
|
43
75
|
def validate!
|
44
76
|
error = validate
|
45
77
|
error&.raise!
|
46
78
|
end
|
47
79
|
|
80
|
+
# Returns the response definition associated with the response.
|
81
|
+
# @return [Definition::Response, nil] Returns the response definition, or nil if not found.
|
48
82
|
def response_definition
|
49
83
|
@response_definition ||= @operation.response_for(status, content_type)
|
50
84
|
end
|
51
85
|
|
52
86
|
private
|
53
87
|
|
88
|
+
# Usually the body responds to #each, but when using manual response validation without the middleware
|
89
|
+
# in Rails request specs the body is a String. So this code handles both cases.
|
54
90
|
def original_body
|
55
91
|
buffered_body = String.new
|
56
|
-
@rack_response.body.each
|
57
|
-
|
92
|
+
if @rack_response.body.respond_to?(:each)
|
93
|
+
@rack_response.body.each { |chunk| buffered_body.to_s << chunk }
|
94
|
+
return buffered_body
|
95
|
+
end
|
96
|
+
@rack_response.body
|
58
97
|
end
|
59
98
|
|
60
99
|
def load_json(string)
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
module OpenapiFirst
|
4
4
|
class Schema
|
5
|
+
# One of multiple validation errors. Returned by Schema::ValidationResult#errors.
|
5
6
|
class ValidationError
|
6
7
|
def initialize(json_schemer_error)
|
7
8
|
@error = json_schemer_error
|
8
9
|
end
|
9
10
|
|
10
11
|
def error = @error['error']
|
12
|
+
alias message error
|
11
13
|
def schemer_error = @error
|
12
14
|
def instance_location = @error['data_pointer']
|
13
15
|
def schema_location = @error['schema_pointer']
|
@@ -4,6 +4,7 @@ require_relative 'validation_error'
|
|
4
4
|
|
5
5
|
module OpenapiFirst
|
6
6
|
class Schema
|
7
|
+
# Result of validating data against a schema. Return value of Schema#validate.
|
7
8
|
class ValidationResult
|
8
9
|
def initialize(validation, schema:, data:)
|
9
10
|
@validation = validation
|
@@ -15,18 +16,12 @@ module OpenapiFirst
|
|
15
16
|
|
16
17
|
def error? = @validation.any?
|
17
18
|
|
19
|
+
# Returns an array of ValidationError objects.
|
18
20
|
def errors
|
19
21
|
@errors ||= @validation.map do |err|
|
20
22
|
ValidationError.new(err)
|
21
23
|
end
|
22
24
|
end
|
23
|
-
|
24
|
-
# Returns a message that is used in exception messages.
|
25
|
-
def message
|
26
|
-
return unless error?
|
27
|
-
|
28
|
-
errors.map(&:error).join('. ')
|
29
|
-
end
|
30
25
|
end
|
31
26
|
end
|
32
27
|
end
|
data/lib/openapi_first/schema.rb
CHANGED
data/lib/openapi_first.rb
CHANGED
@@ -12,14 +12,18 @@ require_relative 'openapi_first/error_response'
|
|
12
12
|
require_relative 'openapi_first/middlewares/response_validation'
|
13
13
|
require_relative 'openapi_first/middlewares/request_validation'
|
14
14
|
|
15
|
+
# OpenapiFirst is a toolchain to build HTTP APIS based on OpenAPI API descriptions.
|
15
16
|
module OpenapiFirst
|
16
17
|
extend Plugins
|
17
18
|
|
18
19
|
class << self
|
20
|
+
# @return [Configuration]
|
19
21
|
def configuration
|
20
22
|
@configuration ||= Configuration.new
|
21
23
|
end
|
22
24
|
|
25
|
+
# @return [Configuration]
|
26
|
+
# @yield [Configuration]
|
23
27
|
def configure
|
24
28
|
yield configuration
|
25
29
|
end
|
@@ -28,12 +32,26 @@ module OpenapiFirst
|
|
28
32
|
# Key in rack to find instance of RuntimeRequest
|
29
33
|
REQUEST = 'openapi.request'
|
30
34
|
|
31
|
-
|
32
|
-
|
35
|
+
# Load and dereference an OpenAPI spec file
|
36
|
+
# @return [Definition]
|
37
|
+
def self.load(filepath, only: nil)
|
38
|
+
resolved = bundle(filepath)
|
39
|
+
parse(resolved, only:, filepath:)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse a dereferenced Hash
|
43
|
+
# @return [Definition]
|
44
|
+
def self.parse(resolved, only: nil, filepath: nil)
|
33
45
|
resolved['paths'].filter!(&->(key, _) { only.call(key) }) if only
|
34
|
-
Definition.new(resolved,
|
46
|
+
Definition.new(resolved, filepath)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @!visibility private
|
50
|
+
def self.bundle(filepath)
|
51
|
+
Bundle.resolve(filepath)
|
35
52
|
end
|
36
53
|
|
54
|
+
# @!visibility private
|
37
55
|
module Bundle
|
38
56
|
def self.resolve(spec_path)
|
39
57
|
Dir.chdir(File.dirname(spec_path)) do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openapi_first
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Haller
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-01-
|
11
|
+
date: 2024-01-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json_refs
|
@@ -59,7 +59,7 @@ dependencies:
|
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '1.15'
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
|
-
name: mustermann
|
62
|
+
name: mustermann
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
@@ -119,16 +119,6 @@ executables: []
|
|
119
119
|
extensions: []
|
120
120
|
extra_rdoc_files: []
|
121
121
|
files:
|
122
|
-
- ".github/CODEOWNERS"
|
123
|
-
- ".github/workflows/ruby.yml"
|
124
|
-
- ".gitignore"
|
125
|
-
- CHANGELOG.md
|
126
|
-
- Gemfile
|
127
|
-
- Gemfile.lock
|
128
|
-
- Gemfile.rack2
|
129
|
-
- Gemfile.rack2.lock
|
130
|
-
- LICENSE.txt
|
131
|
-
- README.md
|
132
122
|
- lib/openapi_first.rb
|
133
123
|
- lib/openapi_first/body_parser.rb
|
134
124
|
- lib/openapi_first/configuration.rb
|
@@ -157,12 +147,11 @@ files:
|
|
157
147
|
- lib/openapi_first/schema/validation_error.rb
|
158
148
|
- lib/openapi_first/schema/validation_result.rb
|
159
149
|
- lib/openapi_first/version.rb
|
160
|
-
- openapi_first.gemspec
|
161
150
|
homepage: https://github.com/ahx/openapi_first
|
162
151
|
licenses:
|
163
152
|
- MIT
|
164
153
|
metadata:
|
165
|
-
|
154
|
+
homepage_uri: https://github.com/ahx/openapi_first
|
166
155
|
source_code_uri: https://github.com/ahx/openapi_first
|
167
156
|
changelog_uri: https://github.com/ahx/openapi_first/blob/main/CHANGELOG.md
|
168
157
|
rubygems_mfa_required: 'true'
|
@@ -184,5 +173,5 @@ requirements: []
|
|
184
173
|
rubygems_version: 3.5.3
|
185
174
|
signing_key:
|
186
175
|
specification_version: 4
|
187
|
-
summary: Implement
|
176
|
+
summary: Implement HTTP APIs based on OpenApi 3.x
|
188
177
|
test_files: []
|
data/.github/CODEOWNERS
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
* @ahx
|
data/.github/workflows/ruby.yml
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
name: Test
|
2
|
-
on: [push, pull_request]
|
3
|
-
jobs:
|
4
|
-
test:
|
5
|
-
runs-on: ubuntu-latest
|
6
|
-
steps:
|
7
|
-
- uses: actions/checkout@v3
|
8
|
-
- uses: ruby/setup-ruby@v1
|
9
|
-
with:
|
10
|
-
ruby-version: '3.1'
|
11
|
-
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
12
|
-
- run: BUNDLE_GEMFILE=Gemfile bundle exec rake
|
13
|
-
- run: BUNDLE_GEMFILE=Gemfile.rack2 bundle lock --add-platform x86_64-linux && bundle exec rake
|