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.
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