skooma 0.3.0 → 0.3.1

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