rspec-openapi 0.10.0 → 0.12.0

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: 9749f7a6121a78737336bd436f5fda8b20381a82f4cb507f42667bf3d38a2a4b
4
- data.tar.gz: ff0bc4a559323f1285d282ab786b5666c950b8ad0b9585fa749cf1f2b0f3613e
3
+ metadata.gz: b221d36e6ff92306e1bd720b4bb07e114839c8e047ad530f6ac4cb2a11eb4ae5
4
+ data.tar.gz: 5f5daf56dddc8a7b9477f961611d3bd4550eb498aa38d166035e5b18fe7027bd
5
5
  SHA512:
6
- metadata.gz: 4e412c58158ffaa1518640e3082ef04c41d50ec43de94ef032c866ebbec2468775d4ea3b2c976294f14ce6b8570b3e86df7248f17877c491efa9fc31e5a8819a
7
- data.tar.gz: 65d7308f7d552d9ac299a6d3330933283c237345b477de260a2e2af24210268f2e41350619cec83f145f028a7374b197281cbd9752d1cb2aa3baa696f8240739
6
+ metadata.gz: 18410768ff613a19b39f910e20e58ecda87337095b354e718bdb5f6b6e2713429acd26a289fd4ffa89a493348aa226561d89340d9685d37caa30b44c54d17be8
7
+ data.tar.gz: 5542bf85a5f1d46eacbdc0936b57ae197279bf618fa1628082e7d33c5600a2583f450426999dbc6eea4d4a401ffb307ea5d207b551aa27d586eb4682e572c9fb
@@ -28,12 +28,12 @@ jobs:
28
28
  uses: actions/checkout@v4
29
29
 
30
30
  - name: Initialize CodeQL
31
- uses: github/codeql-action/init@v2
31
+ uses: github/codeql-action/init@v3
32
32
  with:
33
33
  languages: ${{ matrix.language }}
34
34
 
35
35
  - name: Autobuild
36
- uses: github/codeql-action/autobuild@v2
36
+ uses: github/codeql-action/autobuild@v3
37
37
 
38
38
  - name: Perform CodeQL Analysis
39
- uses: github/codeql-action/analyze@v2
39
+ uses: github/codeql-action/analyze@v3
@@ -30,6 +30,6 @@ jobs:
30
30
  "
31
31
 
32
32
  - name: Upload Sarif output
33
- uses: github/codeql-action/upload-sarif@v2
33
+ uses: github/codeql-action/upload-sarif@v3
34
34
  with:
35
35
  sarif_file: rubocop.sarif
@@ -26,6 +26,8 @@ jobs:
26
26
  rails: 6.1.6
27
27
  - ruby: ruby:3.1
28
28
  rails: 7.0.3
29
+ - ruby: ruby:3.3
30
+ rails: 7.1.2
29
31
  coverage: coverage
30
32
  env:
31
33
  RAILS_VERSION: ${{ matrix.rails == '' && '6.1.6' || matrix.rails }}
@@ -41,9 +43,12 @@ jobs:
41
43
  if: matrix.coverage == 'coverage'
42
44
  - run: bundle exec rspec
43
45
  timeout-minutes: 1
46
+ - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
47
+ name: codecov-action@v4 workaround
44
48
  - name: Upload coverage reports
45
- uses: codecov/codecov-action@v3
49
+ uses: codecov/codecov-action@v4
46
50
  if: matrix.coverage == 'coverage'
47
51
  with:
48
52
  fail_ci_if_error: true
49
53
  files: ./coverage/coverage.xml
54
+ token: ${{ secrets.CODECOV_TOKEN }}
data/.rubocop_todo.yml CHANGED
@@ -1,25 +1,35 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2023-04-14 06:22:29 UTC using RuboCop version 1.49.0.
3
+ # on 2024-01-13 11:12:43 UTC using RuboCop version 1.50.2.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 10
9
+ # Offense count: 9
10
10
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
11
11
  Metrics/AbcSize:
12
- Max: 31
12
+ Max: 48
13
+
14
+ # Offense count: 2
15
+ # Configuration parameters: CountComments, CountAsOne.
16
+ Metrics/ClassLength:
17
+ Max: 192
13
18
 
14
- # Offense count: 4
19
+ # Offense count: 5
15
20
  # Configuration parameters: AllowedMethods, AllowedPatterns.
16
21
  Metrics/CyclomaticComplexity:
17
- Max: 10
22
+ Max: 13
18
23
 
19
- # Offense count: 13
24
+ # Offense count: 16
20
25
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
21
26
  Metrics/MethodLength:
22
- Max: 30
27
+ Max: 31
28
+
29
+ # Offense count: 1
30
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
31
+ Metrics/PerceivedComplexity:
32
+ Max: 13
23
33
 
