rspec-openapi 0.18.3 → 0.19.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: 39da96c0f0b1650e0504d45a624be7905c9f47a2b128e992eecb5b05b828dd87
4
- data.tar.gz: 5c6cb0fdaf3900ae93d0618c82b6762c8cf50002e7ac155c23a46b543f91d126
3
+ metadata.gz: f0fd8968099cf455a6ee1eabe6039c60955020493d97e584fb57b1ba79aca8d3
4
+ data.tar.gz: 15ae08318ba0843a65cbd4bf418b82f2ce9d7c173600f0ea2d776763f4790819
5
5
  SHA512:
6
- metadata.gz: df87ce7e2b08623ec314330020d011ede771872a435f11b266be1f1c1a2e24c8329559b1f3e42b50a58a86c442c0afed2e98444694138823b9b17ebb94e54878
7
- data.tar.gz: c89fb952a0d8fbc4c5ed814bd11513d2323f2c5237ad0a7b0e341e2522c329a331230b51ecab3bcf42a60dd4e5da090a337202dac37db3d4f68ab82ad7a9f91b
6
+ metadata.gz: 2c15dc4a7d63f610cd654ea2718ffccbd15c59ec66bcc45f219352bdccbe7a6e49baa4810a866fefd483b34ec94992a7f48ce799d8cace7ced59f2dd5fb9db81
7
+ data.tar.gz: 9306ae8e95e9d4dcb0aa40d20b716217c2712ad081448c7f61cb832dd210864883c6f93999257715347fd85b5eb9f1d5ccb0615db3102de112fdfddebeb78318
@@ -25,6 +25,8 @@ 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 }}
@@ -38,7 +40,7 @@ jobs:
38
40
  - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
39
41
  name: codecov-action@v4 workaround
40
42
  - name: Upload coverage reports
41
- uses: codecov/codecov-action@v4
43
+ uses: codecov/codecov-action@v5
42
44
  if: matrix.coverage == 'coverage'
43
45
  with:
44
46
  fail_ci_if_error: true
data/Gemfile CHANGED
@@ -11,8 +11,12 @@ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0')
11
11
  gem 'hanami', ENV['HANAMI_VERSION'] || '2.1.0'
12
12
  gem 'hanami-controller', ENV['HANAMI_VERSION'] || '2.1.0'
13
13
  gem 'hanami-router', ENV['HANAMI_VERSION'] || '2.1.0'
14
+
15
+ gem 'dry-logger', '1.0.3'
14
16
  end
15
17
 
18
+ gem 'concurrent-ruby', '1.3.4'
19
+
16
20
  gem 'roda'
17
21
 
18
22
  gem 'rails-dom-testing', '~> 2.2'
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:
@@ -122,8 +123,18 @@ RSpec::OpenAPI.path = -> (example) {
122
123
  end
123
124
  }
124
125
 
126
+ # Change the default title of the generated schema
125
127
  RSpec::OpenAPI.title = 'OpenAPI Documentation'
126
128
 
129
+ # Or generate individual titles for your partial schema files, given an RSpec example
130
+ RSpec::OpenAPI.title = -> (example) {
131
+ case example.file_path
132
+ when %r[spec/requests/api/v1/] then 'API v1 Documentation'
133
+ when %r[spec/requests/api/v2/] then 'API v2 Documentation'
134
+ else 'OpenAPI Documentation'
135
+ end
136
+ }
137
+
127
138
  # Disable generating `example`
128
139
  RSpec::OpenAPI.enable_example = false
129
140
 
@@ -177,6 +188,10 @@ RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_gro
177
188
  # This example uses the tags from the parent_example_group
178
189
  RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }
179
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
+
180
195
  # Change the example type(s) that will generate schema
181
196
  RSpec::OpenAPI.example_types = %i[request]
182
197
 
@@ -59,6 +59,7 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new
59
59
  metadata = example.metadata[:openapi] || {}
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
@@ -9,6 +9,7 @@ class << RSpec::OpenAPI::Extractors::Rack = Object.new
9
9
  metadata = example.metadata[:openapi] || {}
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
@@ -12,13 +12,15 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new
12
12
 
13
13
  route, path = find_rails_route(fixed_request)
14
14
 
15
- raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil?
16
-
17
15
  return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless path
18
16
 
17
+ raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil?
18
+
19
19
  metadata = example.metadata[:openapi] || {}
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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec::OpenAPI::Record = Struct.new(
4
+ :title, # @param [String] - "API Documentation - Statuses"
4
5
  :http_method, # @param [String] - "GET"
5
6
  :path, # @param [String] - "/v1/status/:id"
6
7
  :path_params, # @param [Hash] - {:controller=>"v1/statuses", :action=>"create", :id=>"1"}
@@ -11,6 +12,7 @@ RSpec::OpenAPI::Record = Struct.new(
11
12
  :request_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
12
13
  :summary, # @param [String] - "v1/statuses #show"
13
14
  :tags, # @param [Array] - ["Status"]
15
+ :formats, # @param [Proc] - ->(key) { key.end_with?('_at') ? 'date-time' : nil }
14
16
  :operation_id, # @param [String] - "request-1234"
15
17
  :description, # @param [String] - "returns a status"
16
18
  :security, # @param [Array] - [{securityScheme1: []}]
@@ -11,7 +11,8 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
11
11
  request, response = extractor.request_response(context)
12
12
  return if request.nil?
13
13
 
14
- path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated =
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, formats =
15
16
  extractor.request_attributes(request, example)
16
17
 
17
18
  return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) }
@@ -19,6 +20,7 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
19
20
  request_headers, response_headers = extract_headers(request, response)
20
21
 
