skooma 0.3.0 → 0.3.2

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: 9f8caa0911924b17126cad7544d4c54e4fe3f819c6b21c3489cd47180bb69996
4
- data.tar.gz: 052a2f72272a02b99cb44f62b6a1b1a319ca1c743742c66125d4da4ae9b402c1
3
+ metadata.gz: de042bc8ca38a43927cc9f6c1fdb829aecbbf214f771f9a092345f25d2d2f36a
4
+ data.tar.gz: a084b23b9eeb66af1a36d010b99bc86ae4e1b0c39e470096fc2ef788c9afb725
5
5
  SHA512:
6
- metadata.gz: 4392d35e7aade0d2683f38ef782ed6b2b980aee835ea489f173202ff75627a4cff4bc3ae522bb9e06ed6b69d7b073a08520902a3a6388cd22f3e8321bbedbb08
7
- data.tar.gz: 990bfcb689cd4ad8f76060c955abf9ce01fd3513a54499592136045d1f87f3000575356b1d0e7ddea78b16cc76e516d099dfeda18c35c36426a6426e933068ea
6
+ metadata.gz: 4286775a7c888f90b46fb35b827ce0f0f21c10ffd4737f6823326a14e00a21786f3e976877d71aee7b07a6578b09188fbe5334c8c67e4f24b78ccfe6ccc00494
7
+ data.tar.gz: 92e474fe454174ad5d4f1703cf8b1a9c3f9bd47bbe6eb382f60912926e28e3f9888acc612cc751c18ca8d7182f8c0922507302fbde8570ade7c0c7950c76925c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.2] - 2024-06-24
11
+
12
+ ### Fixed
13
+
14
+ - Fix deprecation `MiniTest::Unit.after_tests is now Minitest.after_run`. ([@barnaclebarnes])
15
+ - Exclude test helpers from eager loading. ([@skryukov])
16
+ - Update oas-3.1 base schema. ([@skryukov])
17
+
18
+ ## [0.3.1] - 2024-04-11
19
+
20
+ ### Added
21
+
22
+ - Add coverage for tested API operations. ([@skryukov])
23
+
24
+ ```ruby
25
+
26
+ # spec/rails_helper.rb
27
+
28
+ RSpec.configure do |config|
29
+ # To enable coverage, pass `coverage: :report` option,
30
+ # and to raise an error when an operation is not covered, pass `coverage: :strict` option:
31
+ config.include Skooma::RSpec[Rails.root.join("docs", "openapi.yml"), coverage: :report], type: :request
32
+ end
33
+ ```
34
+
35
+ ```shell
36
+ $ bundle exec rspec
37
+ # ...
38
+ OpenAPI schema /openapi.yml coverage report: 110 / 194 operations (56.7%) covered.
39
+ Uncovered paths:
40
+ GET /api/uncovered 200
41
+ GET /api/partially_covered 403
42
+ # ...
43
+ ```
44
+
10
45
  ## [0.3.0] - 2024-04-09
11
46
 
12
47
  ### Changed
@@ -39,16 +74,16 @@ and this project adheres to [Semantic Versioning].
39
74
 
40
75
  - Add support for APIs mounted under a path prefix. ([@skryukov])
41
76
 
42
- ```ruby
43
- # spec/rails_helper.rb
44
-
45
- RSpec.configure do |config|
46
- # ...
47
- path_to_openapi = Rails.root.join("docs", "openapi.yml")
48
- # pass path_prefix option if your API is mounted under a prefix:
49
- config.include Skooma::RSpec[path_to_openapi, path_prefix: "/internal/api"], type: :request
50
- end
51
- ```
77
+ ```ruby
78
+ # spec/rails_helper.rb
79
+
80
+ RSpec.configure do |config|
81
+ # ...
82
+ path_to_openapi = Rails.root.join("docs", "openapi.yml")
83
+ # pass path_prefix option if your API is mounted under a prefix:
84
+ config.include Skooma::RSpec[path_to_openapi, path_prefix: "/internal/api"], type: :request
85
+ end
86
+ ```
52
87
 
