skooma 0.3.3 → 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 +19 -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/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,22 @@ 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
|
+
|
10
26
|
## [0.3.3] - 2024-10-14
|
11
27
|
|
12
28
|
### Fixed
|
@@ -125,10 +141,12 @@ and this project adheres to [Semantic Versioning].
|
|
125
141
|
- Initial implementation. ([@skryukov])
|
126
142
|
|
127
143
|
[@barnaclebarnes]: https://github.com/barnaclebarnes
|
144
|
+
[@pvcarrera]: https://github.com/pvcarrera
|
128
145
|
[@skryukov]: https://github.com/skryukov
|
129
146
|
[@ursm]: https://github.com/ursm
|
130
147
|
|
131
|
-
[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
|
132
150
|
[0.3.3]: https://github.com/skryukov/skooma/compare/v0.3.2...v0.3.3
|
133
151
|
[0.3.2]: https://github.com/skryukov/skooma/compare/v0.3.1...v0.3.2
|
134
152
|
[0.3.1]: https://github.com/skryukov/skooma/compare/v0.3.0...v0.3.1
|
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
|
|
@@ -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: []
|