rspec-openapi 0.18.4 → 0.20.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: ec34ef0e56eb4c46221d956120a836d20addbfa8d636807effad4b93cf5ac8f8
4
- data.tar.gz: 8b2c33feefc0f7173e6f31a09c66c9824b2f3f6124a24c6f56f59f7f5ff3e938
3
+ metadata.gz: a94cc7be13ab8a500bf3eb4bb5831f1c06c7722e96f9f7157e424fa402f2f137
4
+ data.tar.gz: db6591ab069d1f7bd1d7f07b6f2c4d9be9ef86f90df497c28a2b8ed01ee1d5e4
5
5
  SHA512:
6
- metadata.gz: e2e12be8f18ac6e3c6641bb3b128bad631c120f4400148f999b758b1b2820b44dd047569036c1adc4393b001a438f2595aacf5869540411ec077c054a6b8d4c3
7
- data.tar.gz: 347a1c54c418ad9957b3137b2d7db8c0c74496b473cdaf9f14dc9e4dab0205340e510317c324ff68494e237b27a369642d7cba2687b6eaa88318a70387557f37
6
+ metadata.gz: 785d100550f4ecde118a8986ee7c98349e310de9c870a04067a9f84892d37b63cadf9a88d0ff2158012c153c561c3a0576a3c0c79860a645bb53bb295c64bbc5
7
+ data.tar.gz: 660bd242fa8c5686bc6fbead56f595be959f1f1cae465723ed89e93c0b0096ee6c86094c8561193cabf119cedb74762e84e6b9c3651d34fca7691b63317fc611
@@ -25,7 +25,7 @@ jobs:
25
25
 
26
26
  steps:
27
27
  - name: Checkout repository
28
- uses: actions/checkout@v4
28
+ uses: actions/checkout@v5
29
29
 
30
30
  - name: Initialize CodeQL
31
31
  uses: github/codeql-action/init@v3
@@ -14,7 +14,7 @@ jobs:
14
14
 
15
15
  steps:
16
16
  - name: Checkout repository
17
- uses: actions/checkout@v4
17
+ uses: actions/checkout@v5
18
18
 
19
19
  - name: Set up Ruby
20
20
  uses: ruby/setup-ruby@v1
@@ -25,12 +25,14 @@ jobs:
25
25
  rails: 7.0.8
26
26
  - ruby: ruby:3.3
27
27
  rails: 7.1.3.2
28
+ - ruby: ruby:3.4
29
+ rails: 8.0.2
28
30
  coverage: coverage
29
31
  env:
30
32
  RAILS_VERSION: ${{ matrix.rails == '' && '6.1.6' || matrix.rails }}
31
33
  COVERAGE: ${{ matrix.coverage || '' }}
32
34
  steps:
33
- - uses: actions/checkout@v4
35
+ - uses: actions/checkout@v5
34
36
  - name: bundle install
35
37
  run: bundle install -j$(nproc) --retry 3
36
38
  - run: bundle exec rspec
data/.gitignore CHANGED
@@ -10,3 +10,5 @@
10
10
 
11
11
  # rspec failure tracking
12
12
  .rspec_status
13
+
14
+ spec/apps/rails/log/
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
- # rspec-openapi [![Gem Version](https://badge.fury.io/rb/rspec-openapi.svg)](https://rubygems.org/gems/rspec-openapi) [![test](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml/badge.svg)](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/exoego/rspec-openapi/branch/master/graph/badge.svg?token=egYm6AlxkD)](https://codecov.io/gh/exoego/rspec-openapi) [![Ruby-toolbox](https://img.shields.io/badge/ruby-toolbox-a61414?cacheSeconds=31536000)](https://www.ruby-toolbox.com/projects/rspec-openapi)
1
+ # rspec-openapi [![Gem Version](https://badge.fury.io/rb/rspec-openapi.svg)](https://rubygems.org/gems/rspec-openapi) [![test](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml/badge.svg)](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/exoego/rspec-openapi/branch/master/graph/badge.svg?token=egYm6AlxkD)](https://codecov.io/gh/exoego/rspec-openapi) [![Ruby-toolbox](https://img.shields.io/badge/ruby-toolbox-a61414?cacheSeconds=31536000)](https://www.ruby-toolbox.com/projects/rspec-openapi) [![DeepWiki](https://img.shields.io/badge/See_on-DeepWiki-blue)](https://deepwiki.com/exoego/rspec-openapi)
2
+
2
3
 