53
88
  ### Changed
54
89
 
@@ -83,10 +118,13 @@ end
83
118
 
84
119
  - Initial implementation. ([@skryukov])
85
120
 
121
+ [@barnaclebarnes]: https://github.com/barnaclebarnes
86
122
  [@skryukov]: https://github.com/skryukov
87
123
  [@ursm]: https://github.com/ursm
88
124
 
89
- [Unreleased]: https://github.com/skryukov/skooma/compare/v0.3.0...HEAD
125
+ [Unreleased]: https://github.com/skryukov/skooma/compare/v0.3.2...HEAD
126
+ [0.3.2]: https://github.com/skryukov/skooma/compare/v0.3.1...v0.3.2
127
+ [0.3.1]: https://github.com/skryukov/skooma/compare/v0.3.0...v0.3.1
90
128
  [0.3.0]: https://github.com/skryukov/skooma/compare/v0.2.3...v0.3.0
91
129
  [0.2.3]: https://github.com/skryukov/skooma/compare/v0.2.2...v0.2.3
92
130
  [0.2.2]: https://github.com/skryukov/skooma/compare/v0.2.1...v0.2.2
data/README.md CHANGED
@@ -16,7 +16,7 @@ Skooma is a Ruby library for validating API implementations against OpenAPI docu
16
16
 
17
17
  ### Learn more
18
18
 
19
- - [Let there be docs! A documentation-first approach to Rails API development](https://evilmartians.com/events/let-there-be-docs-a-documentation-first-approach-to-rails-api-development) – Talk and slides from Friendly.rb 2023
19
+ - [Let there be docs! A documentation-first approach to Rails API development](https://evilmartians.com/chronicles/let-there-be-docs-a-documentation-first-approach-to-rails-api-development)
20
20
 
21
21
  <a href="https://evilmartians.com/?utm_source=skooma&utm_campaign=project_page">
22
22
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
@@ -51,6 +51,10 @@ RSpec.configure do |config|
51
51
 
52
52
  # OR pass path_prefix option if your API is mounted under a prefix:
53
53
  config.include Skooma::RSpec[path_to_openapi, path_prefix: "/internal/api"], type: :request
54
+
55
+ # To enable coverage, pass `coverage: :report` option,
56
+ # and to raise an error when an operation is not covered, pass `coverage: :strict` option:
57
+ config.include Skooma::RSpec[path_to_openapi, coverage: :report], type: :request
54
58
  end
55
59
  ```
56
60
 
@@ -115,8 +119,15 @@ end
115
119
 
116
120
  ```ruby
117
121
  # test/test_helper.rb
122
+ path_to_openapi = Rails.root.join("docs", "openapi.yml")
123
+ ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi]
124
+
125
+ # OR pass path_prefix option if your API is mounted under a prefix:
126
+ ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi, path_prefix: "/internal/api"], type: :request
118
127
 
