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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76912cb6b09166983cae2e1e03a104d16351116d2076e3c0b27e24491002a89b
4
- data.tar.gz: 65f728f3eda604da0eb7750f7f6cad612cd98870626c8fc766c1554336a6efe1
3
+ metadata.gz: 44e41865174e5bfeb3bf254ed74bb980aaa5d6aea0ca937fbea38b36242ac2ca
4
+ data.tar.gz: 0b692735b5e0cd2e079b6a7d804b20c53d5069f5e6ed700117fb825f341e71a9
5
5
  SHA512:
6
- metadata.gz: 2253d20d61746403cd726d47092ba45c7614a283a1af9becfea8fcf387bcd1c9f6f4d52b48efd6ebd7284df52ac5ac944958f6997011f9b8da0b6602d5c21f0d
7
- data.tar.gz: 9b220779384f848d308708a1ccd704314ca98d8a3ffc9683fe03962f10a997bc6465835e495638a83a2f955d19191a330e524050deef0345c858255421d2fd1c
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.3...HEAD
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
@@ -6,16 +6,38 @@ module Skooma
6
6
  DEFAULT_PARSER = ->(body, **_options) { body }
7
7
 
8
8
  def [](media_type)
9
- parsers[media_type.to_s.strip.downcase] || DEFAULT_PARSER
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.to_s.strip.downcase] = parser
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skooma
4
- VERSION = "0.3.3"
4
+ VERSION = "0.3.4"
5
5
  end
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.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: 2024-10-14 00:00:00.000000000 Z
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.0
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.0
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.5.17
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: []