3
4
  Generate OpenAPI schema from RSpec request specs.
4
5
 
@@ -52,7 +53,7 @@ end
52
53
  If you run the spec with `OPENAPI=1`,
53
54
 
54
55
  ```
55
- OPENAPI=1 rspec spec/requests/tables_spec.rb
56
+ OPENAPI=1 bundle exec rspec spec/requests/tables_spec.rb
56
57
  ```
57
58
 
58
59
  It will generate [`doc/openapi.yaml` file](./spec/rails/doc/openapi.yaml) like:
@@ -187,6 +188,10 @@ RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_gro
187
188
  # This example uses the tags from the parent_example_group
188
189
  RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }
189
190
 
191
+ # Configure custom format for specific properties
192
+ # This example assigns 'date-time' format to properties with names ending in '_at'
193
+ RSpec::OpenAPI.formats_builder = ->(_example, key) { key.end_with?('_at') ? 'date-time' : nil }
194
+
190
195
  # Change the example type(s) that will generate schema
191
196
  RSpec::OpenAPI.example_types = %i[request]
192
197
 
@@ -56,9 +56,10 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new
56
56
 
57
57
  return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless route.routable?
58
58
 
59
- metadata = example.metadata[:openapi] || {}
59
+ metadata = merge_openapi_metadata(example.metadata)
60
60
  summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
61
61
  tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
62
+ formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
62
63
  operation_id = metadata[:operation_id]
63
64
  required_request_params = metadata[:required_request_params] || []
64
65
  security = metadata[:security]
@@ -76,7 +77,18 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new
76
77
 
77
78
  raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
78
79
 
