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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -20
  3. data/lib/openapi_contracts/doc/operation.rb +27 -0
  4. data/lib/openapi_contracts/doc/parameter.rb +22 -59
  5. data/lib/openapi_contracts/doc/path.rb +23 -38
  6. data/lib/openapi_contracts/doc/pointer.rb +81 -0
  7. data/lib/openapi_contracts/doc/request.rb +17 -0
  8. data/lib/openapi_contracts/doc/response.rb +5 -5
  9. data/lib/openapi_contracts/doc/schema.rb +49 -10
  10. data/lib/openapi_contracts/doc/with_parameters.rb +9 -0
  11. data/lib/openapi_contracts/doc.rb +17 -20
  12. data/lib/openapi_contracts/match.rb +34 -10
  13. data/lib/openapi_contracts/operation_router.rb +33 -0
  14. data/lib/openapi_contracts/parser/transformers/base.rb +15 -0
  15. data/lib/openapi_contracts/parser/transformers/nullable.rb +10 -0
  16. data/lib/openapi_contracts/parser/transformers/pointer.rb +34 -0
  17. data/lib/openapi_contracts/parser/transformers.rb +5 -0
  18. data/lib/openapi_contracts/parser.rb +61 -0
  19. data/lib/openapi_contracts/payload_parser.rb +39 -0
  20. data/lib/openapi_contracts/rspec.rb +2 -2
  21. data/lib/openapi_contracts/validators/base.rb +5 -1
  22. data/lib/openapi_contracts/validators/documented.rb +12 -5
  23. data/lib/openapi_contracts/validators/headers.rb +4 -0
  24. data/lib/openapi_contracts/validators/http_status.rb +2 -6
  25. data/lib/openapi_contracts/validators/request_body.rb +26 -0
  26. data/lib/openapi_contracts/validators/response_body.rb +28 -0
  27. data/lib/openapi_contracts/validators/schema_validation.rb +40 -0
  28. data/lib/openapi_contracts/validators.rb +9 -6
  29. data/lib/openapi_contracts.rb +10 -5
  30. metadata +32 -22
  31. data/lib/openapi_contracts/doc/file_parser.rb +0 -85
  32. data/lib/openapi_contracts/doc/method.rb +0 -18
  33. data/lib/openapi_contracts/doc/parser.rb +0 -44
  34. data/lib/openapi_contracts/validators/body.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25a37c81bfa9cb9d1d26f26a25b7c81f0675a153c199223639bdd48c76537cb6
4
- data.tar.gz: 48529ed541f4ad72568d222d2a57e30ef7f1a7b5e59fbb447961a58d57eafe26
3
+ metadata.gz: 989040d95a13df9bd1c3138c908071ff04593a4de03422e13ae4262dd6414cdb
4
+ data.tar.gz: 1d5202b6e194132c185ed6476e169b4678dbc9e234225c30de65cf2decbeb53b
5
5
  SHA512:
6
- metadata.gz: 1cc4ee62cea4e015834ae6a493bc771f13670b553cfdc1085b7dac79ea5a347583786ec4cd1db31c9054d88a010dd5d779c64f13df4388a3f55a3eae07ef577a
7
- data.tar.gz: 9991124beb074d92107b1026e6ffd529cca988d8154eac6c6cc92aa5103df5de11cf917d1809545e26810de07959202f295a5d23a1a15077e639d2212bbc6286
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 openapi documentation as an api contract.
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 api documentation:
15
+ First, parse your API documentation:
16
16
 
17
17
  ```ruby
18
- # This must point to the folder where the "openapi.yaml" file is
19
- $doc = OpenapiContracts::Doc.parse(Rails.root.join('spec/fixtures/openapi/api-docs/openapi'))
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
- Ideally you do this once in a RSpec `before(:suite)` hook.
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($api_doc).with_http_status(:ok) }
37
+ it { is_expected.to match_openapi_doc($doc).with_http_status(:ok) }
38
38
 
39
- # this is equal to
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($api_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
- Example:
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($api_doc, path: '/messages/{id}').with_http_status(:ok) }
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
- ### How it works
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 schema in the OpenAPI document.
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 initialize(options)
6
- @name = options[:name]
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['type']
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
- # Not yet implemented
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
- parsed = value.to_i
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
- parsed = value.to_f
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
- def initialize(path, schema)
4
- @path = path
5
- @schema = schema
3
+ include Doc::WithParameters
6
4
 
7
- @methods = (known_http_methods & @schema.keys).to_h do |method|
8
- [method, Doc::Method.new(@schema.navigate(method))]
9
- end
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 matches?(path)
17
- @path == path || regexp_path.match(path) do |m|
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 methods
26
- @methods.each_value
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 with_method(method)
34
- @methods[method]
36
+ def supports_method?(method)
37
+ @supported_methods.include?(method)
35
38
  end
36
39
 
37
- private
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
- def known_http_methods
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(content_type)
16
- return unless supports_content_type?(content_type)
15
+ def schema_for(media_type)
16
+ return unless supports_media_type?(media_type)
17
17
 
18
- @schema.navigate('content', content_type, 'schema')
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 supports_content_type?(content_type)
26
- @schema.dig('content', content_type).present?
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 = nil)
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
- if (ref = as_h['$ref'])
18
- at_pointer(ref.split('/')[1..])
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.map { |p| p.gsub('/', '~1') }.join('/').then { |s| "#/#{s}" }
57
+ pointer.to_json_schemer_pointer
27
58
  end
28
59
 
29
- delegate :dig, :fetch, :keys, :key?, :[], :to_h, to: :as_h
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 as_h
36
- return @raw if pointer.nil? || pointer.empty?
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
- @raw.dig(*pointer)
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(*spointer)
42
- self.class.new(@raw, (pointer + Array.wrap(spointer)))
80
+ def navigate(*segments)
81
+ self.class.new(@raw, pointer.navigate(segments)).follow_refs
43
82
  end
44
83
  end
45
84
  end
@@ -0,0 +1,9 @@
1
+ module OpenapiContracts
2
+ class Doc
3
+ module WithParameters
4
+ def parameters
5
+ @parameters ||= Array.wrap(@spec.navigate('parameters')&.each&.map { |s| Doc::Parameter.new(s) })
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,13 +1,14 @@
1
1
  module OpenapiContracts
2
2
  class Doc
3
- autoload :Header, 'openapi_contracts/doc/header'
4
- autoload :FileParser, 'openapi_contracts/doc/file_parser'
5
- autoload :Method, 'openapi_contracts/doc/method'
6
- autoload :Parser, 'openapi_contracts/doc/parser'
7
- autoload :Parameter, 'openapi_contracts/doc/parameter'
8
- autoload :Path, 'openapi_contracts/doc/path'
9
- autoload :Response, 'openapi_contracts/doc/response'
10
- autoload :Schema, 'openapi_contracts/doc/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(schema)
19
- @schema = Schema.new(schema)
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 response_for(path, method, status)
32
- with_path(path)&.with_method(method)&.with_status(status)
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.methods.each do |method|
41
- method.responses.each(&block)
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
- if @paths.key?(path)
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