openapi_contracts 0.8.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +41 -20
- data/lib/openapi_contracts/doc/operation.rb +27 -0
- data/lib/openapi_contracts/doc/parameter.rb +22 -59
- data/lib/openapi_contracts/doc/path.rb +23 -38
- 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 +49 -10
- data/lib/openapi_contracts/doc/with_parameters.rb +9 -0
- data/lib/openapi_contracts/doc.rb +17 -20
- 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 +10 -5
- metadata +32 -22
- 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: 989040d95a13df9bd1c3138c908071ff04593a4de03422e13ae4262dd6414cdb
|
4
|
+
data.tar.gz: 1d5202b6e194132c185ed6476e169b4678dbc9e234225c30de65cf2decbeb53b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c4f45d3aa218538737b24630b952fe90c91d824063b2a18573d681809edb6766d28dc0d3503f126b204d48a92ee7120d83f1f73c96dfc94df678d282e275f08
|
7
|
+
data.tar.gz: 2782dbc72660c32caf5dc8b23d7719df2b1176c2cb2c7feeef838f54f6e306ef3574abb836b26ee66c25f5f8e394f1462d660712f56de90ab37f0aac1e873990
|
data/README.md
CHANGED
@@ -4,24 +4,24 @@
|
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/openapi_contracts.svg)](https://badge.fury.io/rb/openapi_contracts)
|
5
5
|
[![Depfu](https://badges.depfu.com/badges/8ac57411497df02584bbf59685634e45/overview.svg)](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,51 +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 can be usefull when there is
|
51
|
-
dynamic parameters in the path and the matcher fails to resolve the request path to
|
52
|
-
an endpoint in the openapi specification.
|
53
49
|
|
54
|
-
|
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.
|
55
54
|
|
56
55
|
```ruby
|
57
|
-
it { is_expected.to match_openapi_doc($
|
56
|
+
it { is_expected.to match_openapi_doc($doc, path: '/messages/{id}').with_http_status(:ok) }
|
58
57
|
```
|
59
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
|
+
|
60
67
|
### Without RSpec
|
61
68
|
|
62
69
|
You can also use the Validator directly:
|
70
|
+
|
63
71
|
```ruby
|
64
72
|
# Let's raise an error if the response does not match
|
65
73
|
result = OpenapiContracts.match($doc, response, options = {})
|
66
74
|
raise result.errors.merge("/n") unless result.valid?
|
67
75
|
```
|
68
76
|
|
69
|
-
|
77
|
+
## How it works
|
70
78
|
|
71
79
|
It uses the `request.path`, `request.method`, `status` and `headers` on the test subject
|
72
|
-
(which must be the response) to find the response
|
80
|
+
(which must be the response) to find the request and response schemas in the OpenAPI document.
|
73
81
|
Then it does the following checks:
|
74
82
|
|
75
83
|
* The response is documented
|
76
84
|
* Required headers are present
|
77
85
|
* Documented headers match the schema (via json_schemer)
|
78
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.
|
79
101
|
|
80
102
|
## Future plans
|
81
103
|
|
82
|
-
* Validate sent requests against the request schema
|
83
104
|
* Validate Webmock stubs against the OpenAPI doc
|
84
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').presence&.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
|
@@ -1,86 +1,49 @@
|
|
1
1
|
module OpenapiContracts
|
2
2
|
class Doc::Parameter
|
3
|
-
attr_reader :schema
|
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
|
4
12
|
|
5
|
-
def
|
6
|
-
@
|
7
|
-
@in = options[:in]
|
8
|
-
@required = options[:required]
|
9
|
-
@schema = options[:schema]
|
13
|
+
def in_path?
|
14
|
+
@in == 'path'
|
10
15
|
end
|
11
16
|
|
12
17
|
def matches?(value)
|
13
|
-
case schema
|
18
|
+
case @spec.dig('schema', 'type')
|
14
19
|
when 'integer'
|
15
20
|
integer_parameter_matches?(value)
|
16
21
|
when 'number'
|
17
22
|
number_parameter_matches?(value)
|
18
|
-
when 'string'
|
19
|
-
string_parameter_matches?(value)
|
20
23
|
else
|
21
|
-
|
22
|
-
false
|
24
|
+
schemer.valid?(value)
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
28
|
private
|
27
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
|
+
|
28
37
|
def integer_parameter_matches?(value)
|
29
38
|
return false unless /^-?\d+$/.match?(value)
|
30
39
|
|
31
|
-
|
32
|
-
return false unless minimum_number_matches?(parsed)
|
33
|
-
return false unless maximum_number_matches?(parsed)
|
34
|
-
|
35
|
-
true
|
40
|
+
schemer.valid?(value.to_i)
|
36
41
|
end
|
37
42
|
|
38
43
|
def number_parameter_matches?(value)
|
39
44
|
return false unless /^-?(\d+\.)?\d+$/.match?(value)
|
40
45
|
|
41
|
-
|
42
|
-
return false unless minimum_number_matches?(parsed)
|
43
|
-
return false unless maximum_number_matches?(parsed)
|
44
|
-
|
45
|
-
true
|
46
|
-
end
|
47
|
-
|
48
|
-
def minimum_number_matches?(value)
|
49
|
-
if (min = schema['minimum'])
|
50
|
-
if schema['exclusiveMinimum']
|
51
|
-
return false if value <= min
|
52
|
-
elsif value < min
|
53
|
-
return false
|
54
|
-
end
|
55
|
-
end
|
56
|
-
true
|
57
|
-
end
|
58
|
-
|
59
|
-
def maximum_number_matches?(value)
|
60
|
-
if (max = schema['maximum'])
|
61
|
-
if schema['exclusiveMaximum']
|
62
|
-
return false if value >= max
|
63
|
-
elsif value > max
|
64
|
-
return false
|
65
|
-
end
|
66
|
-
end
|
67
|
-
true
|
68
|
-
end
|
69
|
-
|
70
|
-
def string_parameter_matches?(value)
|
71
|
-
if (pat = schema['pattern'])
|
72
|
-
Regexp.new(pat).match?(value)
|
73
|
-
else
|
74
|
-
if (min = schema['minLength']) && (value.length < min)
|
75
|
-
return false
|
76
|
-
end
|
77
|
-
|
78
|
-
if (max = schema['maxLength']) && (value.length > max)
|
79
|
-
return false
|
80
|
-
end
|
81
|
-
|
82
|
-
true
|
83
|
-
end
|
46
|
+
schemer.valid?(value.to_f)
|
84
47
|
end
|
85
48
|
end
|
86
49
|
end
|
@@ -1,61 +1,46 @@
|
|
1
1
|
module OpenapiContracts
|
2
2
|
class Doc::Path
|
3
|
-
|
4
|
-
@path = path
|
5
|
-
@schema = schema
|
3
|
+
include Doc::WithParameters
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
10
13
|
end
|
11
14
|
|
12
15
|
def dynamic?
|
13
16
|
@path.include?('{')
|
14
17
|
end
|
15
18
|
|
16
|
-
def
|
17
|
-
@
|
18
|
-
m.named_captures.each do |k, v|
|
19
|
-
return false unless parameter_matches?(k, v)
|
20
|
-
end
|
21
|
-
true
|
22
|
-
end
|
19
|
+
def operations
|
20
|
+
@supported_methods.each.lazy.map { |m| Doc::Operation.new(self, @spec.navigate(m)) }
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
@
|
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) }
|
29
|
+
end
|
27
30
|
end
|
28
31
|
|
29
32
|
def static?
|
30
33
|
!dynamic?
|
31
34
|
end
|
32
35
|
|
33
|
-
def
|
34
|
-
@
|
36
|
+
def supports_method?(method)
|
37
|
+
@supported_methods.include?(method)
|
35
38
|
end
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
def parameter_matches?(name, value)
|
40
|
-
parameter = @schema['parameters']
|
41
|
-
&.find { |p| p['name'] == name && p['in'] == 'path' }
|
42
|
-
&.then { |s| Doc::Parameter.new(s.with_indifferent_access) }
|
43
|
-
|
44
|
-
return false unless parameter
|
45
|
-
|
46
|
-
parameter.matches?(value)
|
47
|
-
end
|
48
|
-
|
49
|
-
def regexp_path
|
50
|
-
re = /\{(\S+)\}/
|
51
|
-
@path.gsub(re) { |placeholder|
|
52
|
-
placeholder.match(re) { |m| "(?<#{m[1]}>[^/]*)" }
|
53
|
-
}.then { |str| Regexp.new(str) }
|
54
|
-
end
|
40
|
+
def with_method(method)
|
41
|
+
return unless supports_method?(method)
|
55
42
|
|
56
|
-
|
57
|
-
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
58
|
-
%w(get head post put delete connect options trace patch).freeze
|
43
|
+
Doc::Operation.new(self, @spec.navigate(method))
|
59
44
|
end
|
60
45
|
end
|
61
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,31 @@ 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, :
|
60
|
+
delegate :dig, to: :resolve, allow_nil: true
|
61
|
+
delegate :fetch, :keys, :key?, :[], :to_h, to: :resolve
|
30
62
|
|
31
63
|
def at_pointer(pointer)
|
32
64
|
self.class.new(raw, pointer)
|
33
65
|
end
|
34
66
|
|
35
|
-
def
|
36
|
-
|
67
|
+
def openapi_version
|
68
|
+
@raw['openapi']&.then { |v| Gem::Version.new(v) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def presence
|
72
|
+
resolve.present? ? self : nil
|
73
|
+
end
|
37
74
|
|
38
|
-
|
75
|
+
# Returns the actual sub-specification contents at the pointer of this Specification
|
76
|
+
def resolve
|
77
|
+
@pointer.walk(@raw)
|
39
78
|
end
|
40
79
|
|
41
|
-
def navigate(*
|
42
|
-
self.class.new(@raw,
|
80
|
+
def navigate(*segments)
|
81
|
+
self.class.new(@raw, pointer.navigate(segments)).follow_refs
|
43
82
|
end
|
44
83
|
end
|
45
84
|
end
|
@@ -1,13 +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 :Response,
|
10
|
-
autoload :Schema,
|
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'
|
11
12
|
|
12
13
|
def self.parse(dir, filename = 'openapi.yaml')
|
13
14
|
new Parser.call(dir, filename)
|
@@ -15,10 +16,10 @@ module OpenapiContracts
|
|
15
16
|
|
16
17
|
attr_reader :schema
|
17
18
|
|
18
|
-
def initialize(
|
19
|
-
@schema = Schema.new(
|
19
|
+
def initialize(raw)
|
20
|
+
@schema = Schema.new(raw)
|
20
21
|
@paths = @schema['paths'].to_h do |path, _|
|
21
|
-
[path, Path.new(path, @schema.at_pointer(['paths', path]))]
|
22
|
+
[path, Path.new(path, @schema.at_pointer(Doc::Pointer['paths', path]))]
|
22
23
|
end
|
23
24
|
@dynamic_paths = paths.select(&:dynamic?)
|
24
25
|
end
|
@@ -28,8 +29,8 @@ module OpenapiContracts
|
|
28
29
|
@paths.each_value
|
29
30
|
end
|
30
31
|
|
31
|
-
def
|
32
|
-
|
32
|
+
def operation_for(path, method)
|
33
|
+
OperationRouter.new(self).route(path, method.downcase)
|
33
34
|
end
|
34
35
|
|
35
36
|
# Returns an Enumerator over all Responses
|
@@ -37,18 +38,14 @@ module OpenapiContracts
|
|
37
38
|
return enum_for(:responses) unless block_given?
|
38
39
|
|
39
40
|
paths.each do |path|
|
40
|
-
path.
|
41
|
-
|
41
|
+
path.operations.each do |operation|
|
42
|
+
operation.responses.each(&block)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
46
47
|
def with_path(path)
|
47
|
-
|
48
|
-
@paths[path]
|
49
|
-
else
|
50
|
-
@dynamic_paths.find { |p| p.matches?(path) }
|
51
|
-
end
|
48
|
+
@paths[path]
|
52
49
|
end
|
53
50
|
end
|
54
51
|
end
|