79
- [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
80
+ [
81
+ path,
82
+ summary,
83
+ tags,
84
+ operation_id,
85
+ required_request_params,
86
+ raw_path_params,
87
+ description,
88
+ security,
89
+ deprecated,
90
+ formats,
91
+ ]
80
92
  end
81
93
 
82
94
  # @param [RSpec::ExampleGroups::*] context
@@ -88,6 +100,26 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new
88
100
  [request, response]
89
101
  end
90
102
 
103
+ private
104
+
105
+ def merge_openapi_metadata(metadata)
106
+ collect_openapi_metadata(metadata).reduce({}, &:merge)
107
+ end
108
+
109
+ def collect_openapi_metadata(metadata)
110
+ [].tap do |result|
111
+ current = metadata
112
+
113
+ while current
114
+ [current[:example_group], current].each do |meta|
115
+ result.unshift(meta[:openapi]) if meta&.dig(:openapi)
116
+ end
117
+
118
+ current = current[:parent_example_group]
119
+ end
120
+ end
121
+ end
122
+
91
123
  def add_id(path, route)
92
124
  return path if route.params.empty?
93
125
 
@@ -6,9 +6,10 @@ class << RSpec::OpenAPI::Extractors::Rack = Object.new
6
6
  # @param [RSpec::Core::Example] example
7
7
  # @return Array
8
8
  def request_attributes(request, example)
9
- metadata = example.metadata[:openapi] || {}
9
+ metadata = merge_openapi_metadata(example.metadata)
10
10
  summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
11
11
  tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
12
+ formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
12
13
  operation_id = metadata[:operation_id]
13
14
  required_request_params = metadata[:required_request_params] || []
14
15
  security = metadata[:security]
@@ -17,7 +18,18 @@ class << RSpec::OpenAPI::Extractors::Rack = Object.new
17
18
  raw_path_params = request.path_parameters
18
19
  path = request.path
19
20
  summary ||= "#{request.method} #{path}"
20
- [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
21
+ [
22
+ path,
23
+ summary,
24
+ tags,
25
+ operation_id,
26
+ required_request_params,
27
+ raw_path_params,
28
+ description,
29
+ security,
30
+ deprecated,
31
+ formats,
32
+ ]
21
33
  end
22
34
 
23
35
  # @param [RSpec::ExampleGroups::*] context
@@ -28,4 +40,24 @@ class << RSpec::OpenAPI::Extractors::Rack = Object.new
28
40
 
29
41
  [request, response]
30
42
  end
43
+
44
+ private
45
+
46
+ def merge_openapi_metadata(metadata)
47
+ collect_openapi_metadata(metadata).reduce({}, &:merge)
48
+ end
49
+
50
+ def collect_openapi_metadata(metadata)
51
+ [].tap do |result|
52
+ current = metadata
53
+
54
+ while current
55
+ [current[:example_group], current].each do |meta|
56
+ result.unshift(meta[:openapi]) if meta&.dig(:openapi)
57
+ end
58
+
59
+ current = current[:parent_example_group]
60
+ end
61
+ end
62
+ end
31
63
  end
@@ -16,9 +16,11 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new
16
16
 
17
17
  raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil?
18
18
 
19
- metadata = example.metadata[:openapi] || {}
19
+ metadata = merge_openapi_metadata(example.metadata)
20
20
  summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
21
21
  tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
22
+ formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
23
+
22
24
  operation_id = metadata[:operation_id]
23
25
  required_request_params = metadata[:required_request_params] || []
24
26
  security = metadata[:security]
@@ -34,7 +36,18 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new
34
36
 
35
37
  summary ||= "#{request.method} #{path}"
36
38
 
37
- [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
39
+ [
40
+ path,
41
+ summary,
42
+ tags,
43
+ operation_id,
44
+ required_request_params,
45
+ raw_path_params,
46
+ description,
47
+ security,
48
+ deprecated,
49
+ formats,
50
+ ]
38
51
  end
39
52
 
40
53
  # @param [RSpec::ExampleGroups::*] context
@@ -42,6 +55,26 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new
42
55
  [context.request, context.response]
43
56
  end
44
57
 
58
+ private
59
+
60
+ def merge_openapi_metadata(metadata)
61
+ collect_openapi_metadata(metadata).reduce({}, &:merge)
62
+ end
63
+
64
+ def collect_openapi_metadata(metadata)
65
+ [].tap do |result|
66
+ current = metadata
67
+
68
+ while current
69
+ [current[:example_group], current].each do |meta|
70
+ result.unshift(meta[:openapi]) if meta&.dig(:openapi)
71
+ end
72
+
73
+ current = current[:parent_example_group]
74
+ end
75
+ end
76
+ end
77
+
45
78
  # @param [ActionDispatch::Request] request
46
79
  def find_rails_route(request, app: Rails.application, path_prefix: '')
47
80
  app.routes.router.recognize(request) do |route, _parameters|
@@ -12,6 +12,7 @@ RSpec::OpenAPI::Record = Struct.new(
12
12
  :request_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
13
13
  :summary, # @param [String] - "v1/statuses #show"
14
14
  :tags, # @param [Array] - ["Status"]
15
+ :formats, # @param [Proc] - ->(key) { key.end_with?('_at') ? 'date-time' : nil }
15
16
  :operation_id, # @param [String] - "request-1234"
16
17
  :description, # @param [String] - "returns a status"
17
18
  :security, # @param [Array] - [{securityScheme1: []}]
@@ -12,7 +12,7 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
12
12
  return if request.nil?
13
13
 
14
14
  title = RSpec::OpenAPI.title.then { |t| t.is_a?(Proc) ? t.call(example) : t }
15
- path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated =
15
+ path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated, formats =
16
16
  extractor.request_attributes(request, example)
17
17
 
18
18
  return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) }
@@ -31,6 +31,7 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
31
31
  request_headers: request_headers,
32
32
  summary: summary,
33
33
  tags: tags,
34
+ formats: formats,
34
35
  operation_id: operation_id,
35
36
  description: description,
36
37
  security: security,
@@ -18,7 +18,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
18
18
  if has_content
19
19
  response[:content] = {
20
20
  normalize_content_type(record.response_content_type) => {
21
- schema: build_property(record.response_body, disposition: disposition),
21
+ schema: build_property(record.response_body, disposition: disposition, record: record),
22
22
  example: response_example(record, disposition: disposition),
23
23
  }.compact,
24
24
  }
@@ -73,7 +73,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
73
73
  name: build_parameter_name(key, value),
74
74
  in: 'path',
75
75
  required: true,
76
- schema: build_property(try_cast(value)),
76
+ schema: build_property(try_cast(value), key: key, record: record),
77
77
  example: (try_cast(value) if example_enabled?),
78
78
  }.compact
79
79
  end
@@ -83,7 +83,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
83
83
  name: build_parameter_name(key, value),
84
84
  in: 'query',
85
85
  required: record.required_request_params.include?(key),
86
- schema: build_property(try_cast(value)),
86
+ schema: build_property(try_cast(value), key: key, record: record),
87
87
  example: (try_cast(value) if example_enabled?),
88
88
  }.compact
