skooma 0.3.3 → 0.3.5
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 +40 -1
- data/README.md +7 -0
- data/lib/skooma/body_parsers.rb +24 -2
- data/lib/skooma/coverage.rb +20 -5
- data/lib/skooma/coverage_store.rb +69 -0
- 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 +8 -2
- data/lib/skooma/minitest.rb +3 -1
- data/lib/skooma/objects/openapi/keywords/paths.rb +76 -10
- data/lib/skooma/objects/openapi.rb +20 -0
- data/lib/skooma/version.rb +1 -1
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ecc89f4b7b9744168f5847936e80a6e43d1d9ab32e31df584adaf5c85eaa27af
|
4
|
+
data.tar.gz: bc186b5d374ea4fd8703b8bb58c1dc921760e6731de266e7914035868c921b47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45660ab6ee53e2be9822ee9fccda2850719f10d230a120e4cf1b72855fb88e0cf263f72612e8648b0dccced1cfc08e5e08a4cc462c7f7f8b50fe5122a42a49a3
|
7
|
+
data.tar.gz: 798b59438bdd1ccf3cea2633372f581118dc979fc6c0347b5c0c98b070c4b8a3e765343606cdd756b4b3133d212cf3e2bc06a06bee23273f9a4f4687f1146f5e
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning].
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.3.5] - 2025-07-31
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
|
14
|
+
- Fix the Enforce Access mode with additional properties. ([@aburgel])
|
15
|
+
- Introduce coverage storage to fix Minitest parallel workers reports. ([@skarlcf])
|
16
|
+
- Introduce `use_patterns_for_path_matching` option to allow using `path` patterns for path matching. ([@jandouwebeekman])
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# spec/rails_helper.rb
|
20
|
+
|
21
|
+
RSpec.configure do |config|
|
22
|
+
# To enable path patterns, pass `use_patterns_for_path_matching: true` option:
|
23
|
+
config.include Skooma::RSpec[Rails.root.join("docs", "openapi.yml"), use_patterns_for_path_matching: true], type: :request
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
## [0.3.4] - 2025-01-14
|
28
|
+
|
29
|
+
### Added
|
30
|
+
|
31
|
+
- Experimental support for `readOnly` and `writeOnly` keywords. ([@skryukov])
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# spec/rails_helper.rb
|
35
|
+
|
36
|
+
RSpec.configure do |config|
|
37
|
+
# To enable support for readOnly and writeOnly keywords, pass `enforce_access_modes: true` option:
|
38
|
+
config.include Skooma::RSpec[Rails.root.join("docs", "openapi.yml"), enforce_access_modes: true], type: :request
|
39
|
+
end
|
40
|
+
```
|
41
|
+
- Support fallback parsers for vendor-specific media types. ([@pvcarrera], [@skryukov])
|
42
|
+
|
10
43
|
## [0.3.3] - 2024-10-14
|
11
44
|
|
12
45
|
### Fixed
|
@@ -124,11 +157,17 @@ and this project adheres to [Semantic Versioning].
|
|
124
157
|
|
125
158
|
- Initial implementation. ([@skryukov])
|
126
159
|
|
160
|
+
[@aburgel]: https://github.com/aburgel
|
127
161
|
[@barnaclebarnes]: https://github.com/barnaclebarnes
|
162
|
+
[@jandouwebeekman]: https://github.com/jandouwebeekman
|
163
|
+
[@pvcarrera]: https://github.com/pvcarrera
|
164
|
+
[@skarlcf]: https://github.com/skarlcf
|
128
165
|
[@skryukov]: https://github.com/skryukov
|
129
166
|
[@ursm]: https://github.com/ursm
|
130
167
|
|
131
|
-
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.3.
|
168
|
+
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.3.5...HEAD
|
169
|
+
[0.3.5]: https://github.com/skryukov/skooma/compare/v0.3.4...v0.3.5
|
170
|
+
[0.3.4]: https://github.com/skryukov/skooma/compare/v0.3.3...v0.3.4
|
132
171
|
[0.3.3]: https://github.com/skryukov/skooma/compare/v0.3.2...v0.3.3
|
133
172
|
[0.3.2]: https://github.com/skryukov/skooma/compare/v0.3.1...v0.3.2
|
134
173
|
[0.3.1]: https://github.com/skryukov/skooma/compare/v0.3.0...v0.3.1
|
data/README.md
CHANGED
@@ -128,6 +128,13 @@ 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
|
135
|
+
|
136
|
+
# To enable custom regex patterns for path parameters, pass `use_patterns_for_path_matching: true` option.
|
137
|
+
ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi, use_patterns_for_path_matching: true], type: :request
|
131
138
|
```
|
132
139
|
|
133
140
|
#### 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
|
|
data/lib/skooma/coverage.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Skooma
|
2
4
|
class NoopCoverage
|
3
5
|
def track_request(*)
|
@@ -24,7 +26,7 @@ module Skooma
|
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
|
-
def self.new(schema, mode: nil, format: nil)
|
29
|
+
def self.new(schema, mode: nil, format: nil, storage: nil)
|
28
30
|
case mode
|
29
31
|
when nil, false
|
30
32
|
NoopCoverage.new
|
@@ -35,14 +37,23 @@ module Skooma
|
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
38
|
-
attr_reader :mode, :format, :defined_paths, :
|
40
|
+
attr_reader :mode, :format, :defined_paths, :storage, :schema
|
41
|
+
attr_accessor :covered_paths
|
39
42
|
|
40
|
-
def initialize(schema, mode:, format:)
|
43
|
+
def initialize(schema, mode:, format:, storage:)
|
41
44
|
@schema = schema
|
42
45
|
@mode = mode
|
43
46
|
@format = format || SimpleReport
|
44
|
-
@
|
45
|
-
|
47
|
+
@storage = storage || CoverageStore.new
|
48
|
+
|
49
|
+
stored_data = @storage.load_data
|
50
|
+
@defined_paths = stored_data[:defined_paths]
|
51
|
+
@covered_paths = stored_data[:covered_paths]
|
52
|
+
|
53
|
+
if @defined_paths.empty?
|
54
|
+
@defined_paths = find_defined_paths(schema)
|
55
|
+
@storage.save_data(@defined_paths, @covered_paths)
|
56
|
+
end
|
46
57
|
end
|
47
58
|
|
48
59
|
def track_request(result)
|
@@ -57,6 +68,7 @@ module Skooma
|
|
57
68
|
end
|
58
69
|
end
|
59
70
|
covered_paths << operation
|
71
|
+
storage.save_data(Set.new, Set.new([operation]))
|
60
72
|
end
|
61
73
|
|
62
74
|
def uncovered_paths
|
@@ -68,7 +80,10 @@ module Skooma
|
|
68
80
|
end
|
69
81
|
|
70
82
|
def report
|
83
|
+
stored_data = storage.load_data
|
84
|
+
self.covered_paths = stored_data[:covered_paths]
|
71
85
|
format.new(self).report
|
86
|
+
storage.clear
|
72
87
|
exit 1 if mode == :strict && uncovered_paths.any?
|
73
88
|
end
|
74
89
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
class CoverageStore
|
5
|
+
DEFAULT_FILE_PATH = File.join(Dir.pwd, "tmp", "skooma_coverage.json")
|
6
|
+
|
7
|
+
attr_reader :file_path
|
8
|
+
|
9
|
+
def initialize(file_path: DEFAULT_FILE_PATH)
|
10
|
+
@file_path = file_path
|
11
|
+
ensure_file_exists
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_data
|
15
|
+
with_lock("r") do |file|
|
16
|
+
parse_data(file.read)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def save_data(new_defined_paths, new_covered_paths)
|
21
|
+
with_lock("r+") do |file|
|
22
|
+
existing_data = parse_data(file.read)
|
23
|
+
merged_data = merge_data(existing_data, new_defined_paths, new_covered_paths)
|
24
|
+
|
25
|
+
file.rewind
|
26
|
+
file.write(JSON.generate(merged_data))
|
27
|
+
file.flush
|
28
|
+
file.truncate(file.pos)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
File.delete(file_path) if File.exist?(file_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def ensure_file_exists
|
39
|
+
FileUtils.mkdir_p(File.dirname(@file_path))
|
40
|
+
FileUtils.touch(@file_path) unless File.exist?(@file_path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_data(content)
|
44
|
+
return {defined_paths: Set.new, covered_paths: Set.new} if content.strip.empty?
|
45
|
+
|
46
|
+
data = JSON.parse(content, symbolize_names: true)
|
47
|
+
{
|
48
|
+
defined_paths: Set.new(data[:defined_paths]),
|
49
|
+
covered_paths: Set.new(data[:covered_paths])
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def merge_data(existing_data, new_defined_paths, new_covered_paths)
|
54
|
+
{
|
55
|
+
defined_paths: (existing_data[:defined_paths] | new_defined_paths).to_a,
|
56
|
+
covered_paths: (existing_data[:covered_paths] | new_covered_paths).to_a
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def with_lock(mode)
|
61
|
+
File.open(@file_path, mode) do |file|
|
62
|
+
file.flock(File::LOCK_EX)
|
63
|
+
yield(file)
|
64
|
+
ensure
|
65
|
+
file.flock(File::LOCK_UN)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -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 res && 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, use_patterns_for_path_matching: false, **params)
|
48
48
|
super()
|
49
49
|
|
50
50
|
registry = create_test_registry
|
@@ -57,8 +57,14 @@ 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
|
+
storage = Skooma::CoverageStore.new(
|
63
|
+
file_path: File.join(Dir.pwd, "tmp", "skooma_coverage_#{Digest::SHA256.hexdigest(source_uri)[0..8]}.json")
|
64
|
+
)
|
65
|
+
@coverage = Coverage.new(@schema, mode: params[:coverage], format: params[:coverage_format], storage: storage)
|
66
|
+
|
67
|
+
@schema.use_patterns_for_path_matching = use_patterns_for_path_matching
|
62
68
|
|
63
69
|
include DefaultHelperMethods
|
64
70
|
include helper_methods_module
|
data/lib/skooma/minitest.rb
CHANGED
@@ -13,14 +13,6 @@ module Skooma
|
|
13
13
|
|
14
14
|
def initialize(parent_schema, value)
|
15
15
|
super
|
16
|
-
@regexp_map = json.filter_map do |path, subschema|
|
17
|
-
next unless path.include?("{") && path.include?("}")
|
18
|
-
|
19
|
-
path_regex = path.gsub(ROUTE_REGEXP, "(?<\\1>[^/?#]+)")
|
20
|
-
path_regex = Regexp.new("\\A#{path_regex}\\z")
|
21
|
-
|
22
|
-
[path, path_regex, subschema]
|
23
|
-
end
|
24
16
|
end
|
25
17
|
|
26
18
|
def evaluate(instance, result)
|
@@ -44,11 +36,34 @@ module Skooma
|
|
44
36
|
|
45
37
|
private
|
46
38
|
|
39
|
+
def regexp_map
|
40
|
+
@regexp_map ||= json.filter_map do |path, subschema|
|
41
|
+
next unless path.include?("{") && path.include?("}")
|
42
|
+
|
43
|
+
pattern_hash = if json.root.use_patterns_for_path_matching?
|
44
|
+
create_hash_of_patterns(subschema)
|
45
|
+
else
|
46
|
+
{}
|
47
|
+
end
|
48
|
+
|
49
|
+
path_regex = path.gsub(ROUTE_REGEXP) do |match|
|
50
|
+
param = match[1..-2]
|
51
|
+
if pattern_hash.key?(param)
|
52
|
+
"(?<#{param}>#{pattern_hash[param]})"
|
53
|
+
else
|
54
|
+
"(?<#{param}>[^/?#]+)"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
path_regex = Regexp.new("\\A#{path_regex}\\z")
|
58
|
+
|
59
|
+
[path, path_regex, subschema]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
47
63
|
def find_route(instance_path)
|
48
64
|
instance_path = instance_path.delete_prefix(json.root.path_prefix)
|
49
65
|
return [instance_path, {}, json[instance_path]] if json.key?(instance_path)
|
50
|
-
|
51
|
-
@regexp_map.reduce(nil) do |result, (path, path_regex, subschema)|
|
66
|
+
regexp_map.reduce(nil) do |result, (path, path_regex, subschema)|
|
52
67
|
next result unless path.include?("{") && path.include?("}")
|
53
68
|
|
54
69
|
match = instance_path.match(path_regex)
|
@@ -58,6 +73,57 @@ module Skooma
|
|
58
73
|
[path, match.named_captures, subschema]
|
59
74
|
end
|
60
75
|
end
|
76
|
+
|
77
|
+
def get_child(parent, child_name)
|
78
|
+
if parent
|
79
|
+
parent_to_use = if parent.key?("$ref")
|
80
|
+
parent.resolve_ref(parent["$ref"])
|
81
|
+
else
|
82
|
+
parent
|
83
|
+
end
|
84
|
+
if parent_to_use.key?(child_name)
|
85
|
+
parent_to_use[child_name]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_hash_of_patterns(subschema)
|
91
|
+
output = {}
|
92
|
+
parameters = []
|
93
|
+
parameters = parameters.concat(subschema["parameters"]) if subschema["parameters"]
|
94
|
+
%w[get post put patch delete].each do |method|
|
95
|
+
parameters = parameters.concat(subschema[method]["parameters"]) if subschema[method] && subschema[method]["parameters"]
|
96
|
+
end
|
97
|
+
parameters.each do |parameter|
|
98
|
+
if get_child(parameter, "in") == "path"
|
99
|
+
pattern = "[^/?#]+"
|
100
|
+
new_pattern = get_child(parameter, "pattern")
|
101
|
+
pattern = new_pattern if new_pattern
|
102
|
+
new_pattern = get_child(get_child(parameter, "schema"), "pattern")
|
103
|
+
pattern = new_pattern if new_pattern
|
104
|
+
|
105
|
+
output[get_child(parameter, "name").to_s] = filter_pattern(pattern)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
output
|
109
|
+
end
|
110
|
+
|
111
|
+
def filter_pattern(pattern)
|
112
|
+
to_return = pattern.to_s
|
113
|
+
if to_return.start_with?("^")
|
114
|
+
to_return = to_return[1..]
|
115
|
+
end
|
116
|
+
if to_return.start_with?('\A')
|
117
|
+
to_return = to_return[2..]
|
118
|
+
end
|
119
|
+
if to_return.end_with?("$")
|
120
|
+
to_return = to_return[0..-2]
|
121
|
+
end
|
122
|
+
if to_return.end_with?('\Z', '\z')
|
123
|
+
to_return = to_return[0..-3]
|
124
|
+
end
|
125
|
+
to_return
|
126
|
+
end
|
61
127
|
end
|
62
128
|
end
|
63
129
|
end
|
@@ -39,6 +39,26 @@ 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
|
+
|
52
|
+
def use_patterns_for_path_matching=(value)
|
53
|
+
raise ArgumentError, "Use patterns for path matching must be a boolean" unless [true, false].include?(value)
|
54
|
+
|
55
|
+
@use_patterns_for_path_matching = value
|
56
|
+
end
|
57
|
+
|
58
|
+
def use_patterns_for_path_matching?
|
59
|
+
@use_patterns_for_path_matching
|
60
|
+
end
|
61
|
+
|
42
62
|
def path_prefix
|
43
63
|
@path_prefix || ""
|
44
64
|
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.5
|
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: 1980-01-02 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
|
@@ -55,16 +54,20 @@ files:
|
|
55
54
|
- lib/skooma.rb
|
56
55
|
- lib/skooma/body_parsers.rb
|
57
56
|
- lib/skooma/coverage.rb
|
57
|
+
- lib/skooma/coverage_store.rb
|
58
58
|
- lib/skooma/dialects/oas_3_1.rb
|
59
59
|
- lib/skooma/env_mapper.rb
|
60
60
|
- lib/skooma/inflector.rb
|
61
61
|
- lib/skooma/instance.rb
|
62
62
|
- lib/skooma/keywords/oas_3_1.rb
|
63
|
+
- lib/skooma/keywords/oas_3_1/dialect/additional_properties.rb
|
63
64
|
- lib/skooma/keywords/oas_3_1/dialect/any_of.rb
|
64
65
|
- lib/skooma/keywords/oas_3_1/dialect/discriminator.rb
|
65
66
|
- lib/skooma/keywords/oas_3_1/dialect/example.rb
|
66
67
|
- lib/skooma/keywords/oas_3_1/dialect/external_docs.rb
|
67
68
|
- lib/skooma/keywords/oas_3_1/dialect/one_of.rb
|
69
|
+
- lib/skooma/keywords/oas_3_1/dialect/properties.rb
|
70
|
+
- lib/skooma/keywords/oas_3_1/dialect/required.rb
|
68
71
|
- lib/skooma/keywords/oas_3_1/dialect/xml.rb
|
69
72
|
- lib/skooma/keywords/oas_3_1/schema.rb
|
70
73
|
- lib/skooma/matchers/be_valid_document.rb
|
@@ -149,7 +152,6 @@ metadata:
|
|
149
152
|
homepage_uri: https://github.com/skryukov/skooma
|
150
153
|
source_code_uri: https://github.com/skryukov/skooma
|
151
154
|
rubygems_mfa_required: 'true'
|
152
|
-
post_install_message:
|
153
155
|
rdoc_options: []
|
154
156
|
require_paths:
|
155
157
|
- lib
|
@@ -164,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
166
|
- !ruby/object:Gem::Version
|
165
167
|
version: '0'
|
166
168
|
requirements: []
|
167
|
-
rubygems_version: 3.
|
168
|
-
signing_key:
|
169
|
+
rubygems_version: 3.6.7
|
169
170
|
specification_version: 4
|
170
171
|
summary: Validate API implementations against OpenAPI documents.
|
171
172
|
test_files: []
|