rspec-openapi 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef97d7a9ebc7c50fb3cc6d6ef7d148c375a190680fa1f2a6d6adb4b4971a4106
4
- data.tar.gz: 0ca35f6a022fa7a257818b29b5e5dd35d786f2e9374e0833966756bbf1cd51e1
3
+ metadata.gz: f835fe8be895164639ec2bfdbb6414b922734cc9598b30b100ac7da15d31b98f
4
+ data.tar.gz: 60ba1234561313d8e9fbe5c1f5667e6c759d477ad124867045e3e2e516174ece
5
5
  SHA512:
6
- metadata.gz: 32ccb146dafc799631c7dc37c557d278fbe771f3f017263ff38eaf644ec69967369757f6e7aa48ff0074bff38641dd72354203a36a2f399fe80035c62c9bf85d
7
- data.tar.gz: 5932e0c01b98c0b6d662c43b8a3d26f4fa6709699f9eae0de92dc40cabdfb2b3fd8c2c65bfc65bd2a6ff75ab0edac14490f443bd182a4ca1164d039da2f9b5b2
6
+ metadata.gz: 44c56d011d95c1350eef4ed83011ff00c749cf9e83ec3cb44f605a6e3ee50c05173117d618d3472ea44b20aeef8981c17ae35f8b91220ff8891382447c1a4679
7
+ data.tar.gz: c264b04decebbcd4389633eed77cd98a0241980fa283c320e99c5bdaf5a5b5be36e242367da23abcefbf0f31802dd27f1d123324005c8234657d11328e9e1e60
@@ -20,9 +20,7 @@ jobs:
20
20
  uses: ruby/setup-ruby@v1
21
21
  with:
22
22
  ruby-version: 2.6
23
-
24
- - name: Install dependencies
25
- run: bundle install
23
+ bundler-cache: true
26
24
 
27
25
  - name: Rubocop run
