openapi_contracts 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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