119
- ActionDispatch::IntegrationTest.include Skooma::Minitest[Rails.root.join("docs", "openapi.yml")]
128
+ # To enable coverage, pass `coverage: :report` option,
129
+ # and to raise an error when an operation is not covered, pass `coverage: :strict` option:
130
+ ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi, coverage: :report], type: :request
120
131
  ```
121
132
 
122
133
  #### Validate OpenAPI document
@@ -18,15 +18,6 @@
18
18
  "externalDocs": { "$ref": "#/$defs/external-docs" },
19
19
  "xml": { "$ref": "#/$defs/xml" }
20
20
  },
21
- "dependentSchemas": {
22
- "discriminator": {
23
- "anyOf": [
24
- { "required": [ "oneOf" ] },
25
- { "required": [ "anyOf" ] },
26
- { "required": [ "allOf" ] }
27
- ]
28
- }
29
- },
30
21
 
31
22
  "$defs": {
32
23
  "extensible": {
@@ -167,8 +167,7 @@
167
167
  "type": "object",
168
168
  "properties": {
169
169
  "url": {
170
- "type": "string",
171
- "format": "uri-reference"
170
+ "type": "string"
172
171
  },
173
172
  "description": {
174
173
  "type": "string"
@@ -525,7 +524,7 @@
525
524
  "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie"
526
525
  },
527
526
  {
528
- "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form"
527
+ "$ref": "#/$defs/styles-for-form"
529
528
  }
530
529
  ],
531
530
  "$defs": {
@@ -542,9 +541,6 @@
542
541
  },
543
542
  "then": {
544
543
  "properties": {
545
- "name": {
546
- "pattern": "[^/#?]+$"
547
- },
548
544
  "style": {
549
545
  "default": "simple",
550
546
  "enum": [
@@ -630,32 +626,6 @@
630
626
  }
631
627
  }
632
628
  }
633
- },
634
- "styles-for-form": {
635
- "if": {
636
- "properties": {
637
- "style": {
638
- "const": "form"
639
- }
640
- },
641
- "required": [
642
- "style"
643
- ]
644
- },
645
- "then": {
646
- "properties": {
647
- "explode": {
648
- "default": true
649
- }
650
- }
651
- },
652
- "else": {
653
- "properties": {
654
- "explode": {
655
- "default": false
656
- }
657
- }
658
- }
659
629
  }
660
630
  }
661
631
  }
@@ -782,38 +752,10 @@
782
752
  "$ref": "#/$defs/specification-extensions"
783
753
  },
784
754
  {
785
- "$ref": "#/$defs/encoding/$defs/explode-default"
755
+ "$ref": "#/$defs/styles-for-form"
786
756
  }
787
757
  ],
788
- "unevaluatedProperties": false,
789
- "$defs": {
790
- "explode-default": {
791
- "if": {
792
- "properties": {
793
- "style": {
794
- "const": "form"
795
- }
796
- },
797
- "required": [
798
- "style"
799
- ]
800
- },
801
- "then": {
802
- "properties": {
803
- "explode": {
804
- "default": true
805
- }
806
- }
807
- },
808
- "else": {
809
- "properties": {
810
- "explode": {
811
- "default": false
812
- }
813
- }
814
- }
815
- }
816
- }
758
+ "unevaluatedProperties": false
817
759
  },
818
760
  "responses": {
819
761
  "$comment": "https://spec.openapis.org/oas/v3.1.0#responses-object",
@@ -1100,8 +1042,7 @@
1100
1042
  "description": {
1101
1043
  "type": "string"
1102
1044
  }
1103
- },
1104
- "unevaluatedProperties": false
1045
+ }
1105
1046
  },
1106
1047
  "schema": {
1107
1048
  "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object",
@@ -1436,6 +1377,32 @@
1436
1377
  "additionalProperties": {
1437
1378
  "type": "string"
1438
1379
  }
1380
+ },
1381
+ "styles-for-form": {
1382
+ "if": {
1383
+ "properties": {
1384
+ "style": {
1385
+ "const": "form"
1386
+ }
1387
+ },
1388
+ "required": [
1389
+ "style"
1390
+ ]
1391
+ },
1392
+ "then": {
1393
+ "properties": {
1394
+ "explode": {
1395
+ "default": true
1396
+ }
1397
+ }
1398
+ },
1399
+ "else": {
1400
+ "properties": {
1401
+ "explode": {
1402
+ "default": false
1403
+ }
1404
+ }
1405
+ }
1439
1406
  }
1440
1407
  }
1441
1408
  }
@@ -0,0 +1,90 @@
1
+ module Skooma
2
+ class NoopCoverage
3
+ def track_request(*)
4
+ end
5
+
6
+ def report
7
+ end
8
+ end
9
+
10
+ class Coverage
11
+ class SimpleReport
12
+ def initialize(coverage)
13
+ @coverage = coverage
14
+ end
15
+
16
+ attr_reader :coverage
17
+
18
+ def report
19
+ puts <<~MSG
20
+ OpenAPI schema #{URI.parse(coverage.schema.uri.to_s).path} coverage report: #{coverage.covered_paths.count} / #{coverage.defined_paths.count} operations (#{coverage.covered_percent.round(2)}%) covered.
21
+ #{coverage.uncovered_paths.empty? ? "All paths are covered!" : "Uncovered paths:"}
22
+ #{coverage.uncovered_paths.map { |method, path, status| "#{method.upcase} #{path} #{status}" }.join("\n")}
23
+ MSG
24
+ end
25
+ end
26
+
27
+ def self.new(schema, mode: nil, format: nil)
28
+ case mode
29
+ when nil, false
30
+ NoopCoverage.new
31
+ when :report, :strict
32
+ super
33
+ else
34
+ raise ArgumentError, "Invalid coverage: #{mode}, expected :report, :strict, or false"
35
+ end
36
+ end
37
+
38
+ attr_reader :mode, :format, :defined_paths, :covered_paths, :schema
39
+
40
+ def initialize(schema, mode:, format:)
41
+ @schema = schema
42
+ @mode = mode
43
+ @format = format || SimpleReport
44
+ @defined_paths = find_defined_paths(schema)
45
+ @covered_paths = Set.new
46
+ end
47
+
48
+ def track_request(result)
49
+ operation = [nil, nil, nil]
50
+ result.collect_annotations(result.instance, keys: %w[paths responses]) do |node|
51
+ case node.key
52
+ when "paths"
53
+ operation[0] = node.annotation["method"]
54
+ operation[1] = node.annotation["current_path"]
55
+ when "responses"
56
+ operation[2] = node.annotation
57
+ end
58
+ end
59
+ covered_paths << operation
60
+ end
61
+
62
+ def uncovered_paths
63
+ defined_paths - covered_paths
64
+ end
65
+
66
+ def covered_percent
67
+ covered_paths.count * 100.0 / defined_paths.count
68
+ end
69
+
70
+ def report
71
+ format.new(self).report
72
+ exit 1 if mode == :strict && uncovered_paths.any?
73
+ end
74
+
75
+ private
76
+
77
+ def find_defined_paths(schema)
78
+ Set.new.tap do |paths|
79
+ schema["paths"].each do |path, path_item|
80
+ resolved_path_item = (path_item.key?("$ref") ? path_item.resolve_ref(path_item["$ref"]) : path_item)
81
+ resolved_path_item.slice("get", "post", "put", "patch", "delete", "options", "head", "trace").each do |method, operation|
82
+ operation["responses"]&.each do |code, _|
83
+ paths << [method, path, code]
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -5,13 +5,17 @@ require "pp"
5
5
  module Skooma
6
6
  module Matchers
7
7
  class ConformRequestSchema
8
- def initialize(schema, mapped_response)
9
- @schema = schema
8
+ def initialize(skooma, mapped_response)
9
+ @skooma = skooma
10
+ @schema = skooma.schema
10
11
  @mapped_response = mapped_response
11
12
  end
12
13
 
13
14
  def matches?(*)
14
15
  @result = @schema.evaluate(@mapped_response)
16
+
17
+ @skooma.coverage.track_request(@result) if @mapped_response["response"]
18
+
15
19
  @result.valid?
16
20
  end
17
21
 
@@ -3,8 +3,8 @@
3
3
  module Skooma
4
4
  module Matchers
5
5
  class ConformResponseSchema < ConformRequestSchema
6
- def initialize(schema, mapped_response, expected)
7
- super(schema, mapped_response)
6
+ def initialize(skooma, mapped_response, expected)
7
+ super(skooma, mapped_response)
8
8
  @expected = expected
9
9
  end
10
10
 
@@ -38,30 +38,39 @@ module Skooma
38
38
 
39
39
  raise "Response object not found"
40
40
  end
41
+
42
+ def skooma_openapi_schema
43
+ skooma.schema
44
+ end
41
45
  end
42
46
 
43
- def initialize(helper_methods_module, openapi_path, base_uri: "https://skoomarb.dev/", path_prefix: "")
47
+ def initialize(helper_methods_module, openapi_path, base_uri: "https://skoomarb.dev/", path_prefix: "", **params)
44
48
  super()
45
49
 
46
50
  registry = create_test_registry
47
51
  pathname = Pathname.new(openapi_path)
48
- source_uri = "#{base_uri}#{path_prefix.delete_suffix("/")}"
52
+ source_uri = "#{base_uri}#{path_prefix.delete_suffix("/").delete_prefix("/")}"
49
53
  source_uri += "/" unless source_uri.end_with?("/")
50
54
  registry.add_source(
51
55
  source_uri,
52
56
  JSONSkooma::Sources::Local.new(pathname.dirname.to_s)
53
57
  )
54
- schema = registry.schema(URI.parse("#{source_uri}#{pathname.basename}"), schema_class: Skooma::Objects::OpenAPI)
55
- schema.path_prefix = path_prefix
58
+ @schema = registry.schema(URI.parse("#{source_uri}#{pathname.basename}"), schema_class: Skooma::Objects::OpenAPI)
59
+ @schema.path_prefix = path_prefix
60
+
61
+ @coverage = Coverage.new(@schema, mode: params[:coverage], format: params[:coverage_format])
56
62
 
57
63
  include DefaultHelperMethods
58
64
  include helper_methods_module
59
65
 
60
- define_method :skooma_openapi_schema do
61
- schema
66
+ skooma_self = self
67
+ define_method :skooma do
68
+ skooma_self
62
69
  end
63
70
  end
64
71
 
72
+ attr_accessor :schema, :coverage
73
+
65
74
  private
66
75
 
67
76
  def create_test_registry
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "minitest/unit"
4
+
3
5
  module Skooma
4
6
  # Minitest helpers for OpenAPI schema validation
5
7
  # @example
@@ -10,19 +12,19 @@ module Skooma
10
12
  class Minitest < Matchers::Wrapper
11
13
  module HelperMethods
12
14
  def assert_conform_schema(expected_status)
13
- matcher = Matchers::ConformSchema.new(skooma_openapi_schema, mapped_response, expected_status)
15
+ matcher = Matchers::ConformSchema.new(skooma, mapped_response, expected_status)
14
16
 
15
17
  assert matcher.matches?, -> { matcher.failure_message }
16
18
  end
17
19
 
18
20
  def assert_conform_request_schema
19
- matcher = Matchers::ConformRequestSchema.new(skooma_openapi_schema, mapped_response(with_response: false))
21
+ matcher = Matchers::ConformRequestSchema.new(skooma, mapped_response(with_response: false))
20
22
 
21
23
  assert matcher.matches?, -> { matcher.failure_message }
22
24
  end
23
25
 
24
26
  def assert_conform_response_schema(expected_status)
25
- matcher = Matchers::ConformResponseSchema.new(skooma_openapi_schema, mapped_response(with_request: false), expected_status)
27
+ matcher = Matchers::ConformResponseSchema.new(skooma, mapped_response(with_request: false), expected_status)
26
28
 
27
29
  assert matcher.matches?, -> { matcher.failure_message }
28
30
  end
@@ -36,6 +38,8 @@ module Skooma
36
38
 
37
39
  def initialize(openapi_path, **params)
38
40
  super(HelperMethods, openapi_path, **params)
41
+
42
+ MiniTest.after_run { coverage.report }
39
43
  end
40
44
  end
41
45
  end
@@ -28,6 +28,8 @@ module Skooma
28
28
 
29
29
  return result.failure("Path #{instance["path"]} not found in schema") unless path
30
30
 
31
+ result.annotate({"current_path" => path})
32
+
31
33
  result.call(instance, path) do |subresult|
32
34
  subresult.annotate({"path_attributes" => attributes})
33
35
  path_schema.evaluate(instance, subresult)
@@ -13,12 +13,16 @@ module Skooma
13
13
  end
14
14
 
15
15
  json.evaluate(instance, result)
16
- return result.success if result.passed?
17
16
 
18
17
  path_item_result = result.parent
19
18
  path_item_result = path_item_result.parent until path_item_result.key.start_with?("/")
20
19
 
21
- path = path_item_result.annotation["path"]
20
+ paths_result = path_item_result.parent
21
+ paths_result.annotate(paths_result.annotation.merge("method" => key))
22
+
23
+ return result.success if result.passed?
24
+
25
+ path = paths_result.annotation["current_path"]
22
26
 
23
27
  result.failure("Path #{path}/#{key} is invalid")
24
28
  end
@@ -5,7 +5,7 @@ module Skooma
5
5
  class PathItem
6
6
  module Keywords
7
7
  class Delete < BaseOperation
8
- self.key = "options"
8
+ self.key = "delete"
9
9
  self.depends_on = %w[parameters]
10
10
  self.value_schema = :schema
11
11
  self.schema_value_class = Objects::Operation
data/lib/skooma/rspec.rb CHANGED
@@ -10,15 +10,15 @@ module Skooma
10
10
  class RSpec < Matchers::Wrapper
11
11
  module HelperMethods
12
12
  def conform_schema(expected_status)
13
- Matchers::ConformSchema.new(skooma_openapi_schema, mapped_response, expected_status)
13
+ Matchers::ConformSchema.new(skooma, mapped_response, expected_status)
14
14
  end
15
15
 
16
16
  def conform_response_schema(expected_status)
17
- Matchers::ConformResponseSchema.new(skooma_openapi_schema, mapped_response(with_request: false), expected_status)
17
+ Matchers::ConformResponseSchema.new(skooma, mapped_response(with_request: false), expected_status)
18
18
  end
19
19
 
20
20
  def conform_request_schema
21
- Matchers::ConformRequestSchema.new(skooma_openapi_schema, mapped_response(with_response: false))
21
+ Matchers::ConformRequestSchema.new(skooma, mapped_response(with_response: false))
22
22
  end
23
23
 
24
24
  def be_valid_document
@@ -28,6 +28,13 @@ module Skooma
28
28
 
29
29
  def initialize(openapi_path, **params)
30
30
  super(HelperMethods, openapi_path, **params)
31
+
32
+ skooma_self = self
33
+ ::RSpec.configure do |c|
34
+ c.after(:suite) do
35
+ at_exit { skooma_self.coverage.report }
36
+ end
37
+ end
31
38
  end
32
39
  end
33
40
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skooma
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  end
data/lib/skooma.rb CHANGED
@@ -7,6 +7,11 @@ require_relative "skooma/inflector"
7
7
 
8
8
  loader = Zeitwerk::Loader.for_gem
9
9
  loader.inflector = Skooma::Inflector.new(__FILE__)
10
+
11
+ # Do not eager load the test helpers
12
+ loader.do_not_eager_load(File.join(__dir__, "skooma", "minitest.rb"))
13
+ loader.do_not_eager_load(File.join(__dir__, "skooma", "rspec.rb"))
14
+
10
15
  loader.setup
11
16
 
12
17
  module Skooma
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skooma
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-09 00:00:00.000000000 Z
11
+ date: 2024-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -54,6 +54,7 @@ files:
54
54
  - data/oas-3.1/schema/2022-10-07.json
55
55
  - lib/skooma.rb
56
56
  - lib/skooma/body_parsers.rb
57
+ - lib/skooma/coverage.rb
57
58
  - lib/skooma/dialects/oas_3_1.rb
58
59
  - lib/skooma/env_mapper.rb
59
60
  - lib/skooma/inflector.rb
@@ -163,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
164
  - !ruby/object:Gem::Version
164
165
  version: '0'
165
166
  requirements: []
166
- rubygems_version: 3.3.7
167
+ rubygems_version: 3.5.7
167
168
  signing_key:
168
169
  specification_version: 4
169
170
  summary: Validate API implementations against OpenAPI documents.