skooma 0.3.4 → 0.3.6
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 +35 -1
- data/README.md +3 -0
- data/data/oas-3.1/dialect/2024-11-10.json +93 -0
- data/data/oas-3.1/meta/2024-11-10.json +92 -0
- data/data/oas-3.1/schema/2025-02-13.json +1408 -0
- data/data/oas-3.1/schema-base/2025-02-13.json +25 -0
- data/lib/skooma/coverage.rb +20 -5
- data/lib/skooma/coverage_store.rb +69 -0
- data/lib/skooma/keywords/oas_3_1/dialect/additional_properties.rb +1 -1
- data/lib/skooma/matchers/wrapper.rb +7 -2
- data/lib/skooma/minitest.rb +3 -1
- data/lib/skooma/objects/components.rb +2 -2
- data/lib/skooma/objects/openapi/keywords/openapi.rb +6 -1
- data/lib/skooma/objects/openapi/keywords/paths.rb +76 -10
- data/lib/skooma/objects/openapi.rb +10 -0
- data/lib/skooma/version.rb +1 -1
- metadata +8 -3
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"$id": "https://spec.openapis.org/oas/3.1/schema-base/2025-02-13",
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
4
|
+
"description": "The description of OpenAPI v3.1.x Documents using the OpenAPI JSON Schema dialect",
|
5
|
+
"$ref": "https://spec.openapis.org/oas/3.1/schema/2025-02-13",
|
6
|
+
"properties": {
|
7
|
+
"jsonSchemaDialect": {
|
8
|
+
"$ref": "#/$defs/dialect"
|
9
|
+
}
|
10
|
+
},
|
11
|
+
"$defs": {
|
12
|
+
"dialect": {
|
13
|
+
"const": "https://spec.openapis.org/oas/3.1/dialect/2024-11-10"
|
14
|
+
},
|
15
|
+
"schema": {
|
16
|
+
"$dynamicAnchor": "meta",
|
17
|
+
"$ref": "https://spec.openapis.org/oas/3.1/dialect/2024-11-10",
|
18
|
+
"properties": {
|
19
|
+
"$schema": {
|
20
|
+
"$ref": "#/$defs/dialect"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
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
|
@@ -22,7 +22,7 @@ module Skooma
|
|
22
22
|
properties_result = result.sibling(instance, "properties")
|
23
23
|
instance.each_key do |name|
|
24
24
|
res = properties_result&.children&.[](instance[name]&.path)&.[]name
|
25
|
-
forbidden << name.tap { puts "adding #{name}" } if annotation_exists?(res, key: only_key)
|
25
|
+
forbidden << name.tap { puts "adding #{name}" } if res && annotation_exists?(res, key: only_key)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -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: "", enforce_access_modes: false, **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
|
@@ -59,7 +59,12 @@ module Skooma
|
|
59
59
|
@schema.path_prefix = path_prefix
|
60
60
|
@schema.enforce_access_modes = enforce_access_modes
|
61
61
|
|
62
|
-
|
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
|
63
68
|
|
64
69
|
include DefaultHelperMethods
|
65
70
|
include helper_methods_module
|
data/lib/skooma/minitest.rb
CHANGED
@@ -9,12 +9,12 @@ module Skooma
|
|
9
9
|
"schemas" => JSONSkooma::JSONSchema,
|
10
10
|
"responses" => Response,
|
11
11
|
"parameters" => Parameter,
|
12
|
-
|
12
|
+
"examples" => JSONSkooma::JSONNode,
|
13
13
|
"requestBodies" => RequestBody,
|
14
14
|
"headers" => Header,
|
15
15
|
"securitySchemes" => JSONSkooma::JSONNode,
|
16
16
|
"links" => JSONSkooma::JSONNode,
|
17
|
-
|
17
|
+
"callbacks" => JSONSkooma::JSONNode,
|
18
18
|
"pathItems" => PathItem
|
19
19
|
}
|
20
20
|
|
@@ -12,7 +12,12 @@ module Skooma
|
|
12
12
|
raise Error, "Only OpenAPI version 3.1.x is supported, got #{value}"
|
13
13
|
end
|
14
14
|
|
15
|
-
parent_schema.metaschema_uri =
|
15
|
+
parent_schema.metaschema_uri = if value.to_s.start_with? "3.1.0"
|
16
|
+
"https://spec.openapis.org/oas/3.1/schema-base/2022-10-07"
|
17
|
+
else
|
18
|
+
"https://spec.openapis.org/oas/3.1/schema-base/2025-02-13"
|
19
|
+
end
|
20
|
+
|
16
21
|
parent_schema.json_schema_dialect_uri = "https://spec.openapis.org/oas/3.1/dialect/base"
|
17
22
|
|
18
23
|
super
|
@@ -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
|
@@ -49,6 +49,16 @@ module Skooma
|
|
49
49
|
@enforce_access_modes
|
50
50
|
end
|
51
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
|
+
|
52
62
|
def path_prefix
|
53
63
|
@path_prefix || ""
|
54
64
|
end
|
data/lib/skooma/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +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.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Svyatoslav Kryukov
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: zeitwerk
|
@@ -47,13 +47,18 @@ files:
|
|
47
47
|
- CHANGELOG.md
|
48
48
|
- LICENSE.txt
|
49
49
|
- README.md
|
50
|
+
- data/oas-3.1/dialect/2024-11-10.json
|
50
51
|
- data/oas-3.1/dialect/base.json
|
52
|
+
- data/oas-3.1/meta/2024-11-10.json
|
51
53
|
- data/oas-3.1/meta/base.json
|
52
54
|
- data/oas-3.1/schema-base/2022-10-07.json
|
55
|
+
- data/oas-3.1/schema-base/2025-02-13.json
|
53
56
|
- data/oas-3.1/schema/2022-10-07.json
|
57
|
+
- data/oas-3.1/schema/2025-02-13.json
|
54
58
|
- lib/skooma.rb
|
55
59
|
- lib/skooma/body_parsers.rb
|
56
60
|
- lib/skooma/coverage.rb
|
61
|
+
- lib/skooma/coverage_store.rb
|
57
62
|
- lib/skooma/dialects/oas_3_1.rb
|
58
63
|
- lib/skooma/env_mapper.rb
|
59
64
|
- lib/skooma/inflector.rb
|
@@ -165,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
170
|
- !ruby/object:Gem::Version
|
166
171
|
version: '0'
|
167
172
|
requirements: []
|
168
|
-
rubygems_version: 3.6.
|
173
|
+
rubygems_version: 3.6.9
|
169
174
|
specification_version: 4
|
170
175
|
summary: Validate API implementations against OpenAPI documents.
|
171
176
|
test_files: []
|