rspec-openapi 0.26.0 → 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.*')
@@ -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,37 +25,51 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
25
25
  return base
26
26
  end
27
27
 
28
- # When the new spec converts an object to a dictionary (introduces
29
- # `additionalProperties` on a node that previously had `properties` /
30
- # `required`), drop the stale fields so the merged result reflects the
31
- # new intent. We only prune when base does not already declare
32
- # `additionalProperties`, to preserve manual edits that intentionally
33
- # combine fixed and dynamic keys.
34
- if spec.is_a?(Hash) && spec.key?(:additionalProperties) && !base.key?(:additionalProperties)
35
- base.delete(:properties)
36
- base.delete(:required)
28
+ prune_stale_object_fields!(base, spec)
29
+
30
+ spec.each { |key, value| merge_entry!(base, key, value) }
31
+ base
32
+ end
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
37
55
  end
56
+ end
38
57
 
39
- spec.each do |key, value|
40
- if base[key].is_a?(Hash) && value.is_a?(Hash)
41
- # Handle example/examples conflict - convert to examples when mixed
42
- normalize_example_fields!(base[key], value)
43
-
44
- # If the new value has oneOf, replace the entire value instead of merging
45
- if value.key?(:oneOf)
46
- base[key] = value
47
- else
48
- merge_schema!(base[key], value) unless base[key].key?(:$ref)
49
- end
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
- else
54
- # do not ADD `properties` or `required` fields if `additionalProperties` field is present
55
- base[key] = value unless base.key?(:additionalProperties) && %i[properties required].include?(key)
56
- end
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)
57
67
  end
58
- base
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)
59
73
  end
60
74
 
61
75
  def merge_arrays(base, key, value)
@@ -72,21 +86,33 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
72
86
  end
73
87
 
74
88
  def merge_parameters(base, key, value)
75
- all_parameters = value | base[key]
76
-
77
- unique_base_parameters = build_unique_params(base, key)
78
-
79
- all_parameters = all_parameters.map do |parameter|
80
- base_parameter = unique_base_parameters[[parameter[:name], parameter[:in]]] || {}
81
- if base_parameter.empty?
82
- 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)
83
104
  else
84
- merge_parameter_with_schema(base_parameter, parameter)
105
+ mark_optional_unless_path(base_param)
85
106
  end
86
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'
87
114
 
88
- all_parameters.uniq! { |param| param.slice(:name, :in) }
89
- base[key] = all_parameters
115
+ parameter.merge(required: false)
90
116
  end
91
117
 
92
118
  def merge_parameter_with_schema(base_param, new_param)
@@ -94,12 +120,18 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
94
120
  new_schema = new_param[:schema]
95
121
 
96
122
  # If schemas have different types, create a oneOf
97
- if base_schema && new_schema && schemas_have_different_types?(base_schema, new_schema)
98
- merged_schema = merge_schemas_into_one_of(base_schema, new_schema)
99
- base_param.merge(new_param).merge(schema: merged_schema)
100
- else
101
- base_param.merge(new_param)
102
- 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
103
135
  end
104
136
 
105
137
  def schemas_have_different_types?(schema1, schema2)
@@ -133,10 +165,8 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
133
165
  end
134
166
  end
135
167
 
136
- def build_unique_params(base, key)
137
- base[key].to_h do |parameter|
138
- [[parameter[:name], parameter[:in]], parameter]
139
- end
168
+ def index_parameters_by_identity(parameters)
169
+ parameters.to_h { |p| [[p[:name], p[:in]], p] }
140
170
  end
141
171
 
142
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.26.0'
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
 
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.26.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
@@ -97,6 +97,7 @@ files:
97
97
  - lib/rspec/openapi/result_recorder.rb
98
98
  - lib/rspec/openapi/rspec_hooks.rb
99
99
  - lib/rspec/openapi/schema_builder.rb
100
+ - lib/rspec/openapi/schema_builder/build_context.rb
100
101
  - lib/rspec/openapi/schema_cleaner.rb
101
102
  - lib/rspec/openapi/schema_file.rb
102
103
  - lib/rspec/openapi/schema_merger.rb
@@ -114,7 +115,7 @@ licenses:
114
115
  metadata:
115
116
  homepage_uri: https://github.com/exoego/rspec-openapi
116
117
  source_code_uri: https://github.com/exoego/rspec-openapi
117
- changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.26.0
118
+ changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.27.0
118
119
  rubygems_mfa_required: 'true'
119
120
  rdoc_options: []
120
121
  require_paths:
@@ -130,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
131
  - !ruby/object:Gem::Version
131
132
  version: '0'
132
133
  requirements: []
133
- rubygems_version: 4.0.6
134
+ rubygems_version: 4.0.10
134
135
  specification_version: 4
135
136
  summary: Generate OpenAPI schema from RSpec request specs
136
137
  test_files: []