rspec-openapi 0.25.1 → 0.27.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.
@@ -27,7 +27,7 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
27
27
  cleanup_hash!(base, spec, 'paths.*.*')
28
28
 
29
29
  # cleanup parameters
30
- cleanup_array!(base, spec, 'paths.*.*.parameters', %i[name in])
30
+ cleanup_array!(base, spec, 'paths.*.*.parameters', [:name, :in])
31
31
 
32
32
  # cleanup requestBody
33
33
  cleanup_hash!(base, spec, 'paths.*.*.requestBody.content.application/json.schema.properties.*')
@@ -86,6 +86,9 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
86
86
  hash.delete(:_example_key)
87
87
  hash.delete(:_example_summary)
88
88
  hash.delete(:_example_name)
89
+ if (fallback = hash.delete(:_fallback_description))
90
+ hash[:description] ||= fallback
91
+ end
89
92
 
90
93
  hash.each_value do |value|
91
94
  case value
@@ -27,12 +27,10 @@ class RSpec::OpenAPI::SchemaFile
27
27
  def read
28
28
  return {} unless File.exist?(@path)
29
29
 
30
- RSpec::OpenAPI::KeyTransformer.symbolize(
31
- YAML.safe_load(
32
- File.read(@path),
33
- permitted_classes: [Date, Time],
34
- ),
35
- ) # this can also parse JSON
30
+ content = YAML.safe_load(File.read(@path), permitted_classes: [Date, Time]) # this can also parse JSON
31
+ return {} if content.nil?
32
+
33
+ RSpec::OpenAPI::KeyTransformer.symbolize(content)
36
34
  end
37
35
 
38
36
  # @param [Hash] spec
@@ -25,28 +25,53 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
25
25
  return base
26
26
  end
27
27
 
28
- spec.each do |key, value|
29
- if base[key].is_a?(Hash) && value.is_a?(Hash)
30
- # Handle example/examples conflict - convert to examples when mixed
31
- normalize_example_fields!(base[key], value)
32
-
33
- # If the new value has oneOf, replace the entire value instead of merging
34
- if value.key?(:oneOf)
35
- base[key] = value
36
- else
37
- merge_schema!(base[key], value) unless base[key].key?(:$ref)
38
- end
39
- elsif base[key].is_a?(Array) && value.is_a?(Array)
40
- # parameters need to be merged as if `name` and `in` were the Hash keys.
41
- merge_arrays(base, key, value)
42
- else
43
- # do not ADD `properties` or `required` fields if `additionalProperties` field is present
44
- base[key] = value unless base.key?(:additionalProperties) && %i[properties required].include?(key)
45
- end
46
- end
28
+ prune_stale_object_fields!(base, spec)
29
+
30
+ spec.each { |key, value| merge_entry!(base, key, value) }
47
31
  base
48
32
  end
49
33
 
34
+ # When the new spec converts an object to a dictionary (introduces
35
+ # `additionalProperties` on a node that previously had `properties` /
36
+ # `required`), drop the stale fields so the merged result reflects the
37
+ # new intent. We only prune when base does not already declare
38
+ # `additionalProperties`, to preserve manual edits that intentionally
39
+ # combine fixed and dynamic keys.
40
+ def prune_stale_object_fields!(base, spec)
41
+ return unless spec.is_a?(Hash) && spec.key?(:additionalProperties) && !base.key?(:additionalProperties)
42
+
43
+ base.delete(:properties)
44
+ base.delete(:required)
45
+ end
46
+
47
+ def merge_entry!(base, key, value)
48
+ if base[key].is_a?(Hash) && value.is_a?(Hash)
49
+ merge_hash_entry!(base, key, value)
50
+ elsif base[key].is_a?(Array) && value.is_a?(Array)
51
+ # parameters need to be merged as if `name` and `in` were the Hash keys.
52
+ merge_arrays(base, key, value)
53
+ elsif !skip_due_to_additional_properties?(base, key)
54
+ base[key] = value
55
+ end
56
+ end
57
+
58
+ def merge_hash_entry!(base, key, value)
59
+ # Handle example/examples conflict - convert to examples when mixed
60
+ normalize_example_fields!(base[key], value)
61
+
62
+ # If the new value has oneOf, replace the entire value instead of merging
63
+ if value.key?(:oneOf)
64
+ base[key] = value
65
+ elsif !base[key].key?(:$ref)
66
+ merge_schema!(base[key], value)
67
+ end
68
+ end
69
+
70
+ # do not ADD `properties` or `required` fields if `additionalProperties` field is present
71
+ def skip_due_to_additional_properties?(base, key)
72
+ base.key?(:additionalProperties) && [:properties, :required].include?(key)
73
+ end
74
+
50
75
  def merge_arrays(base, key, value)
