rspec-openapi 0.10.0 → 0.12.0

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: 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