89
89
  end
@@ -93,7 +93,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
93
93
  name: build_parameter_name(key, value),
94
94
  in: 'header',
95
95
  required: true,
96
- schema: build_property(try_cast(value)),
96
+ schema: build_property(try_cast(value), key: key, record: record),
97
97
  example: (try_cast(value) if example_enabled?),
98
98
  }.compact
99
99
  end
@@ -110,7 +110,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
110
110
 
111
111
  record.response_headers.each do |key, value|
112
112
  headers[key] = {
113
- schema: build_property(try_cast(value)),
113
+ schema: build_property(try_cast(value), key: key, record: record),
114
114
  }.compact
115
115
  end
116
116
 
@@ -134,27 +134,29 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
134
134
  {
135
135
  content: {
136
136
  normalize_content_type(record.request_content_type) => {
137
- schema: build_property(record.request_params),
137
+ schema: build_property(record.request_params, record: record),
138
138
  example: (build_example(record.request_params) if example_enabled?),
139
139
  }.compact,
140
140
  },
141
141
  }
142
142
  end
143
143
 
144
- def build_property(value, disposition: nil)
145
- property = build_type(value, disposition)
144
+ def build_property(value, disposition: nil, key: nil, record: nil)
145
+ format = disposition ? 'binary' : infer_format(key, record)
146
+
147
+ property = build_type(value, format: format)
146
148
 
147
149
  case value
148
150
  when Array
149
151
  property[:items] = if value.empty?
150
152
  {} # unknown
151
153
  else
152
- build_property(value.first)
154
+ build_property(value.first, record: record)
153
155
  end
154
156
  when Hash
155
157
  property[:properties] = {}.tap do |properties|
156
158
  value.each do |key, v|
157
- properties[key] = build_property(v)
159
+ properties[key] = build_property(v, record: record, key: key)
158
160
  end
159
161
  end
160
162
  property = enrich_with_required_keys(property)
@@ -162,8 +164,8 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
162
164
  property
163
165
  end
164
166
 
165
- def build_type(value, disposition)
166
- return { type: 'string', format: 'binary' } if disposition
167
+ def build_type(value, format: nil)
168
+ return { type: 'string', format: format } if format
167
169
 
168
170
  case value
169
171
  when String
@@ -187,6 +189,12 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
187
189
  end
188
190
  end
189
191
 