21
22
  RSpec::OpenAPI::Record.new(
23
+ title: title,
22
24
  http_method: request.method,
23
25
  path: path,
24
26
  path_params: raw_path_params,
@@ -29,6 +31,7 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
29
31
  request_headers: request_headers,
30
32
  summary: summary,
31
33
  tags: tags,
34
+ formats: formats,
32
35
  operation_id: operation_id,
33
36
  description: description,
34
37
  security: security,
@@ -16,7 +16,7 @@ class RSpec::OpenAPI::ResultRecorder
16
16
  puts "WARNING: Unable to load #{config_file}: #{e}"
17
17
  end
18
18
 
19
- title = RSpec::OpenAPI.title
19
+ title = records.first.title
20
20
  RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
21
21
  schema = RSpec::OpenAPI::DefaultSchema.build(title)
22
22
  schema[:info].merge!(RSpec::OpenAPI.info)
@@ -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
  }
@@ -36,7 +36,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
36
36
  security: record.security,
37
37
  deprecated: record.deprecated ? true : nil,
38
38
  parameters: build_parameters(record),
39
- requestBody: http_method == 'get' ? nil : build_request_body(record),
39
+ requestBody: include_nil_request_body?(http_method) ? nil : build_request_body(record),
40
40
  responses: {
41
41
  record.status.to_s => response,
42
42
  },
@@ -48,6 +48,10 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
48
48
 
49
49
  private
50
50
 
51
+ def include_nil_request_body?(http_method)
52
+ %w[delete get].include?(http_method)
53
+ end
54
+
51
55
  def enrich_with_required_keys(obj)
52
56
  obj[:required] = obj[:properties]&.keys || []
53
57
  obj
@@ -69,7 +73,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
69
73
  name: build_parameter_name(key, value),
70
74
  in: 'path',
71
75
  required: true,
72
- schema: build_property(try_cast(value)),
76
+ schema: build_property(try_cast(value), key: key, record: record),
73
77
  example: (try_cast(value) if example_enabled?),
74
78
  }.compact
75
79
  end
@@ -79,7 +83,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
79
83
  name: build_parameter_name(key, value),
80
84
  in: 'query',
81
85
  required: record.required_request_params.include?(key),
82
- schema: build_property(try_cast(value)),
86
+ schema: build_property(try_cast(value), key: key, record: record),
83
87
  example: (try_cast(value) if example_enabled?),
84
88
  }.compact
85
89
  end
@@ -89,7 +93,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
89
93
  name: build_parameter_name(key, value),
90
94
  in: 'header',
91
95
  required: true,
92
- schema: build_property(try_cast(value)),
96
+ schema: build_property(try_cast(value), key: key, record: record),
93
97
  example: (try_cast(value) if example_enabled?),
94
98
  }.compact
95
99
  end
@@ -106,7 +110,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
106
110
 
107
111
  record.response_headers.each do |key, value|
108
112
  headers[key] = {
109
- schema: build_property(try_cast(value)),
113
+ schema: build_property(try_cast(value), key: key, record: record),
110
114
  }.compact
111
115
  end
112
116
 
@@ -130,27 +134,29 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
130
134
  {
131
135
  content: {
132
136
  normalize_content_type(record.request_content_type) => {
133
- schema: build_property(record.request_params),
137
+ schema: build_property(record.request_params, record: record),
134
138
  example: (build_example(record.request_params) if example_enabled?),
135
139
  }.compact,
136
140
  },
137
141
  }
138
142
  end
139
143
 
140
- def build_property(value, disposition: nil)
141
- 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)
142
148
 
143
149
  case value
144
150
  when Array
145
151
  property[:items] = if value.empty?
146
152
  {} # unknown
147
153
  else
148
- build_property(value.first)
154
+ build_property(value.first, record: record)
149
155
  end
150
156
  when Hash
151
157
  property[:properties] = {}.tap do |properties|
152
158
  value.each do |key, v|
153
- properties[key] = build_property(v)
159
+ properties[key] = build_property(v, record: record, key: key)
154
160
  end
155
161
  end
156
162
  property = enrich_with_required_keys(property)
@@ -158,8 +164,8 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
158
164
  property
159
165
  end
160
166
 
161
- def build_type(value, disposition)
162
- return { type: 'string', format: 'binary' } if disposition
167
+ def build_type(value, format: nil)
168
+ return { type: 'string', format: format } if format
163
169
 
164
170
  case value
165
171
  when String
@@ -183,6 +189,12 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
183
189
  end
184
190
  end
185
191
 
192
+ def infer_format(key, record)
193
+ return nil if !key || !record || !record.formats
194
+
195
+ record.formats[key]
196
+ end
197
+
186
198
  # Convert an always-String param to an appropriate type
187
199
  def try_cast(value)
188
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module OpenAPI
5
- VERSION = '0.18.3'
5
+ VERSION = '0.19.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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-openapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.3
4
+ version: 0.19.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: 2024-05-01 00:00:00.000000000 Z
12
+ date: 2025-05-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -109,7 +109,7 @@ licenses:
109
109
  metadata:
110
110
  homepage_uri: https://github.com/exoego/rspec-openapi
111
111
  source_code_uri: https://github.com/exoego/rspec-openapi
112
- changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.18.3
112
+ changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.19.0
113
113
  rubygems_mfa_required: 'true'
114
114
  post_install_message:
115
115
  rdoc_options: []
@@ -126,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
126
  - !ruby/object:Gem::Version
127
127
  version: '0'
128
128
  requirements: []
129
- rubygems_version: 3.4.6
129
+ rubygems_version: 3.4.19
130
130
  signing_key:
131
131
  specification_version: 4
132
132
  summary: Generate OpenAPI schema from RSpec request specs