28
26
  run: |
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ NewCops: enable
5
+ TargetRubyVersion: 2.5
6
+ Exclude:
7
+ - 'spec/rails/**/*'
8
+ - 'vendor/**/*'
9
+
10
+ Style/TrailingCommaInHashLiteral:
11
+ EnforcedStyleForMultiline: consistent_comma
12
+ Style/TrailingCommaInArguments:
13
+ EnforcedStyleForMultiline: consistent_comma
14
+ Style/TrailingCommaInArrayLiteral:
15
+ EnforcedStyleForMultiline: consistent_comma
16
+ Style/ClassAndModuleChildren:
17
+ EnforcedStyle: compact
18
+ Exclude:
19
+ - 'lib/rspec/openapi/version.rb'
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - 'spec/**/*'
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,41 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2023-04-14 06:22:29 UTC using RuboCop version 1.49.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 10
10
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
11
+ Metrics/AbcSize:
12
+ Max: 31
13
+
14
+ # Offense count: 4
15
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
16
+ Metrics/CyclomaticComplexity:
17
+ Max: 10
18
+
19
+ # Offense count: 13
20
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
21
+ Metrics/MethodLength:
22
+ Max: 30
23
+
24
+ # Offense count: 1
25
+ # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
26
+ # SupportedStyles: snake_case, normalcase, non_integer
27
+ # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
28
+ Naming/VariableNumber:
29
+ Exclude:
30
+ - 'spec/integration_tests/roda_test.rb'
31
+
32
+ # Offense count: 5
33
+ # Configuration parameters: AllowedConstants.
34
+ Style/Documentation:
35
+ Exclude:
36
+ - 'spec/**/*'
37
+ - 'test/**/*'
38
+ - 'lib/rspec/openapi.rb'
39
+ - 'lib/rspec/openapi/minitest_hooks.rb'
40
+ - 'lib/rspec/openapi/result_recorder.rb'
41
+ - 'lib/rspec/openapi/schema_file.rb'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## v0.8.0
2
+ - Set `required` in request body and response body
3
+ [#95](https://github.com/exoego/rspec-openapi/pull/95), [#98](https://github.com/exoego/rspec-openapi/pull/98)
4
+ - Generate OpenAPI with minitest instead of RSpec
5
+ [#90](https://github.com/exoego/rspec-openapi/pull/90)
6
+ - Generate security schemas via RSpec::OpenAPI.security_schemes
7
+ [#84](https://github.com/exoego/rspec-openapi/pull/84)
8
+ - Bunch of refactorings
9
+
1
10
  ## v0.7.2
2
11
  - $ref enhancements: Support $ref in arbitrary depth
3
12
  [#82](https://github.com/k0kubun/rspec-openapi/pull/82)
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in rspec-openapi.gemspec
@@ -12,6 +14,6 @@ group :test do
12
14
  end
13
15
 
14
16
  group :development do
17
+ gem 'code-scanning-rubocop'
15
18
  gem 'pry'
16
- gem "code-scanning-rubocop"
17
19
  end
data/README.md CHANGED
@@ -146,6 +146,16 @@ RSpec::OpenAPI.response_headers = %w[X-Cursor]
146
146
  # Set `servers` - generate servers of a schema file
147
147
  RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]
148
148
 
149
+ # Set `security_schemes` - generate security schemes
150
+ RSpec::OpenAPI.security_schemes = {
151
+ 'MyToken' => {
152
+ description: 'Authenticate API requests via a JWT',
153
+ type: 'http',
154
+ scheme: 'bearer',
155
+ bearerFormat: 'JWT',
156
+ },
157
+ }
158
+
149
159
  # Generate a comment on top of a schema file
150
160
  RSpec::OpenAPI.comment = <<~EOS
151
161
  This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi
@@ -189,7 +199,7 @@ paths:
189
199
  application/json:
190
200
  schema:
191
201
  $ref: "#/components/schemas/User"
192
- # Note) #/components/schamas is not needed to be defined.
202
+ # Note) #/components/schamas is not needed to be defined.
193
203
  ```
194
204
 
195
205
  3. Then, re-run rspec-openapi. It will generate `#/components/schemas` with the referenced schema (`User` for example) newly-generated or updated.
@@ -298,6 +308,7 @@ Some examples' attributes can be overwritten via RSpec metadata options. Example
298
308
  summary: 'list all posts',
299
309
  description: 'list all posts ordered by pub_date',
300
310
  tags: %w[v1 posts],
311
+ security: [{"MyToken" => []}],
301
312
  } do
302
313
  # ...
303
314
  end
@@ -305,6 +316,34 @@ Some examples' attributes can be overwritten via RSpec metadata options. Example
305
316
 
306
317
  **NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.
307
318
 
319
+ ## Experimental minitest support
320
+
321
+ Even if you are not using `rspec` this gem might help you with its experimental support for `minitest`.
322
+
323
+ Example:
324
+
325
+ ```rb
326
+ class TablesTest < ActionDispatch::IntegrationTest
327
+ openapi!
328
+
329
+ test "GET /index returns a list of tables" do
330
+ get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
331
+ assert_response :success
332
+ end
333
+
334
+ test "GET /index does not return tables if unauthorized" do
335
+ get '/tables'
336
+ assert_response :unauthorized
337
+ end
338
+
339
+ # ...
340
+ end
341
+ ```
342
+
343
+ It should work with both classes inheriting from `ActionDispatch::IntegrationTest` and with classes using `Rack::Test` directly, as long as you call `openapi!` in your test class.
344
+
345
+ Please note that not all features present in the rspec integration work with minitest (yet). For example, custom per test case metadata is not supported. A custom `description_builder` will not work either.
346
+
308
347
  ## Links
309
348
 
310
349
  Existing RSpec plugins which have OpenAPI integration:
@@ -315,9 +354,9 @@ Existing RSpec plugins which have OpenAPI integration:
315
354
 
316
355
  ## Acknowledgements
317
356
 
318
- This gem was heavily inspired by the following gem:
357
+ * Heavily inspired by [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
358
+ * Orignally created by [k0kubun](https://github.com/k0kubun) and the ownership was transferred to [exoego](https://github.com/exoego) in 2022-11-29.
319
359
 
320
- * [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
321
360
 
322
361
  ## License
323
362
 
data/Rakefile CHANGED
@@ -1,5 +1,7 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec) do |t|
5
7
  t.pattern = 'spec/rspec/openapi/**/*_spec.rb'
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "rspec/openapi"
4
+ require 'bundler/setup'
5
+ require 'rspec/openapi'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "rspec/openapi"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'hash_helper'
2
4
 
3
5
  class << RSpec::OpenAPI::ComponentsUpdater = Object.new
@@ -7,6 +9,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
7
9
  # Top-level schema: Used as the body of request or response
8
10
  top_level_refs = paths_to_top_level_refs(base)
9
11
  return if top_level_refs.empty?
12
+
10
13
  fresh_schemas = build_fresh_schemas(top_level_refs, base, fresh)
11
14
 
12
15
  # Nested schema: References in Top-level schemas. May contain some top-level schema.
@@ -31,7 +34,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
31
34
  RSpec::OpenAPI::SchemaMerger.merge!(fresh_schemas[schema_name], nested_schema)
32
35
  end
33
36
 
34
- RSpec::OpenAPI::SchemaMerger.merge!(base, { 'components' => { 'schemas' => fresh_schemas }})
37
+ RSpec::OpenAPI::SchemaMerger.merge!(base, { 'components' => { 'schemas' => fresh_schemas } })
35
38
  RSpec::OpenAPI::SchemaCleaner.cleanup_components_schemas!(base, { 'components' => { 'schemas' => fresh_schemas } })
36
39
  end
37
40
 
@@ -39,7 +42,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
39
42
 
40
43
  def build_fresh_schemas(references, base, fresh)
41
44
  references.inject({}) do |acc, paths|
42
- ref_link = dig_schema(base, paths).dig('$ref')
45
+ ref_link = dig_schema(base, paths)['$ref']
43
46
  schema_name = ref_link.gsub('#/components/schemas/', '')
44
47
  schema_body = dig_schema(fresh, paths)
45
48
  RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
@@ -51,8 +54,8 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
51
54
  end
52
55
 
53
56
  def paths_to_top_level_refs(base)
54
- request_bodies = RSpec::OpenAPI::HashHelper::matched_paths(base, 'paths.*.*.requestBody.content.application/json')
55
- responses = RSpec::OpenAPI::HashHelper::matched_paths(base, 'paths.*.*.responses.*.content.application/json')
57
+ request_bodies = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.requestBody.content.application/json')
58
+ responses = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.responses.*.content.application/json')
56
59
  (request_bodies + responses).select do |paths|
57
60
  dig_schema(base, paths)&.dig('$ref')&.start_with?('#/components/schemas/')
58
61
  end
@@ -61,7 +64,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
61
64
  def find_non_top_level_nested_refs(base, generated_names)
62
65
  nested_refs = [
63
66
  *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.$ref'),
64
- *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref')
67
+ *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref'),
65
68
  ]
66
69
  # Reject already-generated schemas to reduce unnecessary loop
67
70
  nested_refs.reject do |paths|
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << RSpec::OpenAPI::DefaultSchema = Object.new
2
4
  def build(title)
3
- {
5
+ spec = {
4
6
  openapi: '3.0.3',
5
7
  info: {
6
8
  title: title,
@@ -8,6 +10,14 @@ class << RSpec::OpenAPI::DefaultSchema = Object.new
8
10
  },
9
11
  servers: RSpec::OpenAPI.servers,
10
12
  paths: {},
11
- }.freeze
13
+ }
14
+
15
+ if RSpec::OpenAPI.security_schemes.present?
16
+ spec[:components] = {
17
+ securitySchemes: RSpec::OpenAPI.security_schemes,
18
+ }
19
+ end
20
+
21
+ spec.freeze
12
22
  end
13
23
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << RSpec::OpenAPI::HashHelper = Object.new
2
4
  def paths_to_all_fields(obj)
3
5
  case obj
@@ -13,12 +15,11 @@ class << RSpec::OpenAPI::HashHelper = Object.new
13
15
 
14
16
  def matched_paths(obj, selector)
15
17
  selector_parts = selector.split('.').map(&:to_s)
16
- selectors = paths_to_all_fields(obj).select do |key_parts|
18
+ paths_to_all_fields(obj).select do |key_parts|
17
19
  key_parts.size == selector_parts.size && key_parts.zip(selector_parts).all? do |kp, sp|
18
- kp == sp || (sp == '*' && kp != nil)
20
+ kp == sp || (sp == '*' && !kp.nil?)
19
21
  end
20
22
  end
21
- selectors
22
23
  end
23
24
 
24
25
  def matched_paths_deeply_nested(obj, begin_selector, end_selector)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest'
4
+
5
+ module RSpec::OpenAPI::Minitest
6
+ Example = Struct.new(:context, :description, :metadata, :file_path)
7
+
8
+ module TestPatch
9
+ def self.prepended(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ def run(*args)
14
+ result = super
15
+ if ENV['OPENAPI'] && self.class.openapi?
16
+ file_path = method(name).source_location.first
17
+ human_name = name.sub(/^test_/, '').gsub(/_/, ' ')
18
+ example = Example.new(self, human_name, {}, file_path)
19
+ path = RSpec::OpenAPI.path.yield_self { |p| p.is_a?(Proc) ? p.call(example) : p }
20
+ record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
21
+ RSpec::OpenAPI.path_records[path] << record if record
22
+ end
23
+ result
24
+ end
25
+
26
+ module ClassMethods
27
+ def openapi?
28
+ @openapi
29
+ end
30
+
31
+ def openapi!
32
+ @openapi = true
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ Minitest::Test.prepend RSpec::OpenAPI::Minitest::TestPatch
39
+
40
+ Minitest.after_run do
41
+ if ENV['OPENAPI']
42
+ result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
43
+ result_recorder.record_results!
44
+ puts result_record.error_message if result_recorder.errors?
45
+ end
46
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  RSpec::OpenAPI::Record = Struct.new(
2
4
  :http_method, # @param [String] - "GET"
3
5
  :path, # @param [String] - "/v1/status/:id"
@@ -9,6 +11,7 @@ RSpec::OpenAPI::Record = Struct.new(
9
11
  :summary, # @param [String] - "v1/statuses #show"
10
12
  :tags, # @param [Array] - ["Status"]
11
13
  :description, # @param [String] - "returns a status"
14
+ :security, # @param [Array] - [{securityScheme1: []}]
12
15
  :status, # @param [Integer] - 200
13
16
  :response_body, # @param [Object] - {"status" => "ok"}
14
17
  :response_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'action_dispatch'
2
4
  require 'rspec/openapi/record'
3
5
 
@@ -6,71 +8,90 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
6
8
  # @param [RSpec::Core::Example] example
7
9
  # @return [RSpec::OpenAPI::Record,nil]
8
10
  def build(context, example:)
9
- if rack_test?(context)
10
- request = ActionDispatch::Request.new(context.last_request.env)
11
- request.body.rewind if request.body.respond_to?(:rewind)
12
- response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
13
- else
14
- request = context.request
15
- response = context.response
16
- end
11
+ request, response = extract_request_response(context)
17
12
  return if request.nil?
18
13
 
19
- # Generate `path` and `summary` in a framework-friendly manner when possible
20
- if rails?
21
- route = find_rails_route(request)
22
- path = route.path.spec.to_s.delete_suffix('(.:format)')
23
- summary = route.requirements[:action] || "#{request.method} #{path}"
24
- tags = [route.requirements[:controller]&.classify].compact
25
- else
26
- path = request.path
27
- summary = "#{request.method} #{request.path}"
28
- end
14
+ path, summary, tags, raw_path_params, description, security = extract_request_attributes(request, example)
29
15
 
30
- response_body =
31
- begin
32
- response.parsed_body
33
- rescue JSON::ParserError
34
- nil
35
- end
36
-
37
- request_headers = RSpec::OpenAPI.request_headers.each_with_object([]) do |header, headers_arr|
38
- header_key = header.gsub(/-/, '_').upcase
39
- header_value = request.get_header(['HTTP', header_key].join('_')) || request.get_header(header_key)
40
- headers_arr << [header, header_value] if header_value
41
- end
42
-
43
- metadata_options = example.metadata[:openapi] || {}
44
-
45
- response_headers = RSpec::OpenAPI.response_headers.each_with_object([]) do |header, headers_arr|
46
- header_key = header
47
- header_value = response.headers[header_key]
48
- headers_arr << [header_key, header_value] if header_value
49
- end
16
+ request_headers, response_headers = extract_headers(request, response)
50
17
 
51
18
  RSpec::OpenAPI::Record.new(
52
19
  http_method: request.method,
53
20
  path: path,
54
- path_params: raw_path_params(request),
21
+ path_params: raw_path_params,
55
22
  query_params: request.query_parameters,
56
23
  request_params: raw_request_params(request),
57
24
  request_content_type: request.media_type,
58
25
  request_headers: request_headers,
59
- summary: metadata_options[:summary] || summary,
60
- tags: metadata_options[:tags] || tags,
61
- description: metadata_options[:description] || RSpec::OpenAPI.description_builder.call(example),
26
+ summary: summary,
27
+ tags: tags,
28
+ description: description,
29
+ security: security,
62
30
  status: response.status,
63
- response_body: response_body,
31
+ response_body: safe_parse_body(response),
64
32
  response_headers: response_headers,
65
33
  response_content_type: response.media_type,
66
- response_content_disposition: response.header["Content-Disposition"],
34
+ response_content_disposition: response.header['Content-Disposition'],
67
35
  ).freeze
68
36
  end
69
37
 
70
38
  private
71
39
 
40
+ def safe_parse_body(response)
41
+ response.parsed_body
42
+ rescue JSON::ParserError
43
+ nil
44
+ end
45
+
46
+ def extract_headers(request, response)
47
+ request_headers = RSpec::OpenAPI.request_headers.each_with_object([]) do |header, headers_arr|
48
+ header_key = header.gsub(/-/, '_').upcase
49
+ header_value = request.get_header(['HTTP', header_key].join('_')) || request.get_header(header_key)
50
+ headers_arr << [header, header_value] if header_value
51
+ end
52
+ response_headers = RSpec::OpenAPI.response_headers.each_with_object([]) do |header, headers_arr|
53
+ header_key = header
54
+ header_value = response.headers[header_key]
55
+ headers_arr << [header_key, header_value] if header_value
56
+ end
57
+ [request_headers, response_headers]
58
+ end
59
+
60
+ def extract_request_attributes(request, example)
61
+ metadata = example.metadata[:openapi] || {}
62
+ summary = metadata[:summary]
63
+ tags = metadata[:tags]
64
+ security = metadata[:security]
65
+ description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
66
+ raw_path_params = request.path_parameters
67
+ path = request.path
68
+ if rails?
69
+ route = find_rails_route(request)
70
+ path = route.path.spec.to_s.delete_suffix('(.:format)')
71
+ summary ||= route.requirements[:action]
72
+ tags ||= [route.requirements[:controller]&.classify].compact
73
+ # :controller and :action always exist. :format is added when routes is configured as such.
74
+ # TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x
75
+ raw_path_params = raw_path_params.slice(*(raw_path_params.keys - %i[controller action format]))
76
+ end
77
+ summary ||= "#{request.method} #{path}"
78
+ [path, summary, tags, raw_path_params, description, security]
79
+ end
80
+
81
+ def extract_request_response(context)
82
+ if rack_test?(context)
83
+ request = ActionDispatch::Request.new(context.last_request.env)
84
+ request.body.rewind if request.body.respond_to?(:rewind)
85
+ response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
86
+ else
87
+ request = context.request
88
+ response = context.response
89
+ end
90
+ [request, response]
91
+ end
92
+
72
93
  def rails?
73
- defined?(Rails) && Rails.application
94
+ defined?(Rails) && Rails.respond_to?(:application) && Rails.application
74
95
  end
75
96
 
76
97
  def rack_test?(context)
@@ -87,25 +108,12 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
87
108
 
88
109
  app.routes.router.recognize(request) do |route|
89
110
  if route.app.matches?(request)
90
- if route.app.engine?
91
- return find_rails_route(request, app: route.app.app, fix_path: false)
92
- else
93
- return route
94
- end
95
- end
96
- end
97
- raise "No route matched for #{request.request_method} #{request.path_info}"
98
- end
111
+ return find_rails_route(request, app: route.app.app, fix_path: false) if route.app.engine?
99
112
 
100
- # :controller and :action always exist. :format is added when routes is configured as such.
101
- def raw_path_params(request)
102
- if rails?
103
- request.path_parameters.reject do |key, _value|
104
- %i[controller action format].include?(key)
113
+ return route
105
114
  end
106
- else
107
- request.path_parameters
108
115
  end
116
+ raise "No route matched for #{request.request_method} #{request.path_info}"
109
117
  end
110
118
 
111
119
  # workaround to get real request parameters
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RSpec::OpenAPI::ResultRecorder
4
+ def initialize(path_records)
5
+ @path_records = path_records
6
+ @error_records = {}
7
+ end
8
+
9
+ def record_results!
10
+ title = File.basename(Dir.pwd)
11
+ @path_records.each do |path, records|
12
+ RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
13
+ schema = RSpec::OpenAPI::DefaultSchema.build(title)
14
+ schema[:info].merge!(RSpec::OpenAPI.info)
15
+ RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
16
+ new_from_zero = {}
17
+ records.each do |record|
18
+ File.open('/tmp/records', 'a') { |f| f.puts record.to_yaml }
19
+ begin
20
+ record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
21
+ RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
22
+ RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
23
+ rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
24
+ @error_records[e] = record # Avoid failing the build
25
+ end
26
+ end
27
+ RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
28
+ RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
29
+ end
30
+ end
31
+ end
32
+
33
+ def errors?
34
+ @error_records.any?
35
+ end
36
+
37
+ def error_message
38
+ <<~ERR_MSG
39
+ RSpec::OpenAPI got errors building #{@error_records.size} requests
40
+
41
+ #{@error_records.map { |e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
42
+ ERR_MSG
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+
5
+ RSpec.configuration.after(:each) do |example|
6
+ if RSpec::OpenAPI.example_types.include?(example.metadata[:type]) && example.metadata[:openapi] != false
7
+ path = RSpec::OpenAPI.path.yield_self { |p| p.is_a?(Proc) ? p.call(example) : p }
8
+ record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
9
+ RSpec::OpenAPI.path_records[path] << record if record
10
+ end
11
+ end
12
+
13
+ RSpec.configuration.after(:suite) do
14
+ result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
15
+ result_recorder.record_results!
16
+ if result_recorder.errors?
17
+ error_message = result_recorder.error_message
18
+ colorizer = RSpec::Core::Formatters::ConsoleCodes
19
+ RSpec.configuration.reporter.message colorizer.wrap(error_message, :failure)
20
+ end
21
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << RSpec::OpenAPI::SchemaBuilder = Object.new
2
4
  # @param [RSpec::OpenAPI::Record] record
3
5
  # @return [Hash]
@@ -25,6 +27,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
25
27
  record.http_method.downcase => {
26
28
  summary: record.summary,
27
29
  tags: record.tags,
30
+ security: record.security,
28
31
  parameters: build_parameters(record),
29
32
  requestBody: build_request_body(record),
30
33
  responses: {
@@ -38,6 +41,11 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
38
41
 
39
42
  private
40
43
 
44
+ def enrich_with_required_keys(obj)
45
+ obj[:required] = obj[:properties]&.keys
46
+ obj
47
+ end
48
+
41
49
  def response_example(record, disposition:)
42
50
  return nil if !example_enabled? || disposition
43
51
 
@@ -81,6 +89,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
81
89
  end
82
90
 
83
91
  return nil if parameters.empty?
92
+
84
93
  parameters
85
94
  end
86
95
 
@@ -115,8 +124,8 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
115
124
  normalize_content_type(record.request_content_type) => {
116
125
  schema: build_property(record.request_params),
117
126
  example: (build_example(record.request_params) if example_enabled?),
118
- }.compact
119
- }
127
+ }.compact,
128
+ },
120
129
  }
121
130
  end
122
131
 
@@ -125,17 +134,18 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
125
134
 
126
135
  case value
127
136
  when Array
128
- if value.empty?
129
- property[:items] = {} # unknown
130
- else
131
- property[:items] = build_property(value.first)
132
- end
137
+ property[:items] = if value.empty?
138
+ {} # unknown
139
+ else
140
+ build_property(value.first)
141
+ end
133
142
  when Hash
134
143
  property[:properties] = {}.tap do |properties|
135
144
  value.each do |key, v|
136
145
  properties[key] = build_property(v)
137
146
  end
138
147
  end
148
+ property = enrich_with_required_keys(property)
139
149
  end
140
150
  property
141
151
  end
@@ -167,15 +177,14 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
167
177
 
168
178
  # Convert an always-String param to an appropriate type
169
179
  def try_cast(value)
170
- begin
171
- Integer(value)
172
- rescue TypeError, ArgumentError
173
- value
174
- end
180
+ Integer(value)
181
+ rescue TypeError, ArgumentError
182
+ value
175
183
  end
176
184
 
177
185
  def build_example(value)
178
186
  return nil if value.nil?
187
+
179
188
  value = value.dup
180
189
  adjust_params(value)
181
190
  end
@@ -191,7 +200,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
191
200
  end
192
201
 
193
202
  def normalize_path(path)
194
- path.gsub(%r|/:([^:/]+)|, '/{\1}')
203
+ path.gsub(%r{/:([^:/]+)}, '/{\1}')
195
204
  end
196
205
 
197
206
  def normalize_content_type(content_type)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # For Ruby 3.0+
2
4
  require 'set'
3
5
 
@@ -44,15 +46,14 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
44
46
  Marshal.dump(slice(obj, fields_for_identity))
45
47
  end
46
48
 
47
- RSpec::OpenAPI::HashHelper::matched_paths(base, selector).each do |paths|
49
+ RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
48
50
  target_array = base.dig(*paths)
49
51
  spec_array = spec.dig(*paths)
50
- unless target_array.is_a?(Array) && spec_array.is_a?(Array)
51
- next
52
- end
52
+ next unless target_array.is_a?(Array) && spec_array.is_a?(Array)
53
+
53
54
  spec_identities = Set.new(spec_array.map(&marshal))
54
55
  target_array.select! { |e| spec_identities.include?(marshal.call(e)) }
55
- target_array.sort_by! { |param| fields_for_identity.map {|f| param[f] }.join('-') }
56
+ target_array.sort_by! { |param| fields_for_identity.map { |f| param[f] }.join('-') }
56
57
  # Keep the last duplicate to produce the result stably
57
58
  deduplicated = target_array.reverse.uniq { |param| slice(param, fields_for_identity) }.reverse
58
59
  target_array.replace(deduplicated)
@@ -61,7 +62,7 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
61
62
  end
62
63
 
63
64
  def cleanup_hash!(base, spec, selector)
64
- RSpec::OpenAPI::HashHelper::matched_paths(base, selector).each do |paths|
65
+ RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
65
66
  exist_in_base = !base.dig(*paths).nil?
66
67
  not_in_spec = spec.dig(*paths).nil?
67
68
  if exist_in_base && not_in_spec
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fileutils'
2
4
  require 'yaml'
3
5
  require 'json'
@@ -21,7 +23,8 @@ class RSpec::OpenAPI::SchemaFile
21
23
  # @return [Hash]
22
24
  def read
23
25
  return {} unless File.exist?(@path)
24
- YAML.load(File.read(@path)) # this can also parse JSON
26
+
27
+ YAML.safe_load(File.read(@path)) # this can also parse JSON
25
28
  end
26
29
 
27
30
  # @param [Hash] spec
@@ -40,9 +43,7 @@ class RSpec::OpenAPI::SchemaFile
40
43
  return content if RSpec::OpenAPI.comment.nil?
41
44
 
42
45
  comment = RSpec::OpenAPI.comment.dup
43
- unless comment.end_with?("\n")
44
- comment << "\n"
45
- end
46
+ comment << "\n" unless comment.end_with?("\n")
46
47
  "#{comment.gsub(/^/, '# ').gsub(/^# \n/, "#\n")}#{content}"
47
48
  end
48
49
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << RSpec::OpenAPI::SchemaMerger = Object.new
2
4
  # @param [Hash] base
3
5
  # @param [Hash] spec
@@ -22,28 +24,35 @@ class << RSpec::OpenAPI::SchemaMerger = Object.new
22
24
  end
23
25
 
24
26
  # Not doing `base.replace(deep_merge(base, spec))` to preserve key orders.
25
- # Also this needs to be aware of OpenAPI details because a Hash-like structure
27
+ # Also this needs to be aware of OpenAPI details because a Hash-like structure
26
28
  # may be an array whose Hash elements have a key name.
27
29
  #
28
30
  # TODO: Should we probably force-merge `summary` regardless of manual modifications?
29
31
  def merge_schema!(base, spec)
30
32
  spec.each do |key, value|
31
33
  if base[key].is_a?(Hash) && value.is_a?(Hash)
32
- if !base[key].key?("$ref")
33
- merge_schema!(base[key], value)
34
- end
34
+ merge_schema!(base[key], value) unless base[key].key?('$ref')
35
35
  elsif base[key].is_a?(Array) && value.is_a?(Array)
36
36
  # parameters need to be merged as if `name` and `in` were the Hash keys.
37
- if key == 'parameters'
38
- base[key] = value | base[key]
39
- base[key].uniq! { |param| param.slice('name', 'in') }
40
- else
41
- base[key] = value
42
- end
37
+ merge_arrays(base, key, value)
43
38
  else
44
39
  base[key] = value
45
40
  end
46
41
  end
47
42
  base
48
43
  end
44
+
45
+ def merge_arrays(base, key, value)
46
+ case key
47
+ when 'parameters'
48
+ base[key] = value | base[key]
49
+ base[key].uniq! { |param| param.slice('name', 'in') }
50
+ when 'required'
51
+ # Preserve properties that appears in all test cases
52
+ base[key] = value & base[key]
53
+ else
54
+ # last one wins
55
+ base[key] = value
56
+ end
57
+ end
49
58
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RSpec
2
4
  module OpenAPI
3
- VERSION = '0.7.2'.freeze
5
+ VERSION = '0.8.0'
4
6
  end
5
7
  end
data/lib/rspec/openapi.rb CHANGED
@@ -1,17 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec/openapi/version'
2
- require 'rspec/openapi/hooks' if ENV['OPENAPI']
4
+ require 'rspec/openapi/components_updater'
5
+ require 'rspec/openapi/default_schema'
6
+ require 'rspec/openapi/record_builder'
7
+ require 'rspec/openapi/result_recorder'
8
+ require 'rspec/openapi/schema_builder'
9
+ require 'rspec/openapi/schema_file'
10
+ require 'rspec/openapi/schema_merger'
11
+ require 'rspec/openapi/schema_cleaner'
12
+
13
+ if ENV['OPENAPI']
14
+ require 'rspec/openapi/minitest_hooks'
15
+ require 'rspec/openapi/rspec_hooks'
16
+ end
3
17
 
4
18
  module RSpec::OpenAPI
5
19
  @path = 'doc/openapi.yaml'
6
20
  @comment = nil
7
21
  @enable_example = true
8
- @description_builder = -> (example) { example.description }
22
+ @description_builder = ->(example) { example.description }
9
23
  @info = {}
10
24
  @application_version = '1.0.0'
11
25
  @request_headers = []
12
26
  @servers = []
27
+ @security_schemes = []
13
28
  @example_types = %i[request]
14
29
  @response_headers = []
30
+ @path_records = Hash.new { |h, k| h[k] = [] }
15
31
 
16
32
  class << self
17
33
  attr_accessor :path,
@@ -22,7 +38,9 @@ module RSpec::OpenAPI
22
38
  :application_version,
23
39
  :request_headers,
24
40
  :servers,
41
+ :security_schemes,
25
42
  :example_types,
26
- :response_headers
43
+ :response_headers,
44
+ :path_records
27
45
  end
28
46
  end
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'lib/rspec/openapi/version'
2
4
 
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = 'rspec-openapi'
5
7
  spec.version = RSpec::OpenAPI::VERSION
6
- spec.authors = ['Takashi Kokubun']
7
- spec.email = ['takashikkbn@gmail.com']
8
+ spec.authors = ['Takashi Kokubun', 'TATSUNO Yasuhiro']
9
+ spec.email = ['takashikkbn@gmail.com', 'ytatsuno.jp@gmail.com']
8
10
 
9
- spec.summary = %q{Generate OpenAPI schema from RSpec request specs}
10
- spec.description = %q{Generate OpenAPI from RSpec request specs}
11
- spec.homepage = 'https://github.com/k0kubun/rspec-openapi'
11
+ spec.summary = 'Generate OpenAPI schema from RSpec request specs'
12
+ spec.description = 'Generate OpenAPI from RSpec request specs'
13
+ spec.homepage = 'https://github.com/exoego/rspec-openapi'
12
14
  spec.license = 'MIT'
13
15
  spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
14
16
 
@@ -16,7 +18,7 @@ Gem::Specification.new do |spec|
16
18
  spec.metadata['source_code_uri'] = spec.homepage
17
19
  spec.metadata['changelog_uri'] = File.join(spec.homepage, 'blob/master/CHANGELOG.md')
18
20
 
19
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
22
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
23
  end
22
24
  spec.bindir = 'exe'
@@ -24,5 +26,6 @@ Gem::Specification.new do |spec|
24
26
  spec.require_paths = ['lib']
25
27
 
26
28
  spec.add_dependency 'actionpack', '>= 5.2.0'
27
- spec.add_dependency 'rspec'
29
+ spec.add_dependency 'rspec-core'
30
+ spec.metadata['rubygems_mfa_required'] = 'true'
28
31
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-openapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
8
- autorequire:
8
+ - TATSUNO Yasuhiro
9
+ autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2022-11-09 00:00:00.000000000 Z
12
+ date: 2023-04-20 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: actionpack
@@ -25,7 +26,7 @@ dependencies:
25
26
  - !ruby/object:Gem::Version
26
27
  version: 5.2.0
27
28
  - !ruby/object:Gem::Dependency
28
- name: rspec
29
+ name: rspec-core
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
32
  - - ">="
@@ -38,19 +39,21 @@ dependencies:
38
39
  - - ">="
39
40
  - !ruby/object:Gem::Version
40
41
  version: '0'
41
- description: Generate OpenAPI from RSpec request specs
42
+ description: Generate OpenAPI from RSpec request specs
42
43
  email:
43
44
  - takashikkbn@gmail.com
45
+ - ytatsuno.jp@gmail.com
44
46
  executables: []
45
47
  extensions: []
46
48
  extra_rdoc_files: []
47
49
  files:
48
- - ".github/FUNDING.yml"
49
50
  - ".github/workflows/codeql-analysis.yml"
50
51
  - ".github/workflows/rubocop.yml"
51
52
  - ".github/workflows/test.yml"
52
53
  - ".gitignore"
53
54
  - ".rspec"
55
+ - ".rubocop.yml"
56
+ - ".rubocop_todo.yml"
54
57
  - CHANGELOG.md
55
58
  - Gemfile
56
59
  - LICENSE.txt
@@ -62,9 +65,11 @@ files:
62
65
  - lib/rspec/openapi/components_updater.rb
63
66
  - lib/rspec/openapi/default_schema.rb
64
67
  - lib/rspec/openapi/hash_helper.rb
65
- - lib/rspec/openapi/hooks.rb
68
+ - lib/rspec/openapi/minitest_hooks.rb
66
69
  - lib/rspec/openapi/record.rb
67
70
  - lib/rspec/openapi/record_builder.rb
71
+ - lib/rspec/openapi/result_recorder.rb
72
+ - lib/rspec/openapi/rspec_hooks.rb
68
73
  - lib/rspec/openapi/schema_builder.rb
69
74
  - lib/rspec/openapi/schema_cleaner.rb
70
75
  - lib/rspec/openapi/schema_file.rb
@@ -72,14 +77,15 @@ files:
72
77
  - lib/rspec/openapi/version.rb
73
78
  - rspec-openapi.gemspec
74
79
  - test.png
75
- homepage: https://github.com/k0kubun/rspec-openapi
80
+ homepage: https://github.com/exoego/rspec-openapi
76
81
  licenses:
77
82
  - MIT
78
83
  metadata:
79
- homepage_uri: https://github.com/k0kubun/rspec-openapi
80
- source_code_uri: https://github.com/k0kubun/rspec-openapi
81
- changelog_uri: https://github.com/k0kubun/rspec-openapi/blob/master/CHANGELOG.md
82
- post_install_message:
84
+ homepage_uri: https://github.com/exoego/rspec-openapi
85
+ source_code_uri: https://github.com/exoego/rspec-openapi
86
+ changelog_uri: https://github.com/exoego/rspec-openapi/blob/master/CHANGELOG.md
87
+ rubygems_mfa_required: 'true'
88
+ post_install_message:
83
89
  rdoc_options: []
84
90
  require_paths:
85
91
  - lib
@@ -94,8 +100,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
100
  - !ruby/object:Gem::Version
95
101
  version: '0'
96
102
  requirements: []
97
- rubygems_version: 3.3.24
98
- signing_key:
103
+ rubygems_version: 3.4.6
104
+ signing_key:
99
105
  specification_version: 4
100
106
  summary: Generate OpenAPI schema from RSpec request specs
101
107
  test_files: []
data/.github/FUNDING.yml DELETED
@@ -1 +0,0 @@
1
- github: k0kubun
@@ -1,51 +0,0 @@
1
- require 'rspec'
2
- require 'rspec/openapi/components_updater'
3
- require 'rspec/openapi/default_schema'
4
- require 'rspec/openapi/record_builder'
5
- require 'rspec/openapi/schema_builder'
6
- require 'rspec/openapi/schema_file'
7
- require 'rspec/openapi/schema_merger'
8
- require 'rspec/openapi/schema_cleaner'
9
-
10
- path_records = Hash.new { |h, k| h[k] = [] }
11
- error_records = {}
12
-
13
- RSpec.configuration.after(:each) do |example|
14
- if RSpec::OpenAPI.example_types.include?(example.metadata[:type]) && example.metadata[:openapi] != false
15
- path = RSpec::OpenAPI.path.yield_self { |p| p.is_a?(Proc) ? p.call(example) : p }
16
- record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
17
- path_records[path] << record if record
18
- end
19
- end
20
-
21
- RSpec.configuration.after(:suite) do
22
- title = File.basename(Dir.pwd)
23
- path_records.each do |path, records|
24
- RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
25
- schema = RSpec::OpenAPI::DefaultSchema.build(title)
26
- schema[:info].merge!(RSpec::OpenAPI.info)
27
- RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
28
- new_from_zero = {}
29
- records.each do |record|
30
- begin
31
- record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
32
- RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
33
- RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
34
- rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
35
- error_records[e] = record # Avoid failing the build
36
- end
37
- end
38
- RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
39
- RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
40
- end
41
- end
42
- if error_records.any?
43
- error_message = <<~EOS
44
- RSpec::OpenAPI got errors building #{error_records.size} requests
45
-
46
- #{error_records.map {|e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
47
- EOS
48
- colorizer = ::RSpec::Core::Formatters::ConsoleCodes
49
- RSpec.configuration.reporter.message colorizer.wrap(error_message, :failure)
50
- end
51
- end