skooma 0.3.0 → 0.3.1

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: 680f73ad3319719625bdf805f0c50d8ce56ec6294329dcf225a6e7f4bf29d53f
4
+ data.tar.gz: 7e512d7bb1d3a39d03e35a63ec687c9de23c35d3f13b777d7b1bbbdf0880e529
5
5
  SHA512:
6
- metadata.gz: 4392d35e7aade0d2683f38ef782ed6b2b980aee835ea489f173202ff75627a4cff4bc3ae522bb9e06ed6b69d7b073a08520902a3a6388cd22f3e8321bbedbb08
7
- data.tar.gz: 990bfcb689cd4ad8f76060c955abf9ce01fd3513a54499592136045d1f87f3000575356b1d0e7ddea78b16cc76e516d099dfeda18c35c36426a6426e933068ea
6
+ metadata.gz: ecc1c3a07b2fc417fbc47ba497ee53af09f3186ecf8d35c13a7e0d8e03bc0cfc0ca9bacdc6bd5a879d46f7b8d517858d605cafc02494d11bc39a794f0c385eea
7
+ data.tar.gz: 94f69160fb4914679a94bd28ccc48e7e5d0a9664a1cb26ed469906be66e3232dc7be13c4ce11190d04b08ebce52557c17e24b57adb14d0b15542f9fa0994fb52
data/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2024-04-11
11
+
12
+ ### Added
13
+
14
+ - Add coverage for tested API operations. ([@skryukov])
15
+
16
+ ```ruby
17
+
18
+ # spec/rails_helper.rb
19
+
20
+ RSpec.configure do |config|
21
+ # To enable coverage, pass `coverage: :report` option,
22
+ # and to raise an error when an operation is not covered, pass `coverage: :strict` option:
23
+ config.include Skooma::RSpec[Rails.root.join("docs", "openapi.yml"), coverage: :report], type: :request
24
+ end
25
+ ```
26
+
27
+ ```shell
28
+ $ bundle exec rspec
29
+ # ...
30
+ OpenAPI schema /openapi.yml coverage report: 110 / 194 operations (56.7%) covered.
31
+ Uncovered paths:
32
+ GET /api/uncovered 200
33
+ GET /api/partially_covered 403
34
+ # ...
35
+ ```
36
+
10
37
  ## [0.3.0] - 2024-04-09
11
38
 
12
39
  ### Changed
@@ -39,16 +66,16 @@ and this project adheres to [Semantic Versioning].
39
66
 
40
67
  - Add support for APIs mounted under a path prefix. ([@skryukov])
41
68
 
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
- ```
69
+ ```ruby
70
+ # spec/rails_helper.rb
71
+
72
+ RSpec.configure do |config|
73
+ # ...
74
+ path_to_openapi = Rails.root.join("docs", "openapi.yml")
75
+ # pass path_prefix option if your API is mounted under a prefix:
76
+ config.include Skooma::RSpec[path_to_openapi, path_prefix: "/internal/api"], type: :request
77
+ end
78
+ ```
52
79
 
53
80
  ### Changed
54
81
 
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
@@ -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::Unit.after_tests { 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.1"
5
5
  end
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.1
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-04-11 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.