51
76
  base[key] = case key
52
77
  when :parameters
@@ -61,21 +86,33 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
61
86
  end
62
87
 
63
88
  def merge_parameters(base, key, value)
64
- all_parameters = value | base[key]
65
-
66
- unique_base_parameters = build_unique_params(base, key)
67
-
68
- all_parameters = all_parameters.map do |parameter|
69
- base_parameter = unique_base_parameters[[parameter[:name], parameter[:in]]] || {}
70
- if base_parameter.empty?
71
- parameter
89
+ base_params = index_parameters_by_identity(base[key])
90
+ new_params = index_parameters_by_identity(value)
91
+
92
+ base[key] = (base_params.keys | new_params.keys).map do |param_key|
93
+ base_param = base_params[param_key]
94
+ new_param = new_params[param_key]
95
+
96
+ if base_param && new_param
97
+ merge_parameter_with_schema(base_param, new_param)
98
+ elsif new_param
99
+ # Parameter only in the new spec. Treat as optional unless its `required: true`
100
+ # came from explicit `required_request_params` metadata — distinguishable only
101
+ # for `query`, where the schema_builder default is `required: false`. `header`
102
+ # defaults to `required: true`, so the value alone can't signal user intent.
103
+ new_param[:in] == 'query' && new_param[:required] ? new_param : mark_optional_unless_path(new_param)
72
104
  else
73
- merge_parameter_with_schema(base_parameter, parameter)
105
+ mark_optional_unless_path(base_param)
74
106
  end
75
107
  end
108
+ end
109
+
110
+ # OpenAPI requires `in: path` parameters to be `required: true`, so this leaves
111
+ # them untouched.
112
+ def mark_optional_unless_path(parameter)
113
+ return parameter if parameter[:in] == 'path'
76
114
 
77
- all_parameters.uniq! { |param| param.slice(:name, :in) }
78
- base[key] = all_parameters
115
+ parameter.merge(required: false)
79
116
  end
80
117
 
81
118
  def merge_parameter_with_schema(base_param, new_param)
@@ -83,12 +120,18 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
83
120
  new_schema = new_param[:schema]
84
121
 
85
122
  # If schemas have different types, create a oneOf
86
- if base_schema && new_schema && schemas_have_different_types?(base_schema, new_schema)
87
- merged_schema = merge_schemas_into_one_of(base_schema, new_schema)
88
- base_param.merge(new_param).merge(schema: merged_schema)
89
- else
90
- base_param.merge(new_param)
91
- end
123
+ merged = if base_schema && new_schema && schemas_have_different_types?(base_schema, new_schema)
124
+ merged_schema = merge_schemas_into_one_of(base_schema, new_schema)
125
+ base_param.merge(new_param).merge(schema: merged_schema)
126
+ else
127
+ base_param.merge(new_param)
128
+ end
129
+
130
+ # Once a parameter has been seen missing in any earlier test case, keep it optional
131
+ # even if later test cases mark it required again.
132
+ merged = mark_optional_unless_path(merged) if base_param[:required] == false || new_param[:required] == false
133
+
134
+ merged
92
135
  end
93
136
 
94
137
  def schemas_have_different_types?(schema1, schema2)
@@ -122,10 +165,8 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
122
165
  end
123
166
  end
124
167
 
125
- def build_unique_params(base, key)
126
- base[key].to_h do |parameter|
127
- [[parameter[:name], parameter[:in]], parameter]
128
- end
168
+ def index_parameters_by_identity(parameters)
169
+ parameters.to_h { |p| [[p[:name], p[:in]], p] }
129
170
  end
130
171
 
131
172
  # Normalize example/examples fields when there's a conflict
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module OpenAPI
5
- VERSION = '0.25.1'
5
+ VERSION = '0.27.0'
6
6
  end
7
7
  end
