rspec-openapi 0.28.0 → 0.29.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: 43c486a798aeae6de0f9ecbec34ab2b69a11bb2b1ff053011110c611e5151fc0
4
- data.tar.gz: f483a6aca96935df40e9b34386b34548f3322982935c3801bab33ee72d20746e
3
+ metadata.gz: 97857dc0658df5323bf3997e3acc31cf8f8b2f713a2d2ec6e3fdb0be7661aaf5
4
+ data.tar.gz: 9ecdb6c88c4c58042f805d899cda3d50fe2b670286de4d7f64b3799fd1ee5d48
5
5
  SHA512:
6
- metadata.gz: 7860f7a5030487e9671ee926bcf27967c827a4d80f6119eca70c8e9b330c296a5813bc8412d3b210e679700baef2d8710bcd9c9ca24c8b5890a14d90823f8c36
7
- data.tar.gz: c2d9e3aaab37b25acc4d1ce563076a2fcb151f33a699268d9035d1559b428f75a75dab52394d0810a0443f031e9f3f338e9996320e5c258b598d0262a71bf1a0
6
+ metadata.gz: 14577c70d8caca9754f4893be1720ed67dbb4762f1479a4454e188cc954ba8e9651f887cc0c69a95c91c1c98f994739bf12667f7470a491b60f9f2ce21ac81fc
7
+ data.tar.gz: feb18e9c32bed1f101dca61683e30975857257999fac322ff9ed51519a7ac13d583ad1cf68b3f20c56782919c9bb2d6d83d94396204c3287c468c692b1b121e1
data/README.md CHANGED
@@ -116,6 +116,10 @@ RSpec::OpenAPI.path = 'doc/schema.yaml'
116
116
  # Change the output type to JSON
117
117
  RSpec::OpenAPI.path = 'doc/schema.json'
118
118
 
119
+ # Or emit the same schema in multiple formats from a single run by passing an array.
120
+ # The output format of each file is still chosen by its extension (`.json` -> JSON, otherwise YAML).
121
+ RSpec::OpenAPI.path = ['doc/openapi.yaml', 'doc/openapi.json']
122
+
119
123
  # Or generate multiple partial schema files, given an RSpec example
120
124
  RSpec::OpenAPI.path = -> (example) {
121
125
  case example.file_path
@@ -125,6 +129,12 @@ RSpec::OpenAPI.path = -> (example) {
125
129
  end
126
130
  }
127
131
 
132
+ # The two can be combined: a proc may also return an array to fan a partial schema out to multiple formats.
133
+ RSpec::OpenAPI.path = -> (example) {
134
+ base = example.file_path.match?(%r[spec/requests/api/v2/]) ? 'doc/openapi/v2' : 'doc/openapi'
135
+ ["#{base}.yaml", "#{base}.json"]
136
+ }
137
+
128
138
  # Change the default title of the generated schema
129
139
  RSpec::OpenAPI.title = 'OpenAPI Documentation'
130
140
 
@@ -190,10 +200,16 @@ RSpec::OpenAPI.description_builder = -> (example) { example.description }
190
200
 
191
201
  # Generate a custom summary, given an RSpec example
192
202
  # This example uses the summary from the example_group.
203
+ # NOTE: a fixed `dig` only reaches one specific depth. `summary` declared on an
204
+ # ancestor group is already inherited automatically (see "Inheritance of
205
+ # `openapi:` metadata"), so a builder is only needed for fully custom logic.
193
206
  RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_group, :openapi, :summary) }
194
207
 
195
208
  # Generate a custom tags, given an RSpec example
196
- # This example uses the tags from the parent_example_group
209
+ # This example uses the tags from the parent_example_group.
210
+ # NOTE: `dig(:example_group, :parent_example_group, ...)` only matches tags
211
+ # declared exactly two levels up; it won't find tags on deeper ancestors. Rely
212
+ # on the built-in inheritance unless you need custom resolution.
197
213
  RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }
198
214
 
199
215
  # Configure custom format for specific properties
@@ -369,6 +385,26 @@ end
369
385
 
370
386
  **NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.
371
387
 
388
+ ### Inheritance of `openapi:` metadata
389
+
390
+ `openapi:` metadata is inherited from the surrounding `describe`/`context` groups down to each
391
+ example, at any nesting depth. Inner levels override outer ones key by key, so a nested group only
392
+ needs to declare the keys it wants to change:
393
+
394
+ ```rb
395
+ describe 'GET /tables', openapi: { summary: 'Get a list of tables', tags: %w[Table] } do
396
+ context 'with pagination', openapi: { example_mode: :multiple } do
397
+ # This example inherits summary: 'Get a list of tables' and tags: ['Table']
398
+ # from the outer describe, and adds example_mode: :multiple.
399
+ it { get '/tables', params: { page: 1 } }
400
+ end
401
+ end
402
+ ```
403
+
404
+ The merge happens at the top level of the `openapi:` hash. Scalar keys (`summary`, `operation_id`,
405
+ …) follow last-wins, and a nested level that re-declares a structured key (`tags`, `security`,
406
+ `enum`, …) replaces the inherited value for that key rather than deep-merging it.
407
+
372
408
  ### Enum Support
373
409
 
374
410
  You can specify enum values for string properties that should have a fixed set of allowed values. Since enums cannot be
@@ -86,14 +86,12 @@ class SharedExtractor
86
86
 
87
87
  def self.collect_openapi_metadata(metadata)
88
88
  [].tap do |result|
89
- current = metadata
89
+ result.unshift(metadata[:openapi]) if metadata[:openapi]
90
90
 