24
34
  # Offense count: 1
25
35
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
@@ -29,7 +39,7 @@ Naming/VariableNumber:
29
39
  Exclude:
30
40
  - 'spec/integration_tests/roda_test.rb'
31
41
 
32
- # Offense count: 5
42
+ # Offense count: 6
33
43
  # Configuration parameters: AllowedConstants.
34
44
  Style/Documentation:
35
45
  Exclude:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## v0.12.0
2
+
3
+ - feat: Initial support of complex schema with manually-added `oneOf`
4
+ [#174](https://github.com/exoego/rspec-openapi/pull/174)
5
+ - chore: Test with Ruby 3.3 and Rails 7.1.x
6
+ [#169](https://github.com/exoego/rspec-openapi/pull/169)
7
+
8
+ ## v0.11.0
9
+ - feat: Allow path-based config overrides
10
+ [#162](https://github.com/exoego/rspec-openapi/pull/162)
11
+ - enhancement: Sort HTTP methods, response status codes, and contents lexicographically
12
+ [#163](https://github.com/exoego/rspec-openapi/pull/163)
13
+ - enhancement: Remove parameters that conflict with security schemas
14
+ [#166](https://github.com/exoego/rspec-openapi/pull/166)
15
+
1
16
  ## v0.10.0
2
17
  - bugfix: Merge parameter data to preserve description in manually edited Openapi spec
3
18
  [#149](https://github.com/exoego/rspec-openapi/pull/149)
@@ -23,7 +23,8 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
23
23
  # 0 1 2 ^...............................^
24
24
  # ["components", "schema", "Table", "properties", "owner", "properties", "company", "$ref"]
25
25
  # 0 1 2 ^...........................................^
26
- needle = paths.slice(2, paths.size - 3)
26
+ needle = paths.reject { |path| path.is_a?(Integer) || path == 'oneOf' }
27
+ needle = needle.slice(2, needle.size - 3)
27
28
  nested_schema = fresh_schemas.dig(*needle)
28
29
 
29
30
  # Skip if the property using $ref is not found in the parent schema. The property may be removed.
@@ -44,20 +45,28 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
44
45
  references.inject({}) do |acc, paths|
45
46
  ref_link = dig_schema(base, paths)['$ref']
46
47
  schema_name = ref_link.gsub('#/components/schemas/', '')
47
- schema_body = dig_schema(fresh, paths)
48
+ schema_body = dig_schema(fresh, paths.reject { |path| path.is_a?(Integer) })
49
+
48
50
  RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
49
51
  end
50
52
  end
51
53
 
52
54
  def dig_schema(obj, paths)
53
- obj.dig(*paths, 'schema', 'items') || obj.dig(*paths, 'schema')
55
+ item_schema = obj.dig(*paths, 'schema', 'items')
56
+ object_schema = obj.dig(*paths, 'schema')
57
+ one_of_schema = obj.dig(*paths.take(paths.size - 1), 'schema', 'oneOf', paths.last)
58
+
59
+ item_schema || object_schema || one_of_schema
54
60
  end
55
61
 
56
62
  def paths_to_top_level_refs(base)
57
63
  request_bodies = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.requestBody.content.application/json')
58
64
  responses = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.responses.*.content.application/json')
59
- (request_bodies + responses).select do |paths|
60
- dig_schema(base, paths)&.dig('$ref')&.start_with?('#/components/schemas/')
65
+ (request_bodies + responses).flat_map do |paths|
66
+ object_paths = find_object_refs(base, paths)
67
+ one_of_paths = find_one_of_refs(base, paths)
68
+
69
+ object_paths || one_of_paths || []
61
70
  end
62
71
  end
63
72
 
@@ -65,6 +74,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
65
74
  nested_refs = [
66
75
  *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.$ref'),
67
76
  *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref'),
77
+ *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'oneOf.*.$ref'),
68
78
  ]
69
79
  # Reject already-generated schemas to reduce unnecessary loop
70
80
  nested_refs.reject do |paths|
@@ -73,4 +83,14 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
73
83
  generated_names.include?(schema_name)
74
84
  end
75
85
  end
86
+
87
+ def find_one_of_refs(base, paths)
88
+ dig_schema(base, paths)&.dig('oneOf')&.map&.with_index do |schema, index|
89
+ paths + [index] if schema&.dig('$ref')&.start_with?('#/components/schemas/')
90
+ end&.compact
91
+ end
92
+
93
+ def find_object_refs(base, paths)
94
+ [paths] if dig_schema(base, paths)&.dig('$ref')&.start_with?('#/components/schemas/')
95
+ end
76
96
  end
@@ -8,6 +8,10 @@ class << RSpec::OpenAPI::HashHelper = Object.new
8
8
  k = k.to_s
9
9
  [[k]] + paths_to_all_fields(v).map { |x| [k, *x] }
10
10
  end
11
+ when Array
12
+ obj.flat_map.with_index do |value, i|
13
+ [[i]] + paths_to_all_fields(value).map { |x| [i, *x] }
14
+ end
11
15
  else
12
16
  []
13
17
  end
@@ -7,26 +7,33 @@ class RSpec::OpenAPI::ResultRecorder
7
7
  end
8
8
 
9
9
  def record_results!
10
- title = RSpec::OpenAPI.title
11
10
  @path_records.each do |path, records|
11
+ # Look for a path-specific config file and run it.
12
+ config_file = File.join(File.dirname(path), RSpec::OpenAPI.config_filename)
13
+ begin
14
+ require config_file if File.exist?(config_file)
15
+ rescue StandardError => e
16
+ puts "WARNING: Unable to load #{config_file}: #{e}"
17
+ end
18
+
19
+ title = RSpec::OpenAPI.title
12
20
  RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
13
21
  schema = RSpec::OpenAPI::DefaultSchema.build(title)
14
22
  schema[:info].merge!(RSpec::OpenAPI.info)
15
23
  RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
16
24
  new_from_zero = {}
17
25
  records.each do |record|
18
- begin
19
- record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
20
- RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
21
- RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
22
- rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
23
- @error_records[e] = record # Avoid failing the build
24
- end
26
+ record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
27
+ RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
28
+ RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
29
+ rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
30
+ @error_records[e] = record # Avoid failing the build
25
31
  end
32
+ RSpec::OpenAPI::SchemaCleaner.cleanup_conflicting_security_parameters!(spec)
26
33
  RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
27
34
  RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
28
35
  RSpec::OpenAPI::SchemaCleaner.cleanup_empty_required_array!(spec)
29
- RSpec::OpenAPI::SchemaCleaner.sort_paths!(spec)
36
+ RSpec::OpenAPI::SchemaSorter.deep_sort!(spec)
30
37
  end
31
38
  end
32
39
  end
@@ -39,6 +39,24 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
39
39
  base
40
40
  end
41
41
 
42
+ def cleanup_conflicting_security_parameters!(base)
43
+ security_schemes = base.dig('components', 'securitySchemes') || {}
44
+
45
+ return if security_schemes.empty?
46
+
47
+ paths_to_security_definitions = RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'paths', 'security')
48
+
49
+ paths_to_security_definitions.each do |path|
50
+ parent_path_definition = base.dig(*path.take(path.length - 1))
51
+
52
+ security_schemes.each do |security_scheme_name, security_scheme|
53
+ remove_parameters_conflicting_with_security_sceheme!(
54
+ parent_path_definition, security_scheme, security_scheme_name,
55
+ )
56
+ end
57
+ end
58
+ end
59
+
42
60
  def cleanup_empty_required_array!(base)
43
61
  paths_to_objects = [
44
62
  *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties'),
@@ -51,15 +69,20 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
51
69
  end
52
70
  end
53
71
 
54
- # Sort "paths" lexicographically to make the order more predictable
55
- #
56
- # @param [Hash] #
57
- def sort_paths!(spec)
58
- spec['paths'] = spec['paths']&.entries&.sort_by! { |path, _| path }.to_h
59
- end
60
-
61
72
  private
62
73
 
74
+ def remove_parameters_conflicting_with_security_sceheme!(path_definition, security_scheme, security_scheme_name)
75
+ return unless path_definition['security']
76
+ return unless path_definition['parameters']
77
+ return unless path_definition.dig('security', 0).keys.include?(security_scheme_name)
78
+
79
+ path_definition['parameters'].reject! do |parameter|
80
+ parameter['in'] == security_scheme['in'] && # same location (ie. header)
81
+ parameter['name'] == security_scheme['name'] # same name (ie. AUTHORIZATION)
82
+ end
83
+ path_definition.delete('parameters') if path_definition['parameters'].empty?
84
+ end
85
+
63
86
  def cleanup_array!(base, spec, selector, fields_for_identity = [])
64
87
  marshal = lambda do |obj|
65
88
  Marshal.dump(slice(obj, fields_for_identity))
@@ -29,6 +29,12 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
29
29
  #
30
30
  # TODO: Should we probably force-merge `summary` regardless of manual modifications?
31
31
  def merge_schema!(base, spec)
32
+ if (options = base['oneOf'])
33
+ merge_closest_match!(options, spec)
34
+
35
+ return base
36
+ end
37
+
32
38
  spec.each do |key, value|
33
39
  if base[key].is_a?(Hash) && value.is_a?(Hash)
34
40
  merge_schema!(base[key], value) unless base[key].key?('$ref')
@@ -67,4 +73,39 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
67
73
  all_parameters.uniq! { |param| param.slice('name', 'in') }
68
74
  base[key] = all_parameters
69
75
  end
76
+
77
+ SIMILARITY_THRESHOLD = 0.5
78
+
79
+ def merge_closest_match!(options, spec)
80
+ score, option = options.map { |option| [similarity(option, spec), option] }.max_by(&:first)
81
+
82
+ return if option&.key?('$ref')
83
+
84
+ if score.to_f > SIMILARITY_THRESHOLD
85
+ merge_schema!(option, spec)
86
+ else
87
+ options.push(spec)
88
+ end
89
+ end
90
+
91
+ def similarity(first, second)
92
+ return 1 if first == second
93
+
94
+ score =
95
+ case [first.class, second.class]
96
+ when [Array, Array]
97
+ (first & second).size / [first.size, second.size].max.to_f
98
+ when [Hash, Hash]
99
+ return 1 if first.merge(second).key?('$ref')
100
+
101
+ intersection = first.keys & second.keys
102
+ total_size = [first.size, second.size].max.to_f
103
+
104
+ intersection.sum { |key| similarity(first[key], second[key]) } / total_size
105
+ else
106
+ 0
107
+ end
108
+
109
+ score.finite? ? score : 0
110
+ end
70
111
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class << RSpec::OpenAPI::SchemaSorter = Object.new
4
+ # Sort some unpredictably ordered properties in a lexicographical manner to make the order more predictable.
5
+ #
6
+ # @param [Hash|Array]
7
+ def deep_sort!(spec)
8
+ # paths
9
+ deep_sort_by_selector!(spec, 'paths')
10
+
11
+ # methods
12
+ deep_sort_by_selector!(spec, 'paths.*')
13
+
14
+ # response status code
15
+ deep_sort_by_selector!(spec, 'paths.*.*.responses')
16
+
17
+ # content-type
18
+ deep_sort_by_selector!(spec, 'paths.*.*.responses.*.content')
19
+ end
20
+
21
+ private
22
+
23
+ # @param [Hash] base
24
+ # @param [String] selector
25
+ def deep_sort_by_selector!(base, selector)
26
+ RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
27
+ deep_sort_hash!(base.dig(*paths))
28
+ end
29
+ end
30
+
31
+ def deep_sort_hash!(hash)
32
+ sorted = hash.entries.sort_by { |k, _| k }.to_h
33
+ hash.replace(sorted)
34
+ end
35
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module OpenAPI
5
- VERSION = '0.10.0'
5
+ VERSION = '0.12.0'
6
6
  end
7
7
  end
data/lib/rspec/openapi.rb CHANGED
@@ -9,6 +9,7 @@ require 'rspec/openapi/schema_builder'
9
9
  require 'rspec/openapi/schema_file'
10
10
  require 'rspec/openapi/schema_merger'
11
11
  require 'rspec/openapi/schema_cleaner'
12
+ require 'rspec/openapi/schema_sorter'
12
13
 
13
14
  require 'rspec/openapi/minitest_hooks' if Object.const_defined?('Minitest')
14
15
  require 'rspec/openapi/rspec_hooks' if ENV['OPENAPI'] && Object.const_defined?('RSpec')
@@ -31,6 +32,9 @@ module RSpec::OpenAPI
31
32
  @path_records = Hash.new { |h, k| h[k] = [] }
32
33
  @ignored_path_params = %i[controller action format]
33
34
 
35
+ # This is the configuraion override file name we look for within each path.
36
+ @config_filename = 'rspec_openapi.rb'
37
+
34
38
  class << self
35
39
  attr_accessor :path,
36
40
  :title,
@@ -48,5 +52,7 @@ module RSpec::OpenAPI
48
52
  :response_headers,
49
53
  :path_records,
50
54
  :ignored_path_params
55
+
56
+ attr_reader :config_filename
51
57
  end
52
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-openapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-12-11 00:00:00.000000000 Z
12
+ date: 2024-02-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -76,6 +76,7 @@ files:
76
76
  - lib/rspec/openapi/schema_cleaner.rb
77
77
  - lib/rspec/openapi/schema_file.rb
78
78
  - lib/rspec/openapi/schema_merger.rb
79
+ - lib/rspec/openapi/schema_sorter.rb
79
80
  - lib/rspec/openapi/version.rb
80
81
  - rspec-openapi.gemspec
81
82
  - scripts/rspec