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 +4 -4
- data/CHANGELOG.md +37 -10
- data/README.md +13 -2
- data/lib/skooma/coverage.rb +90 -0
- data/lib/skooma/matchers/conform_request_schema.rb +6 -2
- data/lib/skooma/matchers/conform_response_schema.rb +2 -2
- data/lib/skooma/matchers/wrapper.rb +15 -6
- data/lib/skooma/minitest.rb +7 -3
- data/lib/skooma/objects/openapi/keywords/paths.rb +2 -0
- data/lib/skooma/objects/path_item/keywords/base_operation.rb +6 -2
- data/lib/skooma/objects/path_item/keywords/delete.rb +1 -1
- data/lib/skooma/rspec.rb +10 -3
- data/lib/skooma/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 680f73ad3319719625bdf805f0c50d8ce56ec6294329dcf225a6e7f4bf29d53f
|
4
|
+
data.tar.gz: 7e512d7bb1d3a39d03e35a63ec687c9de23c35d3f13b777d7b1bbbdf0880e529
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
48
|
-
|
49
|
-
|
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/
|
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
|
-
|
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(
|
9
|
-
@
|
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(
|
7
|
-
super(
|
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
|
-
|
61
|
-
|
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
|
data/lib/skooma/minitest.rb
CHANGED
@@ -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(
|
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(
|
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(
|
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
|
-
|
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
|
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(
|
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(
|
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(
|
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
|
data/lib/skooma/version.rb
CHANGED
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.
|
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-
|
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.
|
167
|
+
rubygems_version: 3.5.7
|
167
168
|
signing_key:
|
168
169
|
specification_version: 4
|
169
170
|
summary: Validate API implementations against OpenAPI documents.
|