skooma 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
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: []