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