91
- while current
92
- [current[:example_group], current].each do |meta|
93
- result.unshift(meta[:openapi]) if meta&.dig(:openapi)
94
- end
95
-
96
- current = current[:parent_example_group]
91
+ group = metadata.fetch(:example_group) { metadata[:parent_example_group] }
92
+ while group
93
+ result.unshift(group[:openapi]) if group[:openapi]
94
+ group = group[:parent_example_group]
97
95
  end
98
96
  end
99
97
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
3
4
  require 'action_dispatch'
4
5
  require 'rspec/openapi/record'
5
6
 
@@ -7,31 +7,16 @@ class RSpec::OpenAPI::ResultRecorder
7
7
  end
8
8
 
9
9
  def record_results!
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
10
+ @path_records.each do |paths, records|
11
+ # A single record set may target multiple output files (e.g. both YAML and
12
+ # JSON). The first path is the canonical source we read/merge into; the rest
13
+ # mirror the same built spec, each formatted by its own extension.
14
+ primary, *mirrors = Array(paths)
15
+ next if primary.nil?
18
16
 
19
- title = records.first.title
20
- RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
21
- schema = RSpec::OpenAPI::DefaultSchema.build(title)
22
- schema[:info].merge!(RSpec::OpenAPI.info)
23
- RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
24
- new_from_zero = {}
25
- records.each do |record|
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
31
- end
32
- cleanup_schema!(new_from_zero, spec)
33
- execute_post_process_hook(path, records, spec)
34
- end
17
+ load_path_config(primary)
18
+ built_spec = build_spec(primary, records)
19
+ mirrors.each { |mirror_path| RSpec::OpenAPI::SchemaFile.new(mirror_path).write(built_spec) }
35
20
  end
36
21
  end
37
22
 
@@ -49,6 +34,33 @@ class RSpec::OpenAPI::ResultRecorder
49
34
 
50
35
  private
51
36
 
37
+ # Look for a path-specific config file and run it.
38
+ def load_path_config(path)
39
+ config_file = File.join(File.dirname(path), RSpec::OpenAPI.config_filename)
40
+ require config_file if File.exist?(config_file)
41
+ rescue StandardError => e
42
+ puts "WARNING: Unable to load #{config_file}: #{e}"
43
+ end
44
+
45
+ def build_spec(primary, records)
46
+ title = records.first.title
47
+ RSpec::OpenAPI::SchemaFile.new(primary).edit do |spec|
48
+ schema = RSpec::OpenAPI::DefaultSchema.build(title)
49
+ schema[:info].merge!(RSpec::OpenAPI.info)
50
+ RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
51
+ new_from_zero = {}
52
+ records.each do |record|
53
+ record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
54
+ RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
55
+ RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
56
+ rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
57
+ @error_records[e] = record # Avoid failing the build
58
+ end
59
+ cleanup_schema!(new_from_zero, spec)
60
+ execute_post_process_hook(primary, records, spec)
61
+ end
62
+ end
63
+
52
64
  def execute_post_process_hook(path, records, spec)
53
65
  RSpec::OpenAPI.post_process_hook.call(path, records, spec) if RSpec::OpenAPI.post_process_hook.is_a?(Proc)
54
66
  end
@@ -14,11 +14,30 @@ class RSpec::OpenAPI::SchemaFile
14
14
  @path = path
15
15
  end
16
16
 
17
+ # Reads the existing spec, lets the block mutate it, writes the result back
18
+ # and returns the (symbolized) spec so it can be mirrored to other files.
19
+ # @return [Hash]
17
20
  def edit(&block)
18
21
  spec = read
19
22
  block.call(spec)
23
+ spec
20
24
  ensure
21
- write(RSpec::OpenAPI::KeyTransformer.stringify(spec))
25
+ write(spec)
26
+ end
27
+
28
+ # Writes an already-built spec to this file, choosing the format from the
29
+ # file extension.
30
+ # @param [Hash] spec
31
+ def write(spec)
32
+ stringified = RSpec::OpenAPI::KeyTransformer.stringify(spec)
33
+ FileUtils.mkdir_p(File.dirname(@path))
34
+ output =
35
+ if json?
36
+ JSON.pretty_generate(stringified)
37
+ else
38
+ prepend_comment(YAML.dump(stringified))
39
+ end
40
+ File.write(@path, output)
22
41
  end
23
42
 
24
43
  private
@@ -33,18 +52,6 @@ class RSpec::OpenAPI::SchemaFile
33
52
  RSpec::OpenAPI::KeyTransformer.symbolize(content)
34
53
  end
35
54
 
36
- # @param [Hash] spec
37
- def write(spec)
38
- FileUtils.mkdir_p(File.dirname(@path))
39
- output =
40
- if json?
41
- JSON.pretty_generate(spec)
42
- else
43
- prepend_comment(YAML.dump(spec))
44
- end
45
- File.write(@path, output)
46
- end
47
-
48
55
  def prepend_comment(content)
49
56
  return content if RSpec::OpenAPI.comment.nil?
50
57
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module OpenAPI
5
- VERSION = '0.28.0'
5
+ VERSION = '0.29.0'
6
6
  end
7
7
  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.28.0
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
@@ -93,7 +93,7 @@ licenses:
93
93
  metadata:
94
94
  homepage_uri: https://github.com/exoego/rspec-openapi
95
95
  source_code_uri: https://github.com/exoego/rspec-openapi
96
- changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.28.0
96
+ changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.29.0
97
97
  rubygems_mfa_required: 'true'
98
98
  rdoc_options: []
99
99
  require_paths: