skooma 0.3.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44e41865174e5bfeb3bf254ed74bb980aaa5d6aea0ca937fbea38b36242ac2ca
4
- data.tar.gz: 0b692735b5e0cd2e079b6a7d804b20c53d5069f5e6ed700117fb825f341e71a9
3
+ metadata.gz: ecc89f4b7b9744168f5847936e80a6e43d1d9ab32e31df584adaf5c85eaa27af
4
+ data.tar.gz: bc186b5d374ea4fd8703b8bb58c1dc921760e6731de266e7914035868c921b47
5
5
  SHA512:
6
- metadata.gz: 2cbc3a15cf9640bb7bb5614d3b1610db5742218612168f873b74fc9343c1a20855e7a7809bdc3f0da7797071fedf5651057bd7a98fa0b29834e85eed4c453295
7
- data.tar.gz: 24717dabd2a57266162aa5827075e7c5c97d8d387faf782549887f1c0fc5383b4109679283f62320f45b01e8a6e6860bab66214e4e30224096ee1267b3b0dc3b
6
+ metadata.gz: 45660ab6ee53e2be9822ee9fccda2850719f10d230a120e4cf1b72855fb88e0cf263f72612e8648b0dccced1cfc08e5e08a4cc462c7f7f8b50fe5122a42a49a3
7
+ data.tar.gz: 798b59438bdd1ccf3cea2633372f581118dc979fc6c0347b5c0c98b070c4b8a3e765343606cdd756b4b3133d212cf3e2bc06a06bee23273f9a4f4687f1146f5e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ 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
+
10
27
  ## [0.3.4] - 2025-01-14
11
28
 
12
29
  ### Added
@@ -140,12 +157,16 @@ and this project adheres to [Semantic Versioning].
140
157
 
141
158
  - Initial implementation. ([@skryukov])
142
159
 
160
+ [@aburgel]: https://github.com/aburgel
143
161
  [@barnaclebarnes]: https://github.com/barnaclebarnes
162
+ [@jandouwebeekman]: https://github.com/jandouwebeekman
144
163
  [@pvcarrera]: https://github.com/pvcarrera
164
+ [@skarlcf]: https://github.com/skarlcf
145
165
  [@skryukov]: https://github.com/skryukov
146
166
  [@ursm]: https://github.com/ursm
147
167
 
148
- [Unreleased]: https://github.com/skryukov/skooma/compare/v0.3.4...HEAD
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
149
170
  [0.3.4]: https://github.com/skryukov/skooma/compare/v0.3.3...v0.3.4
150
171
  [0.3.3]: https://github.com/skryukov/skooma/compare/v0.3.2...v0.3.3
151
172
  [0.3.2]: https://github.com/skryukov/skooma/compare/v0.3.1...v0.3.2
data/README.md CHANGED
@@ -132,6 +132,9 @@ ActionDispatch::IntegrationTest.include Skooma::Minitest[path_to_openapi, covera
132
132
  # EXPERIMENTAL
133
133
  # To enable support for readOnly and writeOnly keywords, pass `enforce_access_modes: true` option:
134
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
135
138
  ```
136
139
 
137
140
  #### Validate OpenAPI document
@@ -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, :covered_paths, :schema
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
- @defined_paths = find_defined_paths(schema)
45
- @covered_paths = Set.new
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
- @coverage = Coverage.new(@schema, mode: params[:coverage], format: params[:coverage_format])
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
@@ -39,7 +39,9 @@ module Skooma
39
39
  def initialize(openapi_path, **params)
40
40
  super(HelperMethods, openapi_path, **params)
41
41
 
42
- ::Minitest.after_run { coverage.report }
42
+ ::Minitest.after_run do
43
+ coverage.report
44
+ end
43
45
  end
44
46
  end
45
47
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skooma
4
- VERSION = "0.3.4"
4
+ VERSION = "0.3.5"
5
5
  end
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
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk
@@ -54,6 +54,7 @@ files:
54
54
  - lib/skooma.rb
55
55
  - lib/skooma/body_parsers.rb
56
56
  - lib/skooma/coverage.rb
57
+ - lib/skooma/coverage_store.rb
57
58
  - lib/skooma/dialects/oas_3_1.rb
58
59
  - lib/skooma/env_mapper.rb
59
60
  - lib/skooma/inflector.rb
@@ -165,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
165
166
  - !ruby/object:Gem::Version
166
167
  version: '0'
167
168
  requirements: []
168
- rubygems_version: 3.6.2
169
+ rubygems_version: 3.6.7
169
170
  specification_version: 4
170
171
  summary: Validate API implementations against OpenAPI documents.
171
172
  test_files: []