openapi_contracts 0.7.1 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6c02afb8cd5c3ad005ff169eb4c1294b8de98dd97c4cd643609a8f3a5cc66d5
4
- data.tar.gz: e8808416c903da9662789d67da35fa6b2f44e9ba7659b7baa675ef5cdabbcd8a
3
+ metadata.gz: 25a37c81bfa9cb9d1d26f26a25b7c81f0675a153c199223639bdd48c76537cb6
4
+ data.tar.gz: 48529ed541f4ad72568d222d2a57e30ef7f1a7b5e59fbb447961a58d57eafe26
5
5
  SHA512:
6
- metadata.gz: 199fa68a93326277251d413ff5e6c3d2b4718b3345a74cb79f227252a4e741f4cb179f05eaa304b2807b29e64f5510a0c9f5bec817c9373337c68a7f8a1ba8d1
7
- data.tar.gz: 81c16071342cf8203380e85ced5bf52974231371d269c72ea4d182b472148b4e801c12cc3ca699237643559c1a52dd935f9b984f73c974e51002061da8b3dfa3
6
+ metadata.gz: 1cc4ee62cea4e015834ae6a493bc771f13670b553cfdc1085b7dac79ea5a347583786ec4cd1db31c9054d88a010dd5d779c64f13df4388a3f55a3eae07ef577a
7
+ data.tar.gz: 9991124beb074d92107b1026e6ffd529cca988d8154eac6c6cc92aa5103df5de11cf917d1809545e26810de07959202f295a5d23a1a15077e639d2212bbc6286
data/README.md CHANGED
@@ -47,8 +47,9 @@ it 'responds with 200 and matches the doc' do
47
47
 
48
48
  The `match_openapi_doc($doc)` method allows passing options as a 2nd argument.
49
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.
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.
52
53
 
53
54
  Example:
54
55
 
@@ -67,7 +68,9 @@ raise result.errors.merge("/n") unless result.valid?
67
68
 
68
69
  ### How it works
69
70
 
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:
71
+ 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.
73
+ Then it does the following checks:
71
74
 
72
75
  * The response is documented
73
76
  * Required headers are present
@@ -0,0 +1,86 @@
1
+ module OpenapiContracts
2
+ class Doc::Parameter
3
+ attr_reader :schema
4
+
5
+ def initialize(options)
6
+ @name = options[:name]
7
+ @in = options[:in]
8
+ @required = options[:required]
9
+ @schema = options[:schema]
10
+ end
11
+
12
+ def matches?(value)
13
+ case schema['type']
14
+ when 'integer'
15
+ integer_parameter_matches?(value)
16
+ when 'number'
17
+ number_parameter_matches?(value)
18
+ when 'string'
19
+ string_parameter_matches?(value)
20
+ else
21
+ # Not yet implemented
22
+ false
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def integer_parameter_matches?(value)
29
+ return false unless /^-?\d+$/.match?(value)
30
+
31
+ parsed = value.to_i
32
+ return false unless minimum_number_matches?(parsed)
33
+ return false unless maximum_number_matches?(parsed)
34
+
35
+ true
36
+ end
37
+
38
+ def number_parameter_matches?(value)
39
+ return false unless /^-?(\d+\.)?\d+$/.match?(value)
40
+
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
84
+ end
85
+ end
86
+ end
@@ -1,6 +1,7 @@
1
1
  module OpenapiContracts
2
2
  class Doc::Path
3
- def initialize(schema)
3
+ def initialize(path, schema)
4
+ @path = path
4
5
  @schema = schema
5
6
 
6
7
  @methods = (known_http_methods & @schema.keys).to_h do |method|
@@ -8,16 +9,50 @@ module OpenapiContracts
8
9
  end
9
10
  end
10
11
 
12
+ def dynamic?
13
+ @path.include?('{')
14
+ end
15
+
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
23
+ end
24
+
11
25
  def methods
12
26
  @methods.each_value
13
27
  end
14
28
 
29
+ def static?
30
+ !dynamic?
31
+ end
32
+
15
33
  def with_method(method)
16
34
  @methods[method]
17
35
  end
18
36
 
19
37
  private
20
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
55
+
21
56
  def known_http_methods
22
57
  # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
23
58
  %w(get head post put delete connect options trace patch).freeze
@@ -4,6 +4,7 @@ module OpenapiContracts
4
4
  autoload :FileParser, 'openapi_contracts/doc/file_parser'
5
5
  autoload :Method, 'openapi_contracts/doc/method'
6
6
  autoload :Parser, 'openapi_contracts/doc/parser'
7
+ autoload :Parameter, 'openapi_contracts/doc/parameter'
7
8
  autoload :Path, 'openapi_contracts/doc/path'
8
9
  autoload :Response, 'openapi_contracts/doc/response'
9
10
  autoload :Schema, 'openapi_contracts/doc/schema'
@@ -17,8 +18,9 @@ module OpenapiContracts
17
18
  def initialize(schema)
18
19
  @schema = Schema.new(schema)
19
20
  @paths = @schema['paths'].to_h do |path, _|
20
- [path, Path.new(@schema.at_pointer(['paths', path]))]
21
+ [path, Path.new(path, @schema.at_pointer(['paths', path]))]
21
22
  end
23
+ @dynamic_paths = paths.select(&:dynamic?)
22
24
  end
23
25
 
24
26
  # Returns an Enumerator over all paths
@@ -42,7 +44,11 @@ module OpenapiContracts
42
44
  end
43
45
 
44
46
  def with_path(path)
45
- @paths[path]
47
+ if @paths.key?(path)
48
+ @paths[path]
49
+ else
50
+ @dynamic_paths.find { |p| p.matches?(path) }
51
+ end
46
52
  end
47
53
  end
48
54
  end
@@ -23,9 +23,9 @@ module OpenapiContracts::Validators
23
23
 
24
24
  def error_to_message(error)
25
25
  if error.key?('details')
26
- error['details'].to_a.map do |(key, val)|
26
+ error['details'].to_a.map { |(key, val)|
27
27
  "#{key.humanize}: #{val} at #{error['data_pointer']}"
28
- end.to_sentence
28
+ }.to_sentence
29
29
  else
30
30
  "#{error['data'].inspect} at #{error['data_pointer']} does not match the schema"
31
31
  end
@@ -1,5 +1,6 @@
1
1
  require 'active_support'
2
2
  require 'active_support/core_ext/array'
3
+ require 'active_support/core_ext/hash'
3
4
  require 'active_support/core_ext/class'
4
5
  require 'active_support/core_ext/module'
5
6
  require 'active_support/core_ext/string'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_contracts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mkon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-15 00:00:00.000000000 Z
11
+ date: 2023-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -141,6 +141,7 @@ files:
141
141
  - lib/openapi_contracts/doc/file_parser.rb
142
142
  - lib/openapi_contracts/doc/header.rb
143
143
  - lib/openapi_contracts/doc/method.rb
144
+ - lib/openapi_contracts/doc/parameter.rb
144
145
  - lib/openapi_contracts/doc/parser.rb
145
146
  - lib/openapi_contracts/doc/path.rb
146
147
  - lib/openapi_contracts/doc/response.rb