openapi_contracts 0.7.1 → 0.9.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/README.md +43 -19
- data/lib/openapi_contracts/doc/operation.rb +27 -0
- data/lib/openapi_contracts/doc/parameter.rb +49 -0
- data/lib/openapi_contracts/doc/path.rb +32 -12
- data/lib/openapi_contracts/doc/pointer.rb +81 -0
- data/lib/openapi_contracts/doc/request.rb +17 -0
- data/lib/openapi_contracts/doc/response.rb +5 -5
- data/lib/openapi_contracts/doc/schema.rb +44 -10
- data/lib/openapi_contracts/doc/with_parameters.rb +9 -0
- data/lib/openapi_contracts/doc.rb +17 -14
- data/lib/openapi_contracts/match.rb +34 -10
- data/lib/openapi_contracts/operation_router.rb +33 -0
- data/lib/openapi_contracts/parser/transformers/base.rb +15 -0
- data/lib/openapi_contracts/parser/transformers/nullable.rb +10 -0
- data/lib/openapi_contracts/parser/transformers/pointer.rb +34 -0
- data/lib/openapi_contracts/parser/transformers.rb +5 -0
- data/lib/openapi_contracts/parser.rb +61 -0
- data/lib/openapi_contracts/payload_parser.rb +39 -0
- data/lib/openapi_contracts/rspec.rb +2 -2
- data/lib/openapi_contracts/validators/base.rb +5 -1
- data/lib/openapi_contracts/validators/documented.rb +12 -5
- data/lib/openapi_contracts/validators/headers.rb +4 -0
- data/lib/openapi_contracts/validators/http_status.rb +2 -6
- data/lib/openapi_contracts/validators/request_body.rb +26 -0
- data/lib/openapi_contracts/validators/response_body.rb +28 -0
- data/lib/openapi_contracts/validators/schema_validation.rb +40 -0
- data/lib/openapi_contracts/validators.rb +9 -6
- data/lib/openapi_contracts.rb +11 -5
- metadata +31 -20
- data/lib/openapi_contracts/doc/file_parser.rb +0 -85
- data/lib/openapi_contracts/doc/method.rb +0 -18
- data/lib/openapi_contracts/doc/parser.rb +0 -44
- data/lib/openapi_contracts/validators/body.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a6d176876364825de3b5e11746a49add3125a4511003e24002f8dda270347d6
|
4
|
+
data.tar.gz: 468ebb8a26dadc72b3f0d9fdb0e9a2f8a39b2e94418c555926fe0b12d0ea0b8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c310ffa9c64a85d3cef12f2e2fa685ae52f0bfeff975ab63a1532f88be964dc2acd577b24f4a70017efb0bc4b19c0a522a56b9660686ba33c89d970959ca1690
|
7
|
+
data.tar.gz: 1a2293a48671a21d6f6915162c1240fa56b719e2d40947b8e46782635c6cc6cd7d4e26ee6012f7360b098d1ab04bf82f282e090fb373d9e1e9ddb982a6672dce
|
data/README.md
CHANGED
@@ -4,24 +4,24 @@
|
|
4
4
|
[](https://badge.fury.io/rb/openapi_contracts)
|
5
5
|
[](https://depfu.com/github/mkon/openapi_contracts?project_id=35354)
|
6
6
|
|
7
|
-
Use
|
7
|
+
Use OpenAPI documentation as an API contract.
|
8
8
|
|
9
9
|
Currently supports OpenAPI documentation in the structure as used by [Redocly](https://github.com/Redocly/create-openapi-repo), but should also work for single file schemas.
|
10
10
|
|
11
|
-
Adds RSpec matchers to easily verify that your responses match the OpenAPI documentation.
|
11
|
+
Adds RSpec matchers to easily verify that your requests and responses match the OpenAPI documentation.
|
12
12
|
|
13
13
|
## Usage
|
14
14
|
|
15
|
-
First parse your
|
15
|
+
First, parse your API documentation:
|
16
16
|
|
17
17
|
```ruby
|
18
|
-
# This must point to the folder where the
|
19
|
-
$doc = OpenapiContracts::Doc.parse(Rails.root.join('spec/fixtures/openapi/api-docs
|
18
|
+
# This must point to the folder where the OAS file is stored
|
19
|
+
$doc = OpenapiContracts::Doc.parse(Rails.root.join('spec/fixtures/openapi/api-docs'), '<filename>')
|
20
20
|
```
|
21
21
|
|
22
|
-
|
22
|
+
In case the `filename` argument is not set, parser will by default search for the file named `openapi.yaml`.
|
23
23
|
|
24
|
-
Then you can use these matchers in your request specs:
|
24
|
+
Ideally you do this once in an RSpec `before(:suite)` hook. Then you can use these matchers in your request specs:
|
25
25
|
|
26
26
|
```ruby
|
27
27
|
subject { make_request and response }
|
@@ -34,48 +34,72 @@ it { is_expected.to match_openapi_doc($doc) }
|
|
34
34
|
You can assert a specific http status to make sure the response is of the right status:
|
35
35
|
|
36
36
|
```ruby
|
37
|
-
it { is_expected.to match_openapi_doc($
|
37
|
+
it { is_expected.to match_openapi_doc($doc).with_http_status(:ok) }
|
38
38
|
|
39
|
-
#
|
39
|
+
# This is equal to
|
40
40
|
it 'responds with 200 and matches the doc' do
|
41
41
|
expect(subject).to have_http_status(:ok)
|
42
|
-
expect(subject).to match_openapi_doc($
|
43
|
-
|
42
|
+
expect(subject).to match_openapi_doc($doc)
|
43
|
+
end
|
44
44
|
```
|
45
45
|
|
46
46
|
### Options
|
47
47
|
|
48
48
|
The `match_openapi_doc($doc)` method allows passing options as a 2nd argument.
|
49
|
-
This allows overriding the default request.path lookup in case this does not find
|
50
|
-
the correct response definition in your schema. This is especially important with
|
51
|
-
dynamic paths.
|
52
49
|
|
53
|
-
|
50
|
+
* `path` allows overriding the default `request.path` lookup in case it does not find the
|
51
|
+
correct response definition in your schema. This is especially important when there are
|
52
|
+
dynamic parameters in the path and the matcher fails to resolve the request path to
|
53
|
+
an endpoint in the OAS file.
|
54
54
|
|
55
55
|
```ruby
|
56
|
-
it { is_expected.to match_openapi_doc($
|
56
|
+
it { is_expected.to match_openapi_doc($doc, path: '/messages/{id}').with_http_status(:ok) }
|
57
57
|
```
|
58
58
|
|
59
|
+
* `request_body` can be set to `true` in case the validation of the request body against the OpenAPI _requestBody_ schema is required.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
it { is_expected.to match_openapi_doc($doc, request_body: true).with_http_status(:created) }
|
63
|
+
```
|
64
|
+
|
65
|
+
Both options can as well be used simultaneously.
|
66
|
+
|
59
67
|
### Without RSpec
|
60
68
|
|
61
69
|
You can also use the Validator directly:
|
70
|
+
|
62
71
|
```ruby
|
63
72
|
# Let's raise an error if the response does not match
|
64
73
|
result = OpenapiContracts.match($doc, response, options = {})
|
65
74
|
raise result.errors.merge("/n") unless result.valid?
|
66
75
|
```
|
67
76
|
|
68
|
-
|
77
|
+
## How it works
|
69
78
|
|
70
|
-
It uses the `request.path`, `request.method`, `status` and `headers` on the test subject
|
79
|
+
It uses the `request.path`, `request.method`, `status` and `headers` on the test subject
|
80
|
+
(which must be the response) to find the request and response schemas in the OpenAPI document.
|
81
|
+
Then it does the following checks:
|
71
82
|
|
72
83
|
* The response is documented
|
73
84
|
* Required headers are present
|
74
85
|
* Documented headers match the schema (via json_schemer)
|
75
86
|
* The response body matches the schema (via json_schemer)
|
87
|
+
* The request body matches the schema (via json_schemer) - if `request_body: true`
|
88
|
+
|
89
|
+
## Known Issues
|
90
|
+
|
91
|
+
### OpenApi 3.0
|
92
|
+
|
93
|
+
For openapi schemas < 3.1, data is validated using JSON Schema Draft 04, even tho OpenApi 3.0 is a super+subset of Draft 05.
|
94
|
+
This is due to the fact that we validate the data using json-schemer which does not support 05 and even then would not be fully compatible.
|
95
|
+
However compatibility issues should be fairly rare and there might be workarounds by describing the data slightly different.
|
96
|
+
|
97
|
+
### OpenAPi 3.1
|
98
|
+
|
99
|
+
Here exists a similar problem. OpenApi 3.1 is finally fully compatible with JSON Draft 2020-12, but there is no support yet in json-schemer,
|
100
|
+
so we use the closest draft which is 07.
|
76
101
|
|
77
102
|
## Future plans
|
78
103
|
|
79
|
-
* Validate sent requests against the request schema
|
80
104
|
* Validate Webmock stubs against the OpenAPI doc
|
81
105
|
* Generate example payloads from the OpenAPI doc
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module OpenapiContracts
|
2
|
+
class Doc::Operation
|
3
|
+
include Doc::WithParameters
|
4
|
+
|
5
|
+
def initialize(path, spec)
|
6
|
+
@path = path
|
7
|
+
@spec = spec
|
8
|
+
@responses = spec.navigate('responses').each.to_h do |status, subspec| # rubocop:disable Style/HashTransformValues
|
9
|
+
[status, Doc::Response.new(subspec)]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def request_body
|
14
|
+
return @request_body if instance_variable_defined?(:@request_body)
|
15
|
+
|
16
|
+
@request_body = @spec.navigate('requestBody')&.then { |s| Doc::Request.new(s) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def responses
|
20
|
+
@responses.each_value
|
21
|
+
end
|
22
|
+
|
23
|
+
def response_for_status(status)
|
24
|
+
@responses[status.to_s]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module OpenapiContracts
|
2
|
+
class Doc::Parameter
|
3
|
+
attr_reader :name, :in, :schema
|
4
|
+
|
5
|
+
def initialize(spec)
|
6
|
+
@spec = spec
|
7
|
+
options = spec.to_h
|
8
|
+
@name = options['name']
|
9
|
+
@in = options['in']
|
10
|
+
@required = options['required']
|
11
|
+
end
|
12
|
+
|
13
|
+
def in_path?
|
14
|
+
@in == 'path'
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(value)
|
18
|
+
case @spec.dig('schema', 'type')
|
19
|
+
when 'integer'
|
20
|
+
integer_parameter_matches?(value)
|
21
|
+
when 'number'
|
22
|
+
number_parameter_matches?(value)
|
23
|
+
else
|
24
|
+
schemer.valid?(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def schemer
|
31
|
+
@schemer ||= begin
|
32
|
+
schema = @spec.navigate('schema')
|
33
|
+
JSONSchemer.schema(Validators::SchemaValidation.build_validation_schema(schema))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def integer_parameter_matches?(value)
|
38
|
+
return false unless /^-?\d+$/.match?(value)
|
39
|
+
|
40
|
+
schemer.valid?(value.to_i)
|
41
|
+
end
|
42
|
+
|
43
|
+
def number_parameter_matches?(value)
|
44
|
+
return false unless /^-?(\d+\.)?\d+$/.match?(value)
|
45
|
+
|
46
|
+
schemer.valid?(value.to_f)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,26 +1,46 @@
|
|
1
1
|
module OpenapiContracts
|
2
2
|
class Doc::Path
|
3
|
-
|
4
|
-
@schema = schema
|
3
|
+
include Doc::WithParameters
|
5
4
|
|
6
|
-
|
7
|
-
|
5
|
+
HTTP_METHODS = %w(get head post put delete connect options trace patch).freeze
|
6
|
+
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
def initialize(path, spec)
|
10
|
+
@path = path
|
11
|
+
@spec = spec
|
12
|
+
@supported_methods = HTTP_METHODS & @spec.keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def dynamic?
|
16
|
+
@path.include?('{')
|
17
|
+
end
|
18
|
+
|
19
|
+
def operations
|
20
|
+
@supported_methods.each.lazy.map { |m| Doc::Operation.new(self, @spec.navigate(m)) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def path_regexp
|
24
|
+
@path_regexp ||= begin
|
25
|
+
re = /\{(\S+)\}/
|
26
|
+
@path.gsub(re) { |placeholder|
|
27
|
+
placeholder.match(re) { |m| "(?<#{m[1]}>[^/]*)" }
|
28
|
+
}.then { |str| Regexp.new(str) }
|
8
29
|
end
|
9
30
|
end
|
10
31
|
|
11
|
-
def
|
12
|
-
|
32
|
+
def static?
|
33
|
+
!dynamic?
|
13
34
|
end
|
14
35
|
|
15
|
-
def
|
16
|
-
@
|
36
|
+
def supports_method?(method)
|
37
|
+
@supported_methods.include?(method)
|
17
38
|
end
|
18
39
|
|
19
|
-
|
40
|
+
def with_method(method)
|
41
|
+
return unless supports_method?(method)
|
20
42
|
|
21
|
-
|
22
|
-
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
23
|
-
%w(get head post put delete connect options trace patch).freeze
|
43
|
+
Doc::Operation.new(self, @spec.navigate(method))
|
24
44
|
end
|
25
45
|
end
|
26
46
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module OpenapiContracts
|
2
|
+
class Doc::Pointer
|
3
|
+
def self.[](*segments)
|
4
|
+
new Array.wrap(segments).flatten
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.from_json_pointer(str)
|
8
|
+
raise ArguementError unless %r{^#/(?<pointer>.*)} =~ str
|
9
|
+
|
10
|
+
new(pointer.split('/').map { |s| s.gsub('~1', '/') })
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_path(pathname)
|
14
|
+
new pathname.to_s.split('/')
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(segments)
|
18
|
+
@segments = segments
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"<#{self.class.name}#{to_a}>"
|
23
|
+
end
|
24
|
+
|
25
|
+
delegate :empty?, to: :@segments
|
26
|
+
|
27
|
+
def navigate(*segments)
|
28
|
+
self.class[to_a + segments]
|
29
|
+
end
|
30
|
+
|
31
|
+
def parent
|
32
|
+
self.class[to_a[0..-2]]
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_a
|
36
|
+
@segments
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_json_pointer
|
40
|
+
escaped_segments.join('/').then { |s| "#/#{s}" }
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json_schemer_pointer
|
44
|
+
www_escaped_segments.join('/').then { |s| "#/#{s}" }
|
45
|
+
end
|
46
|
+
|
47
|
+
def walk(object)
|
48
|
+
return object if empty?
|
49
|
+
|
50
|
+
@segments.inject(object) do |obj, key|
|
51
|
+
return nil unless obj
|
52
|
+
|
53
|
+
if obj.is_a?(Array)
|
54
|
+
raise ArgumentError unless /^\d+$/ =~ key
|
55
|
+
|
56
|
+
key = key.to_i
|
57
|
+
end
|
58
|
+
|
59
|
+
obj[key]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def ==(other)
|
64
|
+
to_a == other.to_a
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def escaped_segments
|
70
|
+
@segments.map do |s|
|
71
|
+
s.gsub(%r{/}, '~1')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def www_escaped_segments
|
76
|
+
escaped_segments.map do |s|
|
77
|
+
URI.encode_www_form_component(s)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module OpenapiContracts
|
2
|
+
class Doc::Request
|
3
|
+
def initialize(schema)
|
4
|
+
@schema = schema.follow_refs
|
5
|
+
end
|
6
|
+
|
7
|
+
def schema_for(media_type)
|
8
|
+
return unless supports_media_type?(media_type)
|
9
|
+
|
10
|
+
@schema.navigate('content', media_type, 'schema')
|
11
|
+
end
|
12
|
+
|
13
|
+
def supports_media_type?(media_type)
|
14
|
+
@schema.dig('content', media_type).present?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -12,18 +12,18 @@ module OpenapiContracts
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
def schema_for(
|
16
|
-
return unless
|
15
|
+
def schema_for(media_type)
|
16
|
+
return unless supports_media_type?(media_type)
|
17
17
|
|
18
|
-
@schema.navigate('content',
|
18
|
+
@schema.navigate('content', media_type, 'schema')
|
19
19
|
end
|
20
20
|
|
21
21
|
def no_content?
|
22
22
|
!@schema.key? 'content'
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
26
|
-
@schema.dig('content',
|
25
|
+
def supports_media_type?(media_type)
|
26
|
+
@schema.dig('content', media_type).present?
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
@@ -6,16 +6,47 @@ module OpenapiContracts
|
|
6
6
|
class Doc::Schema
|
7
7
|
attr_reader :pointer, :raw
|
8
8
|
|
9
|
-
def initialize(raw, pointer =
|
9
|
+
def initialize(raw, pointer = Doc::Pointer[])
|
10
|
+
raise ArgumentError unless pointer.is_a?(Doc::Pointer)
|
11
|
+
|
10
12
|
@raw = raw
|
11
13
|
@pointer = pointer.freeze
|
12
14
|
end
|
13
15
|
|
16
|
+
def each # rubocop:disable Metrics/MethodLength
|
17
|
+
data = resolve
|
18
|
+
case data
|
19
|
+
when Array
|
20
|
+
enum = data.each_with_index
|
21
|
+
Enumerator.new(enum.size) do |yielder|
|
22
|
+
loop do
|
23
|
+
_item, index = enum.next
|
24
|
+
yielder << navigate(index.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
when Hash
|
28
|
+
enum = data.each_key
|
29
|
+
Enumerator.new(enum.size) do |yielder|
|
30
|
+
loop do
|
31
|
+
key = enum.next
|
32
|
+
yielder << [key, navigate(key)]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# :nocov:
|
39
|
+
def inspect
|
40
|
+
"<#{self.class.name} @pointer=#{@pointer.inspect}>"
|
41
|
+
end
|
42
|
+
# :nocov:
|
43
|
+
|
14
44
|
# Resolves Schema ref pointers links like "$ref: #/some/path" and returns new sub-schema
|
15
45
|
# at the target if the current schema is only a ref link.
|
16
46
|
def follow_refs
|
17
|
-
|
18
|
-
|
47
|
+
data = resolve
|
48
|
+
if data.is_a?(Hash) && data.key?('$ref')
|
49
|
+
at_pointer Doc::Pointer.from_json_pointer(data['$ref'])
|
19
50
|
else
|
20
51
|
self
|
21
52
|
end
|
@@ -23,23 +54,26 @@ module OpenapiContracts
|
|
23
54
|
|
24
55
|
# Generates a fragment pointer for the current schema path
|
25
56
|
def fragment
|
26
|
-
pointer.
|
57
|
+
pointer.to_json_schemer_pointer
|
27
58
|
end
|
28
59
|
|
29
|
-
delegate :dig, :fetch, :keys, :key?, :[], :to_h, to: :
|
60
|
+
delegate :dig, :fetch, :keys, :key?, :[], :to_h, to: :resolve
|
30
61
|
|
31
62
|
def at_pointer(pointer)
|
32
63
|
self.class.new(raw, pointer)
|
33
64
|
end
|
34
65
|
|
35
|
-
def
|
36
|
-
|
66
|
+
def openapi_version
|
67
|
+
@raw['openapi']&.then { |v| Gem::Version.new(v) }
|
68
|
+
end
|
37
69
|
|
38
|
-
|
70
|
+
# Returns the actual sub-specification contents at the pointer of this Specification
|
71
|
+
def resolve
|
72
|
+
@pointer.walk(@raw)
|
39
73
|
end
|
40
74
|
|
41
|
-
def navigate(*
|
42
|
-
self.class.new(@raw,
|
75
|
+
def navigate(*segments)
|
76
|
+
self.class.new(@raw, pointer.navigate(segments)).follow_refs
|
43
77
|
end
|
44
78
|
end
|
45
79
|
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
module OpenapiContracts
|
2
2
|
class Doc
|
3
|
-
autoload :Header,
|
4
|
-
autoload :
|
5
|
-
autoload :
|
6
|
-
autoload :
|
7
|
-
autoload :
|
8
|
-
autoload :
|
9
|
-
autoload :
|
3
|
+
autoload :Header, 'openapi_contracts/doc/header'
|
4
|
+
autoload :Operation, 'openapi_contracts/doc/operation'
|
5
|
+
autoload :Parameter, 'openapi_contracts/doc/parameter'
|
6
|
+
autoload :Path, 'openapi_contracts/doc/path'
|
7
|
+
autoload :Pointer, 'openapi_contracts/doc/pointer'
|
8
|
+
autoload :Request, 'openapi_contracts/doc/request'
|
9
|
+
autoload :Response, 'openapi_contracts/doc/response'
|
10
|
+
autoload :Schema, 'openapi_contracts/doc/schema'
|
11
|
+
autoload :WithParameters, 'openapi_contracts/doc/with_parameters'
|
10
12
|
|
11
13
|
def self.parse(dir, filename = 'openapi.yaml')
|
12
14
|
new Parser.call(dir, filename)
|
@@ -14,11 +16,12 @@ module OpenapiContracts
|
|
14
16
|
|
15
17
|
attr_reader :schema
|
16
18
|
|
17
|
-
def initialize(
|
18
|
-
@schema = Schema.new(
|
19
|
+
def initialize(raw)
|
20
|
+
@schema = Schema.new(raw)
|
19
21
|
@paths = @schema['paths'].to_h do |path, _|
|
20
|
-
[path, Path.new(@schema.at_pointer(['paths', path]))]
|
22
|
+
[path, Path.new(path, @schema.at_pointer(Doc::Pointer['paths', path]))]
|
21
23
|
end
|
24
|
+
@dynamic_paths = paths.select(&:dynamic?)
|
22
25
|
end
|
23
26
|
|
24
27
|
# Returns an Enumerator over all paths
|
@@ -26,8 +29,8 @@ module OpenapiContracts
|
|
26
29
|
@paths.each_value
|
27
30
|
end
|
28
31
|
|
29
|
-
def
|
30
|
-
|
32
|
+
def operation_for(path, method)
|
33
|
+
OperationRouter.new(self).route(path, method.downcase)
|
31
34
|
end
|
32
35
|
|
33
36
|
# Returns an Enumerator over all Responses
|
@@ -35,8 +38,8 @@ module OpenapiContracts
|
|
35
38
|
return enum_for(:responses) unless block_given?
|
36
39
|
|
37
40
|
paths.each do |path|
|
38
|
-
path.
|
39
|
-
|
41
|
+
path.operations.each do |operation|
|
42
|
+
operation.responses.each(&block)
|
40
43
|
end
|
41
44
|
end
|
42
45
|
end
|
@@ -1,11 +1,18 @@
|
|
1
1
|
module OpenapiContracts
|
2
2
|
class Match
|
3
|
+
DEFAULT_OPTIONS = {request_body: false}.freeze
|
4
|
+
MIN_REQUEST_ANCESTORS = %w(Rack::Request::Env Rack::Request::Helpers).freeze
|
5
|
+
MIN_RESPONSE_ANCESTORS = %w(Rack::Response::Helpers).freeze
|
6
|
+
|
3
7
|
attr_reader :errors
|
4
8
|
|
5
9
|
def initialize(doc, response, options = {})
|
6
10
|
@doc = doc
|
7
11
|
@response = response
|
8
|
-
@
|
12
|
+
@request = options.delete(:request) { response.request }
|
13
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
14
|
+
raise ArgumentError, "#{@response} must be compatible with Rack::Response::Helpers" unless response_compatible?
|
15
|
+
raise ArgumentError, "#{@request} must be compatible with Rack::Request::{Env,Helpers}" unless request_compatible?
|
9
16
|
end
|
10
17
|
|
11
18
|
def valid?
|
@@ -17,18 +24,35 @@ module OpenapiContracts
|
|
17
24
|
|
18
25
|
private
|
19
26
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
@
|
27
|
+
def matchers
|
28
|
+
env = Env.new(
|
29
|
+
options: @options,
|
30
|
+
operation: operation,
|
31
|
+
request: @request,
|
32
|
+
response: @response
|
25
33
|
)
|
34
|
+
validators = Validators::ALL.dup
|
35
|
+
validators.delete(Validators::HttpStatus) unless @options[:status]
|
36
|
+
validators.delete(Validators::RequestBody) unless @options[:request_body]
|
37
|
+
validators.reverse
|
38
|
+
.reduce(->(err) { err }) { |s, m| m.new(s, env) }
|
26
39
|
end
|
27
40
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
41
|
+
def operation
|
42
|
+
@doc.operation_for(
|
43
|
+
@options.fetch(:path, @request.path),
|
44
|
+
@request.request_method.downcase
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def request_compatible?
|
49
|
+
ancestors = @request.class.ancestors.map(&:to_s)
|
50
|
+
MIN_REQUEST_ANCESTORS.all? { |s| ancestors.include?(s) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def response_compatible?
|
54
|
+
ancestors = @response.class.ancestors.map(&:to_s)
|
55
|
+
MIN_RESPONSE_ANCESTORS.all? { |s| ancestors.include?(s) }
|
32
56
|
end
|
33
57
|
end
|
34
58
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module OpenapiContracts
|
2
|
+
class OperationRouter
|
3
|
+
def initialize(doc)
|
4
|
+
@doc = doc
|
5
|
+
@dynamic_paths = doc.paths.select(&:dynamic?)
|
6
|
+
end
|
7
|
+
|
8
|
+
def route(actual_path, method)
|
9
|
+
@doc.with_path(actual_path)&.then { |p| return p.with_method(method) }
|
10
|
+
|
11
|
+
@dynamic_paths.each do |path|
|
12
|
+
next unless path.supports_method?(method)
|
13
|
+
next unless m = path.path_regexp.match(actual_path)
|
14
|
+
|
15
|
+
operation = path.with_method(method)
|
16
|
+
parameters = (path.parameters + operation.parameters).select(&:in_path?)
|
17
|
+
|
18
|
+
return operation if parameter_match?(m.named_captures, parameters)
|
19
|
+
end
|
20
|
+
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def parameter_match?(actual_params, parameters)
|
27
|
+
actual_params.each do |k, v|
|
28
|
+
return false unless parameters&.find { |s| s.name == k }&.matches?(v)
|
29
|
+
end
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|