openapi_contracts 0.7.1 → 0.9.0

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 +43 -19
  3. data/lib/openapi_contracts/doc/operation.rb +27 -0
  4. data/lib/openapi_contracts/doc/parameter.rb +49 -0
  5. data/lib/openapi_contracts/doc/path.rb +32 -12
  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 +44 -10
  10. data/lib/openapi_contracts/doc/with_parameters.rb +9 -0
  11. data/lib/openapi_contracts/doc.rb +17 -14
  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 +11 -5
  30. metadata +31 -20
  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: e6c02afb8cd5c3ad005ff169eb4c1294b8de98dd97c4cd643609a8f3a5cc66d5
4
- data.tar.gz: e8808416c903da9662789d67da35fa6b2f44e9ba7659b7baa675ef5cdabbcd8a
3
+ metadata.gz: 6a6d176876364825de3b5e11746a49add3125a4511003e24002f8dda270347d6
4
+ data.tar.gz: 468ebb8a26dadc72b3f0d9fdb0e9a2f8a39b2e94418c555926fe0b12d0ea0b8a
5
5
  SHA512:
6
- metadata.gz: 199fa68a93326277251d413ff5e6c3d2b4718b3345a74cb79f227252a4e741f4cb179f05eaa304b2807b29e64f5510a0c9f5bec817c9373337c68a7f8a1ba8d1
7
- data.tar.gz: 81c16071342cf8203380e85ced5bf52974231371d269c72ea4d182b472148b4e801c12cc3ca699237643559c1a52dd935f9b984f73c974e51002061da8b3dfa3
6
+ metadata.gz: c310ffa9c64a85d3cef12f2e2fa685ae52f0bfeff975ab63a1532f88be964dc2acd577b24f4a70017efb0bc4b19c0a522a56b9660686ba33c89d970959ca1690
7
+ data.tar.gz: 1a2293a48671a21d6f6915162c1240fa56b719e2d40947b8e46782635c6cc6cd7d4e26ee6012f7360b098d1ab04bf82f282e090fb373d9e1e9ddb982a6672dce
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,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($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 is especially important with
51
- dynamic paths.
52
49
 
53
- 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.
54
54
 
55
55
  ```ruby
56
- 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) }
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
- ### How it works
77
+ ## How it works
69
78
 
70
- It uses the `request.path`, `request.method`, `status` and `headers` on the test subject (which must be the response) to find the response schema in the OpenAPI document. Then it does the following checks:
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
- def initialize(schema)
4
- @schema = schema
3
+ include Doc::WithParameters
5
4
 
6
- @methods = (known_http_methods & @schema.keys).to_h do |method|
7
- [method, Doc::Method.new(@schema.navigate(method))]
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 methods
12
- @methods.each_value
32
+ def static?
33
+ !dynamic?
13
34
  end
14
35
 
15
- def with_method(method)
16
- @methods[method]
36
+ def supports_method?(method)
37
+ @supported_methods.include?(method)
17
38
  end
18
39
 
19
- private
40
+ def with_method(method)
41
+ return unless supports_method?(method)
20
42
 
21
- def known_http_methods
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(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,26 @@ 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, :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 as_h
36
- return @raw if pointer.nil? || pointer.empty?
66
+ def openapi_version
67
+ @raw['openapi']&.then { |v| Gem::Version.new(v) }
68
+ end
37
69
 
38
- @raw.dig(*pointer)
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(*spointer)
42
- self.class.new(@raw, (pointer + Array.wrap(spointer)))
75
+ def navigate(*segments)
76
+ self.class.new(@raw, pointer.navigate(segments)).follow_refs
43
77
  end
44
78
  end
45
79
  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,12 +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 :Path, 'openapi_contracts/doc/path'
8
- autoload :Response, 'openapi_contracts/doc/response'
9
- 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'
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(schema)
18
- @schema = Schema.new(schema)
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 response_for(path, method, status)
30
- with_path(path)&.with_method(method)&.with_status(status)
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.methods.each do |method|
39
- method.responses.each(&block)
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
- @options = options
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 lookup_api_spec
21
- @doc.response_for(
22
- @options.fetch(:path, @response.request.path),
23
- @response.request.request_method.downcase,
24
- @response.status.to_s
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 matchers
29
- env = Env.new(lookup_api_spec, @response, @options[:status])
30
- Validators::ALL.reverse
31
- .reduce(->(err) { err }) { |s, m| m.new(s, env) }
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
@@ -0,0 +1,15 @@
1
+ module OpenapiContracts::Parser::Transformers
2
+ class Base
3
+ def initialize(parser, cwd, pointer)
4
+ @parser = parser
5
+ @cwd = cwd
6
+ @pointer = pointer
7
+ end
8
+
9
+ # :nocov:
10
+ def call
11
+ raise NotImplementedError
12
+ end
13
+ # :nocov:
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module OpenapiContracts::Parser::Transformers
2
+ class Nullable < Base
3
+ def call(object)
4
+ return unless object['type'].present? && object['nullable'] == true
5
+
6
+ object.delete('nullable')
7
+ object['type'] = [object['type'], 'null']
8
+ end
9
+ end
10
+ end