data/lib/rspec/openapi.rb CHANGED
@@ -43,10 +43,10 @@ module RSpec::OpenAPI
43
43
  @request_headers = []
44
44
  @servers = []
45
45
  @security_schemes = []
46
- @example_types = %i[request]
46
+ @example_types = [:request]
47
47
  @response_headers = []
48
48
  @path_records = Hash.new { |h, k| h[k] = [] }
49
- @ignored_path_params = %i[controller action format]
49
+ @ignored_path_params = [:controller, :action, :format]
50
50
  @ignored_paths = []
51
51
  @post_process_hook = nil
52
52
 
data/redocly.yaml ADDED
@@ -0,0 +1,31 @@
1
+ # Config for `redocly lint` used by .github/workflows/validate-openapi.yml
2
+ # to validate test-fixture OpenAPI documents shipped under spec/apps/.
3
+ #
4
+ # spec/apps/rails/doc/smart/openapi.yaml is intentionally excluded:
5
+ # it is the input fixture for the smart-merge feature test (it contains
6
+ # unresolved $refs by design), not a complete API description.
7
+ extends:
8
+ - minimal
9
+ apis:
10
+ rails-rspec-yaml:
11
+ root: spec/apps/rails/doc/rspec_openapi.yaml
12
+ rails-rspec-json:
13
+ root: spec/apps/rails/doc/rspec_openapi.json
14
+ rails-minitest-yaml:
15
+ root: spec/apps/rails/doc/minitest_openapi.yaml
16
+ rails-minitest-json:
17
+ root: spec/apps/rails/doc/minitest_openapi.json
18
+ rails-smart-expected:
19
+ root: spec/apps/rails/doc/smart/expected.yaml
20
+ roda-rspec-yaml:
21
+ root: spec/apps/roda/doc/rspec_openapi.yaml
22
+ roda-rspec-json:
23
+ root: spec/apps/roda/doc/rspec_openapi.json
24
+ roda-minitest-yaml:
25
+ root: spec/apps/roda/doc/minitest_openapi.yaml
26
+ roda-minitest-json:
27
+ root: spec/apps/roda/doc/minitest_openapi.json
28
+ hanami-yaml:
29
+ root: spec/apps/hanami/doc/openapi.yaml
30
+ hanami-json:
31
+ root: spec/apps/hanami/doc/openapi.json
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.25.1
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
@@ -67,6 +67,7 @@ files:
67
67
  - ".github/workflows/publish.yml"
68
68
  - ".github/workflows/rubocop.yml"
69
69
  - ".github/workflows/test.yml"
70
+ - ".github/workflows/validate-openapi.yml"
70
71
  - ".gitignore"
71
72
  - ".rspec"
72
73
  - ".rubocop.yml"
@@ -96,12 +97,14 @@ files:
96
97
  - lib/rspec/openapi/result_recorder.rb
97
98
  - lib/rspec/openapi/rspec_hooks.rb
98
99
  - lib/rspec/openapi/schema_builder.rb
100
+ - lib/rspec/openapi/schema_builder/build_context.rb
99
101
  - lib/rspec/openapi/schema_cleaner.rb
100
102
  - lib/rspec/openapi/schema_file.rb
101
103
  - lib/rspec/openapi/schema_merger.rb
102
104
  - lib/rspec/openapi/schema_sorter.rb
103
105
  - lib/rspec/openapi/shared_hooks.rb
104
106
  - lib/rspec/openapi/version.rb
107
+ - redocly.yaml
105
108
  - rspec-openapi.gemspec
106
109
  - scripts/rspec
107
110
  - scripts/rspec_with_simplecov
@@ -112,7 +115,7 @@ licenses:
112
115
  metadata:
113
116
  homepage_uri: https://github.com/exoego/rspec-openapi
114
117
  source_code_uri: https://github.com/exoego/rspec-openapi
115
- changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.25.1
118
+ changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.27.0
116
119
  rubygems_mfa_required: 'true'
117
120
  rdoc_options: []
118
121
  require_paths:
@@ -128,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
131
  - !ruby/object:Gem::Version
129
132
  version: '0'
130
133
  requirements: []
131
- rubygems_version: 4.0.6
134
+ rubygems_version: 4.0.10
132
135
  specification_version: 4
133
136
  summary: Generate OpenAPI schema from RSpec request specs
134
137
  test_files: []