skooma 0.3.2 → 0.3.4
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 +26 -1
- data/README.md +4 -0
- data/lib/skooma/body_parsers.rb +24 -2
- data/lib/skooma/dialects/oas_3_1.rb +3 -0
- data/lib/skooma/keywords/oas_3_1/dialect/additional_properties.rb +63 -0
- data/lib/skooma/keywords/oas_3_1/dialect/properties.rb +50 -0
- data/lib/skooma/keywords/oas_3_1/dialect/required.rb +51 -0
- data/lib/skooma/matchers/wrapper.rb +2 -1
- data/lib/skooma/minitest.rb +2 -2
- data/lib/skooma/objects/openapi.rb +10 -0
- data/lib/skooma/version.rb +1 -1
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44e41865174e5bfeb3bf254ed74bb980aaa5d6aea0ca937fbea38b36242ac2ca
|
4
|
+
data.tar.gz: 0b692735b5e0cd2e079b6a7d804b20c53d5069f5e6ed700117fb825f341e71a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cbc3a15cf9640bb7bb5614d3b1610db5742218612168f873b74fc9343c1a20855e7a7809bdc3f0da7797071fedf5651057bd7a98fa0b29834e85eed4c453295
|
7
|
+
data.tar.gz: 24717dabd2a57266162aa5827075e7c5c97d8d387faf782549887f1c0fc5383b4109679283f62320f45b01e8a6e6860bab66214e4e30224096ee1267b3b0dc3b
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning].
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.3.4] - 2025-01-14
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Experimental support for `readOnly` and `writeOnly` keywords. ([@skryukov])
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# spec/rails_helper.rb
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
# To enable support for readOnly and writeOnly keywords, pass `enforce_access_modes: true` option:
|
21
|
+
config.include Skooma::RSpec[Rails.root.join("docs", "openapi.yml"), enforce_access_modes: true], type: :request
|
22
|
+
end
|
23
|
+
```
|
24
|
+
- Support fallback parsers for vendor-specific media types. ([@pvcarrera], [@skryukov])
|
25
|
+
|
26
|
+
## [0.3.3] - 2024-10-14
|
27
|
+
|
28
|
+
### Fixed
|
29
|
+
|
30
|
+
- Fix coverage for Minitest. ([@skryukov])
|
31
|
+
|
10
32
|
## [0.3.2] - 2024-06-24
|
11
33
|
|
12
34
|
### Fixed
|
@@ -119,10 +141,13 @@ and this project adheres to [Semantic Versioning].
|
|
119
141
|
- Initial implementation. ([@skryukov])
|
120
142
|
|
121
143
|
[@barnaclebarnes]: https://github.com/barnaclebarnes
|
144
|
+
[@pvcarrera]: https://github.com/pvcarrera
|
122
145
|
[@skryukov]: https://github.com/skryukov
|
123
146
|
[@ursm]: https://github.com/ursm
|
124
147
|
|
125
|
-
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.3.
|
148
|
+
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.3.4...HEAD
|
149
|
+
[0.3.4]: https://github.com/skryukov/skooma/compare/v0.3.3...v0.3.4
|
150
|
+
[0.3.3]: https://github.com/skryukov/skooma/compare/v0.3.2...v0.3.3
|
126
151
|
[0.3.2]: https://github.com/skryukov/skooma/compare/v0.3.1...v0.3.2
|
127
152
|
[0.3.1]: https://github.com/skryukov/skooma/compare/v0.3.0...v0.3.1
|
128
153
|
[0.3.0]: https://github.com/skryukov/skooma/compare/v0.2.3...v0.3.0
|
data/README.md
CHANGED
@@ -128,6 +128,10 @@ ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi, path_p
|
|
128
128
|
# To enable coverage, pass `coverage: :report` option,
|
129
129
|
# and to raise an error when an operation is not covered, pass `coverage: :strict` option:
|
130
130
|
ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi, coverage: :report], type: :request
|
131
|
+
|
132
|
+
# EXPERIMENTAL
|
133
|
+
# To enable support for readOnly and writeOnly keywords, pass `enforce_access_modes: true` option:
|
134
|
+
ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi, enforce_access_modes: true], type: :request
|
131
135
|
```
|
132
136
|
|
133
137
|
#### Validate OpenAPI document
|
data/lib/skooma/body_parsers.rb
CHANGED
@@ -6,16 +6,38 @@ module Skooma
|
|
6
6
|
DEFAULT_PARSER = ->(body, **_options) { body }
|
7
7
|
|
8
8
|
def [](media_type)
|
9
|
-
|
9
|
+
key = normalize_media_type(media_type)
|
10
|
+
parsers[key] ||
|
11
|
+
find_suffix_parser(key) ||
|
12
|
+
find_fallback_parser(key) ||
|
13
|
+
DEFAULT_PARSER
|
10
14
|
end
|
11
15
|
|
12
16
|
attr_accessor :parsers
|
13
17
|
|
14
18
|
def register(*media_types, parser)
|
15
19
|
media_types.each do |media_type|
|
16
|
-
parsers[media_type
|
20
|
+
parsers[normalize_media_type(media_type)] = parser
|
17
21
|
end
|
18
22
|
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def normalize_media_type(media_type)
|
27
|
+
media_type.to_s.strip.downcase
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_suffix_parser(media_type)
|
31
|
+
return unless media_type.include?("+")
|
32
|
+
|
33
|
+
suffix = media_type.split("+").last
|
34
|
+
parsers["application/#{suffix}"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_fallback_parser(media_type)
|
38
|
+
type = media_type.split("/").first
|
39
|
+
parsers["#{type}/*"] || parsers["*/*"]
|
40
|
+
end
|
19
41
|
end
|
20
42
|
self.parsers = {}
|
21
43
|
|
@@ -16,6 +16,9 @@ module Skooma
|
|
16
16
|
Skooma::Keywords::OAS31::Dialect::OneOf,
|
17
17
|
Skooma::Keywords::OAS31::Dialect::Discriminator,
|
18
18
|
Skooma::Keywords::OAS31::Dialect::Xml,
|
19
|
+
Skooma::Keywords::OAS31::Dialect::Properties,
|
20
|
+
Skooma::Keywords::OAS31::Dialect::AdditionalProperties,
|
21
|
+
Skooma::Keywords::OAS31::Dialect::Required,
|
19
22
|
Skooma::Keywords::OAS31::Dialect::ExternalDocs,
|
20
23
|
Skooma::Keywords::OAS31::Dialect::Example
|
21
24
|
)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Keywords
|
5
|
+
module OAS31
|
6
|
+
module Dialect
|
7
|
+
class AdditionalProperties < JSONSkooma::Keywords::Applicator::AdditionalProperties
|
8
|
+
self.key = "additionalProperties"
|
9
|
+
self.instance_types = "object"
|
10
|
+
self.value_schema = :schema
|
11
|
+
self.depends_on = %w[properties patternProperties]
|
12
|
+
|
13
|
+
def evaluate(instance, result)
|
14
|
+
known_property_names = result.sibling(instance, "properties")&.schema_node&.keys || []
|
15
|
+
known_property_patterns = (result.sibling(instance, "patternProperties")&.schema_node&.keys || [])
|
16
|
+
.map { |pattern| Regexp.new(pattern) }
|
17
|
+
|
18
|
+
forbidden = []
|
19
|
+
|
20
|
+
if json.root.enforce_access_modes?
|
21
|
+
only_key = result.path.include?("responses") ? "writeOnly" : "readOnly"
|
22
|
+
properties_result = result.sibling(instance, "properties")
|
23
|
+
instance.each_key do |name|
|
24
|
+
res = properties_result&.children&.[](instance[name]&.path)&.[]name
|
25
|
+
forbidden << name.tap { puts "adding #{name}" } if annotation_exists?(res, key: only_key)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
annotation = []
|
30
|
+
error = []
|
31
|
+
|
32
|
+
instance.each do |name, item|
|
33
|
+
if forbidden.include?(name) || !known_property_names.include?(name) && known_property_patterns.none? { |pattern| pattern.match?(name) }
|
34
|
+
if json.evaluate(item, result).passed?
|
35
|
+
annotation << name
|
36
|
+
else
|
37
|
+
error << name
|
38
|
+
# reset to success for the next iteration
|
39
|
+
result.success
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
return result.annotate(annotation) if error.empty?
|
44
|
+
|
45
|
+
result.failure(error)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def annotation_exists?(result, key:)
|
51
|
+
return result if result.key == key && result.annotation
|
52
|
+
|
53
|
+
result.each_children do |child|
|
54
|
+
return child if annotation_exists?(child, key: key)
|
55
|
+
end
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Keywords
|
5
|
+
module OAS31
|
6
|
+
module Dialect
|
7
|
+
class Properties < JSONSkooma::Keywords::Applicator::Properties
|
8
|
+
self.key = "properties"
|
9
|
+
self.instance_types = "object"
|
10
|
+
self.value_schema = :object_of_schemas
|
11
|
+
|
12
|
+
def evaluate(instance, result)
|
13
|
+
annotation = []
|
14
|
+
err_names = []
|
15
|
+
instance.each do |name, item|
|
16
|
+
next unless json.value.key?(name)
|
17
|
+
|
18
|
+
result.call(item, name) do |subresult|
|
19
|
+
json[name].evaluate(item, subresult)
|
20
|
+
if ignored_with_only_key?(subresult)
|
21
|
+
subresult.discard
|
22
|
+
elsif subresult.passed?
|
23
|
+
annotation << name
|
24
|
+
else
|
25
|
+
err_names << name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
return result.annotate(annotation) if err_names.empty?
|
31
|
+
|
32
|
+
result.failure("Properties #{err_names.join(", ")} are invalid")
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def ignored_with_only_key?(subresult)
|
38
|
+
return false unless json.root.enforce_access_modes?
|
39
|
+
|
40
|
+
if subresult.parent.path.include?("responses")
|
41
|
+
subresult.children["readOnly"]&.value == true
|
42
|
+
else
|
43
|
+
subresult.children["writeOnly"]&.value == true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Keywords
|
5
|
+
module OAS31
|
6
|
+
module Dialect
|
7
|
+
class Required < JSONSkooma::Keywords::Validation::Required
|
8
|
+
self.key = "required"
|
9
|
+
self.instance_types = "object"
|
10
|
+
self.depends_on = %w[properties]
|
11
|
+
|
12
|
+
def evaluate(instance, result)
|
13
|
+
missing = required_keys.reject { |key| instance.key?(key) }
|
14
|
+
return if missing.none?
|
15
|
+
|
16
|
+
if json.root.enforce_access_modes?
|
17
|
+
properties_schema = result.sibling(instance, "properties")&.schema_node || {}
|
18
|
+
only_key = result.path.include?("responses") ? "writeOnly" : "readOnly"
|
19
|
+
ignore = []
|
20
|
+
missing.each do |name|
|
21
|
+
next unless properties_schema.key?(name)
|
22
|
+
|
23
|
+
result.call(nil, name) do |subresult|
|
24
|
+
properties_schema[name].evaluate(nil, subresult)
|
25
|
+
ignore << name if annotation_exists?(subresult, key: only_key)
|
26
|
+
subresult.discard
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
return if (missing - ignore).none?
|
31
|
+
end
|
32
|
+
|
33
|
+
result.failure(missing_keys_message(missing))
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def annotation_exists?(result, key:)
|
39
|
+
return result if result.key == key && result.annotation
|
40
|
+
|
41
|
+
result.each_children do |child|
|
42
|
+
return child if annotation_exists?(child, key: key)
|
43
|
+
end
|
44
|
+
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -44,7 +44,7 @@ module Skooma
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
def initialize(helper_methods_module, openapi_path, base_uri: "https://skoomarb.dev/", path_prefix: "", **params)
|
47
|
+
def initialize(helper_methods_module, openapi_path, base_uri: "https://skoomarb.dev/", path_prefix: "", enforce_access_modes: false, **params)
|
48
48
|
super()
|
49
49
|
|
50
50
|
registry = create_test_registry
|
@@ -57,6 +57,7 @@ module Skooma
|
|
57
57
|
)
|
58
58
|
@schema = registry.schema(URI.parse("#{source_uri}#{pathname.basename}"), schema_class: Skooma::Objects::OpenAPI)
|
59
59
|
@schema.path_prefix = path_prefix
|
60
|
+
@schema.enforce_access_modes = enforce_access_modes
|
60
61
|
|
61
62
|
@coverage = Coverage.new(@schema, mode: params[:coverage], format: params[:coverage_format])
|
62
63
|
|
data/lib/skooma/minitest.rb
CHANGED
@@ -6,7 +6,7 @@ module Skooma
|
|
6
6
|
# Minitest helpers for OpenAPI schema validation
|
7
7
|
# @example
|
8
8
|
# describe TestApp do
|
9
|
-
# include Skooma::
|
9
|
+
# include Skooma::Minitest[Rails.root.join("docs", "openapi.yml")]
|
10
10
|
# # ...
|
11
11
|
# end
|
12
12
|
class Minitest < Matchers::Wrapper
|
@@ -39,7 +39,7 @@ module Skooma
|
|
39
39
|
def initialize(openapi_path, **params)
|
40
40
|
super(HelperMethods, openapi_path, **params)
|
41
41
|
|
42
|
-
|
42
|
+
::Minitest.after_run { coverage.report }
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
@@ -39,6 +39,16 @@ module Skooma
|
|
39
39
|
@path_prefix = @path_prefix.delete_suffix("/") if @path_prefix.end_with?("/")
|
40
40
|
end
|
41
41
|
|
42
|
+
def enforce_access_modes=(value)
|
43
|
+
raise ArgumentError, "Enforce access modes must be a boolean" unless [true, false].include?(value)
|
44
|
+
|
45
|
+
@enforce_access_modes = value
|
46
|
+
end
|
47
|
+
|
48
|
+
def enforce_access_modes?
|
49
|
+
@enforce_access_modes
|
50
|
+
end
|
51
|
+
|
42
52
|
def path_prefix
|
43
53
|
@path_prefix || ""
|
44
54
|
end
|
data/lib/skooma/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Svyatoslav Kryukov
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-14 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: zeitwerk
|
@@ -30,14 +29,14 @@ dependencies:
|
|
30
29
|
requirements:
|
31
30
|
- - "~>"
|
32
31
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.2.
|
32
|
+
version: 0.2.5
|
34
33
|
type: :runtime
|
35
34
|
prerelease: false
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
37
36
|
requirements:
|
38
37
|
- - "~>"
|
39
38
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.2.
|
39
|
+
version: 0.2.5
|
41
40
|
description: Apply a documentation-first approach to API development.
|
42
41
|
email:
|
43
42
|
- me@skryukov.dev
|
@@ -60,11 +59,14 @@ files:
|
|
60
59
|
- lib/skooma/inflector.rb
|
61
60
|
- lib/skooma/instance.rb
|
62
61
|
- lib/skooma/keywords/oas_3_1.rb
|
62
|
+
- lib/skooma/keywords/oas_3_1/dialect/additional_properties.rb
|
63
63
|
- lib/skooma/keywords/oas_3_1/dialect/any_of.rb
|
64
64
|
- lib/skooma/keywords/oas_3_1/dialect/discriminator.rb
|
65
65
|
- lib/skooma/keywords/oas_3_1/dialect/example.rb
|
66
66
|
- lib/skooma/keywords/oas_3_1/dialect/external_docs.rb
|
67
67
|
- lib/skooma/keywords/oas_3_1/dialect/one_of.rb
|
68
|
+
- lib/skooma/keywords/oas_3_1/dialect/properties.rb
|
69
|
+
- lib/skooma/keywords/oas_3_1/dialect/required.rb
|
68
70
|
- lib/skooma/keywords/oas_3_1/dialect/xml.rb
|
69
71
|
- lib/skooma/keywords/oas_3_1/schema.rb
|
70
72
|
- lib/skooma/matchers/be_valid_document.rb
|
@@ -149,7 +151,6 @@ metadata:
|
|
149
151
|
homepage_uri: https://github.com/skryukov/skooma
|
150
152
|
source_code_uri: https://github.com/skryukov/skooma
|
151
153
|
rubygems_mfa_required: 'true'
|
152
|
-
post_install_message:
|
153
154
|
rdoc_options: []
|
154
155
|
require_paths:
|
155
156
|
- lib
|
@@ -164,8 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
165
|
- !ruby/object:Gem::Version
|
165
166
|
version: '0'
|
166
167
|
requirements: []
|
167
|
-
rubygems_version: 3.
|
168
|
-
signing_key:
|
168
|
+
rubygems_version: 3.6.2
|
169
169
|
specification_version: 4
|
170
170
|
summary: Validate API implementations against OpenAPI documents.
|
171
171
|
test_files: []
|