192
+ def infer_format(key, record)
193
+ return nil if !key || !record || !record.formats
194
+
195
+ record.formats[key]
196
+ end
197
+
190
198
  # Convert an always-String param to an appropriate type
191
199
  def try_cast(value)
192
200
  Integer(value)
@@ -50,7 +50,7 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
50
50
  parent_path_definition = base.dig(*path.take(path.length - 1))
51
51
 
52
52
  security_schemes.each do |security_scheme_name, security_scheme|
53
- remove_parameters_conflicting_with_security_sceheme!(
53
+ remove_parameters_conflicting_with_security_scheme!(
54
54
  parent_path_definition, security_scheme, security_scheme_name,
55
55
  )
56
56
  end
@@ -71,7 +71,7 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
71
71
 
72
72
  private
73
73
 
74
- def remove_parameters_conflicting_with_security_sceheme!(path_definition, security_scheme, security_scheme_name)
74
+ def remove_parameters_conflicting_with_security_scheme!(path_definition, security_scheme, security_scheme_name)
75
75
  return unless path_definition[:security]
76
76
  return unless path_definition[:parameters]
77
77
  return unless path_definition.dig(:security, 0).keys.include?(security_scheme_name)
@@ -4,6 +4,9 @@ require 'fileutils'
4
4
  require 'yaml'
5
5
  require 'json'
6
6
 
7
+ # For Ruby 2.7
8
+ require 'date'
9
+
7
10
  # TODO: Support JSON
8
11
  class RSpec::OpenAPI::SchemaFile
9
12
  # @param [String] path
@@ -24,7 +27,12 @@ class RSpec::OpenAPI::SchemaFile
24
27
  def read
25
28
  return {} unless File.exist?(@path)
26
29
 
27
- RSpec::OpenAPI::KeyTransformer.symbolize(YAML.safe_load(File.read(@path))) # this can also parse JSON
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
28
36
  end
29
37
 
30
38
  # @param [Hash] spec
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module OpenAPI
5
- VERSION = '0.18.4'
5
+ VERSION = '0.20.0'
6
6
  end
7
7
  end
data/lib/rspec/openapi.rb CHANGED
@@ -33,6 +33,7 @@ module RSpec::OpenAPI
33
33
  @description_builder = ->(example) { example.description }
34
34
  @summary_builder = ->(example) { example.metadata[:summary] }
35
35
  @tags_builder = ->(example) { example.metadata[:tags] }
36
+ @formats_builder = ->(example) { example.metadata[:formats] }
36
37
  @info = {}
37
38
  @application_version = '1.0.0'
38
39
  @request_headers = []
@@ -56,6 +57,7 @@ module RSpec::OpenAPI
56
57
  :description_builder,
57
58
  :summary_builder,
58
59
  :tags_builder,
60
+ :formats_builder,
59
61
  :info,
60
62
  :application_version,
61
63
  :request_headers,
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-openapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.4
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
8
8
  - TATSUNO Yasuhiro
9
- autorequire:
10
9
  bindir: exe
11
10
  cert_chain: []
12
- date: 2025-02-06 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: actionpack
@@ -109,9 +108,8 @@ licenses:
109
108
  metadata:
110
109
  homepage_uri: https://github.com/exoego/rspec-openapi
111
110
  source_code_uri: https://github.com/exoego/rspec-openapi
112
- changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.18.4
111
+ changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.20.0
113
112
  rubygems_mfa_required: 'true'
114
- post_install_message:
115
113
  rdoc_options: []
116
114
  require_paths:
117
115
  - lib
@@ -126,8 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
124
  - !ruby/object:Gem::Version
127
125
  version: '0'
128
126
  requirements: []
129
- rubygems_version: 3.4.6
130
- signing_key:
127
+ rubygems_version: 3.6.9
131
128
  specification_version: 4
132
129
  summary: Generate OpenAPI schema from RSpec request specs
133
130
  test_files: []