rspec-openapi 0.25.1 → 0.26.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: fdb09465b214320cfd32bf020363827d17c3346d48cb7e724effc99df6bdf722
4
- data.tar.gz: 9947b0df4b2af301473ff89cd52cd035d8a4cb129bd83f8aacef2fc4df107cd5
3
+ metadata.gz: 254e5e91a3965a417ad69c8b61d4bfdde6af42ab7aba14854d5531d73f4a793d
4
+ data.tar.gz: 3fa43709cf02b95bb1f4815eb4eebefde3ad5e5e2aabf0439267dce2c06cba36
5
5
  SHA512:
6
- metadata.gz: f17329553b8d2a02b196faba4ac592826557acb79648f364994614759e762608241d1310be6080873703af3b7f45d111db3509302f9ec22270c6c3c849bbd180
7
- data.tar.gz: dd616bc6570e99d2b6439d4003bcadcdc9881613c6f4ba0428191ceef2c7c73dd185c5e385ea6c8af271f98d5c67d3e597062052d2c3bd7c59d1c0f6e75f0deb
6
+ metadata.gz: 486180ee4a5b04de2f3539864e6f5be519ea262f0b30afc661377aa2da69c9c6fba883128ff02c006a9b073766848fc8b2ef5ad99cbb2fc55f8201de1a5975de
7
+ data.tar.gz: a074396a8847d7d9950fc414591e68da5c1def3121fdae896aa6e626e512c5860dc58b5be83248979bb6612628f224d367d066a8d5939991fbe98f8000626934
@@ -4,7 +4,7 @@ on:
4
4
  workflow_dispatch:
5
5
  inputs:
6
6
  version:
7
- description: 'Version to release (e.g. 0.25.2 or 0.26.0)'
7
+ description: 'Version to release (e.g. 0.26.1 or 0.27.0)'
8
8
  required: true
9
9
 
10
10
  jobs:
@@ -39,7 +39,7 @@ jobs:
39
39
  - uses: rubygems/release-gem@v1
40
40
 
41
41
  - name: Create GitHub release
42
- uses: softprops/action-gh-release@v2
42
+ uses: softprops/action-gh-release@v3
43
43
  with:
44
44
  tag_name: ${{ github.ref_name }}
45
45
  name: ${{ github.ref_name }}
@@ -0,0 +1,21 @@
1
+ name: validate-openapi
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ types:
9
+ - opened
10
+ - synchronize
11
+ - reopened
12
+
13
+ jobs:
14
+ redocly-lint:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '22'
21
+ - run: npx --yes @redocly/cli@2.30.4 lint
data/.gitignore CHANGED
@@ -14,3 +14,5 @@
14
14
  .rspec_status
15
15
 
16
16
  spec/apps/rails/log/
17
+
18
+ /mise.toml
data/.rubocop_todo.yml CHANGED
@@ -24,7 +24,7 @@ Metrics/CyclomaticComplexity:
24
24
  # Offense count: 22
25
25
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
26
26
  Metrics/MethodLength:
27
- Max: 36
27
+ Max: 37
28
28
 
29
29
  # Offense count: 5
30
30
  # Configuration parameters: AllowedMethods, AllowedPatterns.
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
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
-
4
3
  Generate OpenAPI schema from RSpec request specs.
5
4
 
6
5
  ## What's this?
@@ -8,7 +7,8 @@ Generate OpenAPI schema from RSpec request specs.
8
7
  There are some gems which generate OpenAPI specs from RSpec request specs.
9
8
  However, they require a special DSL specific to these gems, and we can't reuse existing request specs as they are.
10
9
 
11
- Unlike such [existing gems](#links), rspec-openapi can generate OpenAPI specs from request specs without requiring any special DSL.
10
+ Unlike such [existing gems](#links), rspec-openapi can generate OpenAPI specs from request specs without requiring any
11
+ special DSL.
12
12
  Furthermore, rspec-openapi keeps manual modifications when it merges automated changes to OpenAPI specs
13
13
  in case we can't generate everything from request specs.
14
14
 
@@ -30,7 +30,9 @@ $ OPENAPI=1 bundle exec rspec
30
30
 
31
31
  ### Example
32
32
 
33
- Let's say you have [a request spec](https://github.com/exoego/rspec-openapi/blob/24e5c567c2e90945c7a41f19f71634ac028cc314/spec/requests/rails_spec.rb#L38) like this:
33
+ Let's say you
34
+ have [a request spec](https://github.com/exoego/rspec-openapi/blob/24e5c567c2e90945c7a41f19f71634ac028cc314/spec/requests/rails_spec.rb#L38)
35
+ like this:
34
36
 
35
37
  ```rb
36
38
  RSpec.describe 'Tables', type: :request do
@@ -67,18 +69,18 @@ paths:
67
69
  get:
68
70
  summary: index
69
71
  tags:
70
- - Table
72
+ - Table
71
73
  parameters:
72
- - name: page
73
- in: query
74
- schema:
75
- type: integer
76
- example: 1
77
- - name: per
78
- in: query
79
- schema:
80
- type: integer
81
- example: 10
74
+ - name: page
75
+ in: query
76
+ schema:
77
+ type: integer
78
+ example: 1
79
+ - name: per
80
+ in: query
81
+ schema:
82
+ type: integer
83
+ example: 10
82
84
  responses:
83
85
  '200':
84
86
  description: returns a list of tables
@@ -96,11 +98,11 @@ paths:
96
98
  # ...
97
99
  ```
98
100
 
99
- and the schema file can be used as an input of [Swagger UI](https://github.com/swagger-api/swagger-ui) or [Redoc](https://github.com/Redocly/redoc).
101
+ and the schema file can be used as an input of [Swagger UI](https://github.com/swagger-api/swagger-ui)
102
+ or [Redoc](https://github.com/Redocly/redoc).
100
103
 
101
104
  ![Redoc example](./spec/apps/rails/doc/screenshot.png)
102
105
 
103
-
104
106
  ### Configuration
105
107
 
106
108
  The following configurations are optional.
@@ -251,7 +253,8 @@ paths:
251
253
  # Note) #/components/schemas is not needed to be defined.
252
254
  ```
253
255
 
254
- 3. Then, re-run rspec-openapi. It will generate `#/components/schemas` with the referenced schema (`User` for example) newly-generated or updated.
256
+ 3. Then, re-run rspec-openapi. It will generate `#/components/schemas` with the referenced schema (`User` for example)
257
+ newly-generated or updated.
255
258
 
256
259
  ```yaml
257
260
  paths:
@@ -354,21 +357,22 @@ Some examples' attributes can be overwritten via RSpec metadata options. Example
354
357
 
355
358
  ```rb
356
359
  describe 'GET /api/v1/posts', openapi: {
357
- summary: 'list all posts',
358
- description: 'list all posts ordered by pub_date',
359
- tags: %w[v1 posts],
360
- required_request_params: %w[limit],
361
- security: [{"MyToken" => []}],
362
- } do
363
- # ...
364
- end
360
+ summary: 'list all posts',
361
+ description: 'list all posts ordered by pub_date',
362
+ tags: %w[v1 posts],
363
+ required_request_params: %w[limit],
364
+ security: [{ "MyToken" => [] }],
365
+ } do
366
+ # ...
367
+ end
365
368
  ```
366
369
 
367
370
  **NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.
368
371
 
369
372
  ### Enum Support
370
373
 
371
- You can specify enum values for string properties that should have a fixed set of allowed values. Since enums cannot be reliably inferred from test data, you can define them via the `enum` metadata option:
374
+ You can specify enum values for string properties that should have a fixed set of allowed values. Since enums cannot be
375
+ reliably inferred from test data, you can define them via the `enum` metadata option:
372
376
 
373
377
  ```rb
374
378
  it 'returns user status', openapi: {
@@ -431,7 +435,8 @@ end
431
435
 
432
436
  #### Request vs Response Enums
433
437
 
434
- By default, `enum` applies to both request and response bodies. If you need different enum values for request and response, use `request_enum` and `response_enum`:
438
+ By default, `enum` applies to both request and response bodies. If you need different enum values for request and
439
+ response, use `request_enum` and `response_enum`:
435
440
 
436
441
  ```rb
437
442
  it 'creates a task', openapi: {
@@ -447,6 +452,173 @@ it 'creates a task', openapi: {
447
452
  end
448
453
  ```
449
454
 
455
+ ### Dynamic-key Object Support (`additionalProperties`)
456
+
457
+ When an endpoint returns (or accepts) an object whose keys are not part of its
458
+ schema — for example a permissions map `{ "can_edit": true, "can_delete": false }`
459
+ where new permissions can appear without an API change — rspec-openapi would
460
+ otherwise capture the test response literally and emit each observed key as a
461
+ fixed property. You can override this with the `additional_properties` metadata,
462
+ which replaces the captured `properties` / `required` of the matched object with
463
+ [`additionalProperties`](https://swagger.io/docs/specification/data-models/dictionaries/).
464
+
465
+ Keys are dot-notation paths (the same scheme as `enum`); the empty string `''`
466
+ targets the body root.
467
+
468
+ ```rb
469
+ it 'returns a permission map', openapi: {
470
+ additional_properties: { 'data' => { type: 'boolean' } },
471
+ } do
472
+ get '/api/v1/permissions'
473
+ # Response: { "data": { "can_edit": true, "can_delete": false } }
474
+ expect(response.status).to eq(200)
475
+ end
476
+ ```
477
+
478
+ This generates:
479
+
480
+ ```yaml
481
+ schema:
482
+ type: object
483
+ properties:
484
+ data:
485
+ type: object
486
+ additionalProperties:
487
+ type: boolean
488
+ required:
489
+ - data
490
+ ```
491
+
492
+ #### Root-level dynamic keys
493
+
494
+ When the entire body is a dynamic dict, use `''` as the path:
495
+
496
+ ```rb
497
+ it 'lists organisation memberships', openapi: {
498
+ additional_properties: { '' => { type: 'boolean' } },
499
+ } do
500
+ get '/api/v1/organisations/acme/check_memberships'
501
+ # Response: { "user-hash-a": true, "user-hash-b": false }
502
+ expect(response.status).to eq(200)
503
+ end
504
+ ```
505
+
506
+ ```yaml
507
+ schema:
508
+ type: object
509
+ additionalProperties:
510
+ type: boolean
511
+ ```
512
+
513
+ #### Schemas referenced via `$ref`
514
+
515
+ The value of `additionalProperties` can be any schema, including a `$ref`:
516
+
517
+ ```rb
518
+ it 'returns tags by id', openapi: {
519
+ additional_properties: { '' => { '$ref' => '#/components/schemas/Tag' } },
520
+ } do
521
+ get '/tags'
522
+ expect(response.status).to eq(200)
523
+ end
524
+ ```
525
+
526
+ ```yaml
527
+ schema:
528
+ type: object
529
+ additionalProperties:
530
+ $ref: '#/components/schemas/Tag'
531
+ ```
532
+
533
+ #### Request vs Response
534
+
535
+ `additional_properties` applies to both request and response by default. Use
536
+ `request_additional_properties` / `response_additional_properties` to scope to
537
+ one side:
538
+
539
+ ```rb
540
+ it 'records arbitrary metrics', openapi: {
541
+ request_additional_properties: { '' => { type: 'integer' } },
542
+ } do
543
+ post '/metrics', params: { 'page_views' => 100, 'signups' => 5 }
544
+ expect(response.status).to eq(201)
545
+ end
546
+ ```
547
+
548
+ #### Forbidding extras with `false` (or `true`)
549
+
550
+ Attaching `additionalProperties: false` forbid dynamic keys while `: true` (default)
551
+ explicitly allow those.
552
+ This is useful mostly for the `false` case, to prevent unexpected extras:
553
+
554
+ ```rb
555
+ it 'returns a closed object', openapi: {
556
+ additional_properties: { '' => false },
557
+ } do
558
+ get '/api/v1/profile'
559
+ # Response: { "id": 1, "name": "alice" }
560
+ expect(response.status).to eq(200)
561
+ end
562
+ ```
563
+
564
+ ```yaml
565
+ schema:
566
+ type: object
567
+ properties:
568
+ id: { type: integer }
569
+ name: { type: string }
570
+ required: [ id, name ]
571
+ additionalProperties: false
572
+ ```
573
+
574
+ #### Hybrid objects (known keys + dynamic keys)
575
+
576
+ When an object has both a fixed shape and dynamic keys, use `hybrid_additional_properties`
577
+ (`request_hybrid_additional_properties` and `response_hybrid_additional_properties` to scope
578
+ to one side). Captured properties stay; the supplied schema is attached as `additionalProperties`.
579
+
580
+ At the root (whole body is the hybrid object):
581
+
582
+ ```rb
583
+ it 'returns an item with custom attributes', openapi: {
584
+ hybrid_additional_properties: { '' => { type: 'string' } },
585
+ } do
586
+ get '/api/v1/items/42'
587
+ # Response: { "id": 42, "attr_color": "red", "attr_size": "large" }
588
+ expect(response.status).to eq(200)
589
+ end
590
+ ```
591
+
592
+ ```yaml
593
+ schema:
594
+ type: object
595
+ properties:
596
+ id: { type: integer }
597
+ attr_color: { type: string }
598
+ attr_size: { type: string }
599
+ required: [ id, attr_color, attr_size ]
600
+ additionalProperties:
601
+ type: string
602
+ ```
603
+
604
+ #### Notes on behavior
605
+
606
+ - A hash schema in `additional_properties` fully replaces the captured
607
+ `properties` / `required` at the matched node. To keep observed properties
608
+ alongside `additionalProperties`, use `hybrid_additional_properties` or pass
609
+ a boolean.
610
+ - Recursion stops once `additional_properties` matches a path with a hash
611
+ schema: nested overrides underneath (e.g. setting both `'data'` and
612
+ `'data.meta'`) won't be applied, because the supplied dictionary value
613
+ schema describes every child uniformly and isn't traversed further.
614
+ `hybrid_additional_properties` keeps observed properties so children are
615
+ still walked normally.
616
+ - When migrating an existing schema (one previously generated with concrete
617
+ `properties` / `required`) by adding this metadata, the next regeneration
618
+ prunes the stale `properties` / `required` automatically. If you had
619
+ manually added `additionalProperties` to an object that also has
620
+ `properties`, that hybrid shape is preserved.
621
+
450
622
  ### Multiple Examples Mode
451
623
 
452
624
  You can generate multiple named examples for the same endpoint using `example_mode`:
@@ -480,23 +652,144 @@ responses:
480
652
  value: { ... }
481
653
  ```
482
654
 
655
+
483
656
  Available `example_mode` values:
657
+
484
658
  - `:single` (default) - generates single `example` field
485
659
  - `:multiple` - generates named `examples` with test descriptions as keys
486
660
  - `:none` - generates only schema, no examples
487
661
 
662
+ `example_mode` also accepts a hash form so you can configure the request body and the response independently:
663
+
664
+ ```rb
665
+ describe 'POST /sign_in', openapi: { example_mode: { request: :multiple, response: :multiple } } do
666
+ ...
667
+ end
668
+ ```
669
+
670
+ Missing keys default to `:single`, so `{ example_mode: { request: :multiple } }` keeps the response on `:single`.
671
+
672
+ The bare symbol form maps as follows:
673
+
674
+ | `example_mode` value | request side | response side |
675
+ |-----------------------------------------------|--------------|---------------|
676
+ | `:single` (default) | `:single` | `:single` |
677
+ | `:none` | `:none` | `:none` |
678
+ | `:multiple` | `:single` ⚠️ | `:multiple` |
679
+ | `{ request: :multiple, response: :multiple }` | `:multiple` | `:multiple` |
680
+
681
+ ⚠️ The bare `:multiple` shorthand is currently **response-only** to preserve the behavior that existed before
682
+ request-side multi-examples were introduced. A future major release will change it to mean
683
+ `{ request: :multiple, response: :multiple }`.
684
+
488
685
  The mode is inherited by nested contexts and can be overridden at any level.
489
686
 
490
687
  **Note:** If multiple examples resolve to the same example key for a single endpoint, the last one wins (overwrites).
491
688
 
689
+ #### Request Body Multiple Examples
690
+
691
+ With `example_mode: { request: :multiple }`, the generator produces named `examples:` for `requestBody`.
692
+ This is useful when the same endpoint accepts mutually exclusive request shapes (e.g. OTP vs email/password sign-in),
693
+ where merging them into a single `example:` would produce a nonsensical mixed payload.
694
+
695
+ ```rb
696
+ describe 'POST /sign_in',
697
+ openapi: { example_mode: { request: :multiple, response: :multiple } } do
698
+ it 'with otp' do
699
+ post '/sign_in',
700
+ params: { auth_type: 'otp', otp: '123456' }.to_json,
701
+ headers: { 'CONTENT_TYPE' => 'application/json' }
702
+ expect(response.status).to eq(200)
703
+ end
704
+
705
+ it 'with email password' do
706
+ post '/sign_in',
707
+ params: { auth_type: 'email', email: 'a@b.c', password: 'pw' }.to_json,
708
+ headers: { 'CONTENT_TYPE' => 'application/json' }
709
+ expect(response.status).to eq(200)
710
+ end
711
+ end
712
+ ```
713
+
714
+ The generated `requestBody` keeps each shape as its own keyed example:
715
+
716
+ ```yaml
717
+ requestBody:
718
+ content:
719
+ application/json:
720
+ schema:
721
+ type: object
722
+ properties:
723
+ auth_type: { type: string }
724
+ otp: { type: string }
725
+ email: { type: string }
726
+ password: { type: string }
727
+ required:
728
+ - auth_type
729
+ examples:
730
+ with_otp:
731
+ summary: with otp
732
+ value: { auth_type: otp, otp: '123456' }
733
+ with_email_password:
734
+ summary: with email password
735
+ value: { auth_type: email, email: a@b.c, password: pw }
736
+ ```
737
+
738
+ The keys default to the test's description (normalized to lowercase + underscores).
739
+ Override with `openapi: { example_key: 'custom_key', example_name: 'Custom Summary' }`,
740
+ just like for response examples.
741
+
742
+ ##### Capturing 4xx requestBody examples
743
+
744
+ When `request_example_mode` is `:single` (the default), tests with `status >= 400`
745
+ are intentionally skipped for `requestBody` so that error payloads don't pollute
746
+ the request schema. When `request: :multiple` is set, this short-circuit is lifted:
747
+ each failing test contributes its own keyed example, so you can document
748
+ validation-failure shapes alongside success shapes:
749
+
750
+ ```rb
751
+ describe 'POST /sign_in',
752
+ openapi: { example_mode: { request: :multiple, response: :multiple } } do
753
+ it 'with otp' do
754
+ post '/sign_in', params: { auth_type: 'otp', otp: '123456' }.to_json,
755
+ headers: { 'CONTENT_TYPE' => 'application/json' }
756
+ expect(response.status).to eq(200)
757
+ end
758
+
759
+ it 'with missing fields' do
760
+ post '/sign_in', params: { auth_type: 'email' }.to_json,
761
+ headers: { 'CONTENT_TYPE' => 'application/json' }
762
+ expect(response.status).to eq(400)
763
+ end
764
+ end
765
+ ```
766
+
767
+ ```yaml
768
+ requestBody:
769
+ content:
770
+ application/json:
771
+ examples:
772
+ with_otp: { summary: with otp, value: { ... } }
773
+ with_missing_fields: { summary: with missing fields, value: { ... } }
774
+ responses:
775
+ '200': { ... }
776
+ '400': { ... }
777
+ ```
778
+
779
+ Mixing `:single` (some tests) with `:multiple` (others) on the same endpoint also works for `requestBody` -
780
+ the merger up-converts the singular `example:` into the `examples:` map automatically
781
+ (see [Merge Behavior with Mixed Modes](#merge-behavior-with-mixed-modes) below).
782
+
492
783
  #### Merge Behavior with Mixed Modes
493
784
 
494
- When multiple tests target the same endpoint with different `example_mode` settings (even from different spec files), the merger automatically converts to `examples` format:
785
+ When multiple tests target the same endpoint with different `example_mode` settings (even from different spec files),
786
+ the merger automatically converts to `examples` format:
495
787
 
496
788
  ```rb
497
789
  # spec/requests/api_spec.rb
498
790
  describe 'GET /users' do
499
- it 'returns users' do # default :single mode
791
+ it 'returns users' do
792
+ # default :single mode
500
793
  get '/users'
501
794
  expect(response.status).to eq(200)
502
795
  end
@@ -512,6 +805,7 @@ end
512
805
  ```
513
806
 
514
807
  Result - both examples merged into `examples`:
808
+
515
809
  ```yaml
516
810
  responses:
517
811
  '200':
@@ -541,6 +835,7 @@ Even if you are not using `rspec` this gem might help you with its experimental
541
835
  Example:
542
836
 
543
837
  ```rb
838
+
544
839
  class TablesTest < ActionDispatch::IntegrationTest
545
840
  openapi!
546
841
 
@@ -558,9 +853,11 @@ class TablesTest < ActionDispatch::IntegrationTest
558
853
  end
559
854
  ```
560
855
 
561
- 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.
856
+ It should work with both classes inheriting from `ActionDispatch::IntegrationTest` and with classes using `Rack::Test`
857
+ directly, as long as you call `openapi!` in your test class.
562
858
 
563
- 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.
859
+ Please note that not all features present in the rspec integration work with minitest (yet). For example, custom per
860
+ test case metadata is not supported. A custom `description_builder` will not work either.
564
861
 
565
862
  Run minitest with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs.
566
863
 
@@ -579,15 +876,19 @@ Existing RSpec plugins which have OpenAPI integration:
579
876
  ## Acknowledgements
580
877
 
581
878
  * Heavily inspired by [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
582
- * Orignally created by [k0kubun](https://github.com/k0kubun) and the ownership was transferred to [exoego](https://github.com/exoego) in 2022-11-29.
583
-
879
+ * Orignally created by [k0kubun](https://github.com/k0kubun) and the ownership was transferred
880
+ to [exoego](https://github.com/exoego) in 2022-11-29.
584
881
 
585
882
  ## Releasing
586
883
 
587
- 1. Ensure RubyGems trusted publishing is configured for this repo and gem ownership (see [Trusted publishing](https://guides.rubygems.org/trusted-publishing/)).
588
- 2. In GitHub Actions, run the `prepare release` workflow manually. It bumps `lib/rspec/openapi/version.rb`, pushes `release/v<version>` to origin, and opens a PR.
884
+ 1. Ensure RubyGems trusted publishing is configured for this repo and gem ownership (
885
+ see [Trusted publishing](https://guides.rubygems.org/trusted-publishing/)).
886
+ 2. In GitHub Actions, run the `prepare release` workflow manually. It bumps `lib/rspec/openapi/version.rb`, pushes
887
+ `release/v<version>` to origin, and opens a PR.
589
888
  3. Review and merge the release PR into the default branch.
590
- 4. Create and push a tag `v<version>` on the merged commit (via the GitHub UI or `git tag v<version>; git push origin v<version>`). Tag creation triggers the `Publish to RubyGems` workflow, which publishes the gem and creates the GitHub release notes automatically.
889
+ 4. Create and push a tag `v<version>` on the merged commit (via the GitHub UI or
890
+ `git tag v<version>; git push origin v<version>`). Tag creation triggers the `Publish to RubyGems` workflow, which
891
+ publishes the gem and creates the GitHub release notes automatically.
591
892
 
592
893
  ## License
593
894
 
@@ -54,8 +54,11 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new
54
54
 
55
55
  return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless route.routable?
56
56
 
57
- summary, tags, formats, operation_id, required_request_params, security, description, deprecated, example_mode,
58
- example_key, example_name, response_enum, request_enum = SharedExtractor.attributes(example)
57
+ summary, tags, formats, operation_id, required_request_params, security, description, deprecated,
58
+ request_example_mode, response_example_mode,
59
+ example_key, example_name, response_enum, request_enum, response_additional_properties,
60
+ request_additional_properties, response_hybrid_additional_properties,
61
+ request_hybrid_additional_properties = SharedExtractor.attributes(example)
59
62
 
60
63
  path = request.path
61
64
 
@@ -80,11 +83,16 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new
80
83
  security,
81
84
  deprecated,
82
85
  formats,
83
- example_mode,
86
+ request_example_mode,
87
+ response_example_mode,
84
88
  example_key,
85
89
  example_name,
86
90
  response_enum,
87
91
  request_enum,
92
+ response_additional_properties,
93
+ request_additional_properties,
94
+ response_hybrid_additional_properties,
95
+ request_hybrid_additional_properties,
88
96
  ]
89
97
  end
90
98
 
@@ -6,8 +6,11 @@ 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
- summary, tags, formats, operation_id, required_request_params, security, description, deprecated, example_mode,
10
- example_key, example_name, response_enum, request_enum = SharedExtractor.attributes(example)
9
+ summary, tags, formats, operation_id, required_request_params, security, description, deprecated,
10
+ request_example_mode, response_example_mode,
11
+ example_key, example_name, response_enum, request_enum, response_additional_properties,
12
+ request_additional_properties, response_hybrid_additional_properties,
13
+ request_hybrid_additional_properties = SharedExtractor.attributes(example)
11
14
 
12
15
  raw_path_params = request.path_parameters
13
16
  path = request.path
@@ -24,11 +27,16 @@ class << RSpec::OpenAPI::Extractors::Rack = Object.new
24
27
  security,
25
28
  deprecated,
26
29
  formats,
27
- example_mode,
30
+ request_example_mode,
31
+ response_example_mode,
28
32
  example_key,
29
33
  example_name,
30
34
  response_enum,
31
35
  request_enum,
36
+ response_additional_properties,
37
+ request_additional_properties,
38
+ response_hybrid_additional_properties,
39
+ request_hybrid_additional_properties,
32
40
  ]
33
41
  end
34
42
 
@@ -16,8 +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
- summary, tags, formats, operation_id, required_request_params, security, description, deprecated, example_mode,
20
- example_key, example_name, response_enum, request_enum = SharedExtractor.attributes(example)
19
+ summary, tags, formats, operation_id, required_request_params, security, description, deprecated,
20
+ request_example_mode, response_example_mode,
21
+ example_key, example_name, response_enum, request_enum, response_additional_properties,
22
+ request_additional_properties, response_hybrid_additional_properties,
23
+ request_hybrid_additional_properties = SharedExtractor.attributes(example)
21
24
 
22
25
  raw_path_params = request.path_parameters
23
26
 
@@ -40,16 +43,26 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new
40
43
  security,
41
44
  deprecated,
42
45
  formats,
43
- example_mode,
46
+ request_example_mode,
47
+ response_example_mode,
44
48
  example_key,
45
49
  example_name,
46
50
  response_enum,
47
51
  request_enum,
52
+ response_additional_properties,
53
+ request_additional_properties,
54
+ response_hybrid_additional_properties,
55
+ request_hybrid_additional_properties,
48
56
  ]
49
57
  end
50
58
 
51
59
  # @param [RSpec::ExampleGroups::*] context
52
60
  def request_response(context)
61
+ # Read @integration_session directly so user-defined let(:request)/let(:response)
62
+ # don't shadow the real ActionDispatch objects we need for OpenAPI extraction.
63
+ session = context.instance_variable_get(:@integration_session)
64
+ return [session.request, session.response] if session
65
+
53
66
  [context.request, context.response]
54
67
  end
55
68
 
@@ -4,6 +4,14 @@
4
4
  class SharedExtractor
5
5
  VALID_EXAMPLE_MODES = %i[none single multiple].freeze
6
6
 
7
+ EXAMPLE_MODE_MULTIPLE_SHORTHAND_WARNING = <<~MSG.tr("\n", ' ').strip.freeze
8
+ [rspec-openapi] DEPRECATION: example_mode: :multiple currently means
9
+ { request: :single, response: :multiple }. A future major version will
10
+ change it to { request: :multiple, response: :multiple } (both sides
11
+ multi). Specify the hash form explicitly to lock in current behavior or
12
+ opt in early.
13
+ MSG
14
+
7
15
  def self.attributes(example)
8
16
  metadata = merge_openapi_metadata(example.metadata)
9
17
  summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
@@ -14,7 +22,7 @@ class SharedExtractor
14
22
  security = metadata[:security]
15
23
  description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
16
24
  deprecated = metadata[:deprecated]
17
- example_mode = normalize_example_mode(metadata[:example_mode], example)
25
+ request_example_mode, response_example_mode = normalize_example_mode(metadata[:example_mode], example)
18
26
  example_name = metadata[:example_name] || RSpec::OpenAPI.example_name_builder.call(example)
19
27
  raw_example_key = metadata[:example_key] || example_name
20
28
  example_key = RSpec::OpenAPI::ExampleKey.normalize(raw_example_key)
@@ -25,8 +33,29 @@ class SharedExtractor
25
33
  response_enum = normalize_enum(metadata[:response_enum]) || base_enum
26
34
  request_enum = normalize_enum(metadata[:request_enum]) || base_enum
27
35
 
28
- [summary, tags, formats, operation_id, required_request_params, security, description, deprecated, example_mode,
29
- example_key, example_name, response_enum, request_enum,]
36
+ response_additional_properties, request_additional_properties = resolve_additional_properties(metadata)
37
+ response_hybrid_additional_properties, request_hybrid_additional_properties =
38
+ resolve_hybrid_additional_properties(metadata)
39
+
40
+ [summary, tags, formats, operation_id, required_request_params, security, description, deprecated,
41
+ request_example_mode, response_example_mode,
42
+ example_key, example_name, response_enum, request_enum, response_additional_properties,
43
+ request_additional_properties, response_hybrid_additional_properties,
44
+ request_hybrid_additional_properties,]
45
+ end
46
+
47
+ def self.resolve_additional_properties(metadata)
48
+ base = normalize_additional_properties(metadata[:additional_properties])
49
+ response = normalize_additional_properties(metadata[:response_additional_properties]) || base
50
+ request = normalize_additional_properties(metadata[:request_additional_properties]) || base
51
+ [response, request]
52
+ end
53
+
54
+ def self.resolve_hybrid_additional_properties(metadata)
55
+ base = normalize_additional_properties(metadata[:hybrid_additional_properties])
56
+ response = normalize_additional_properties(metadata[:response_hybrid_additional_properties]) || base
57
+ request = normalize_additional_properties(metadata[:request_hybrid_additional_properties]) || base
58
+ [response, request]
30
59
  end
31
60
 
32
61
  def self.normalize_enum(enum_hash)
@@ -36,6 +65,14 @@ class SharedExtractor
36
65
  enum_hash.transform_keys(&:to_s)
37
66
  end
38
67
 
68
+ def self.normalize_additional_properties(hash)
69
+ return nil if hash.nil? || hash.empty?
70
+
71
+ hash.each_with_object({}) do |(path, schema), result|
72
+ result[path.to_s] = RSpec::OpenAPI::KeyTransformer.symbolize(schema)
73
+ end
74
+ end
75
+
39
76
  def self.merge_openapi_metadata(metadata)
40
77
  collect_openapi_metadata(metadata).reduce({}, &:merge)
41
78
  end
@@ -54,9 +91,33 @@ class SharedExtractor
54
91
  end
55
92
  end
56
93
 
94
+ # Returns [request_mode, response_mode]. Accepts either a bare Symbol/String
95
+ # (applied to both sides, except :multiple which is treated as a backward-compat
96
+ # shorthand for { request: :single, response: :multiple } and emits a one-time
97
+ # deprecation warning) or a Hash with :request / :response keys.
57
98
  def self.normalize_example_mode(value, example = nil)
58
- return :single if value.nil?
99
+ return %i[single single] if value.nil?
59
100
 
101
+ case value
102
+ when Hash
103
+ [
104
+ normalize_example_mode_hash_value(value, :request, example),
105
+ normalize_example_mode_hash_value(value, :response, example),
106
+ ]
107
+ when Symbol, String
108
+ mode = coerce_example_mode_value(value, example)
109
+ if mode == :multiple
110
+ warn_example_mode_multiple_shorthand
111
+ %i[single multiple]
112
+ else
113
+ [mode, mode]
114
+ end
115
+ else
116
+ raise ArgumentError, example_mode_error(value, example)
117
+ end
118
+ end
119
+
120
+ def self.coerce_example_mode_value(value, example)
60
121
  raise ArgumentError, example_mode_error(value, example) unless value.is_a?(String) || value.is_a?(Symbol)
61
122
 
62
123
  mode = value.to_s.strip.downcase.to_sym
@@ -65,9 +126,25 @@ class SharedExtractor
65
126
  raise ArgumentError, example_mode_error(value, example)
66
127
  end
67
128
 
129
+ def self.normalize_example_mode_hash_value(hash, key, example)
130
+ raw = hash[key]
131
+ raw = hash[key.to_s] if raw.nil?
132
+ return :single if raw.nil?
133
+
134
+ coerce_example_mode_value(raw, example)
135
+ end
136
+
137
+ def self.warn_example_mode_multiple_shorthand
138
+ return if @warned_example_mode_multiple_shorthand
139
+
140
+ @warned_example_mode_multiple_shorthand = true
141
+ Kernel.warn(EXAMPLE_MODE_MULTIPLE_SHORTHAND_WARNING)
142
+ end
143
+
68
144
  def self.example_mode_error(value, example)
69
145
  context = example&.full_description
70
146
  context = " (example: #{context})" if context
71
- "example_mode must be one of #{VALID_EXAMPLE_MODES.inspect}, got #{value.inspect}#{context}"
147
+ "example_mode must be a Symbol/String in #{VALID_EXAMPLE_MODES.inspect} " \
148
+ "or a Hash with :request/:response keys, got #{value.inspect}#{context}"
72
149
  end
73
150
  end
@@ -20,7 +20,8 @@ RSpec::OpenAPI::Record = Struct.new(
20
20
  :security, # @param [Array] - [{securityScheme1: []}]
21
21
  :deprecated, # @param [Boolean] - true
22
22
  :example_enabled, # @param [Boolean] - true
23
- :example_mode, # @param [Symbol] - :none | :single | :multiple
23
+ :request_example_mode, # @param [Symbol] - :none | :single | :multiple
24
+ :response_example_mode, # @param [Symbol] - :none | :single | :multiple
24
25
  :status, # @param [Integer] - 200
25
26
  :response_body, # @param [Object] - {"status" => "ok"}
26
27
  :response_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
@@ -28,5 +29,9 @@ RSpec::OpenAPI::Record = Struct.new(
28
29
  :response_content_disposition, # @param [String] - "inline"
29
30
  :response_enum, # @param [Hash] - {"status" => ["active", "inactive"], "user.role" => ["admin", "user"]}
30
31
  :request_enum, # @param [Hash] - {"type" => ["create", "update"]}
32
+ :response_additional_properties, # @param [Hash] - {"data" => { type: "boolean" }}
33
+ :request_additional_properties, # @param [Hash] - {"meta" => { type: "string" }}
34
+ :response_hybrid_additional_properties, # @param [Hash] - {"data" => { type: "string" }}
35
+ :request_hybrid_additional_properties, # @param [Hash] - {"meta" => { type: "string" }}
31
36
  keyword_init: true,
32
37
  )
@@ -13,8 +13,10 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
13
13
 
14
14
  title = RSpec::OpenAPI.title.then { |t| t.is_a?(Proc) ? t.call(example) : t }
15
15
  path, summary, tags, operation_id, required_request_params, raw_path_params,
16
- description, security, deprecated, formats, example_mode, example_key,
17
- example_name, response_enum, request_enum = extractor.request_attributes(request, example)
16
+ description, security, deprecated, formats, request_example_mode, response_example_mode,
17
+ example_key, example_name, response_enum, request_enum, response_additional_properties,
18
+ request_additional_properties, response_hybrid_additional_properties,
19
+ request_hybrid_additional_properties = extractor.request_attributes(request, example)
18
20
 
19
21
  return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) }
20
22
 
@@ -43,11 +45,16 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new
43
45
  response_content_type: response.media_type,
44
46
  response_content_disposition: response.header['Content-Disposition'],
45
47
  example_enabled: RSpec::OpenAPI.enable_example,
46
- example_mode: example_mode,
48
+ request_example_mode: request_example_mode,
49
+ response_example_mode: response_example_mode,
47
50
  example_key: example_key,
48
51
  example_name: example_name,
49
52
  response_enum: response_enum,
50
53
  request_enum: request_enum,
54
+ response_additional_properties: response_additional_properties,
55
+ request_additional_properties: request_additional_properties,
56
+ response_hybrid_additional_properties: response_hybrid_additional_properties,
57
+ request_hybrid_additional_properties: request_hybrid_additional_properties,
51
58
  ).freeze
52
59
  end
53
60
 
@@ -4,9 +4,15 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
4
4
  # @param [RSpec::OpenAPI::Record] record
5
5
  # @return [Hash]
6
6
  def build(record)
7
- response = {
8
- description: record.description,
9
- }
7
+ response = if record.response_example_mode == :none
8
+ # `:none` opts out of recording, so the description is provisional.
9
+ # Stash it under a fallback key; SchemaCleaner promotes it to
10
+ # `description` only if no documented test has set one. This makes
11
+ # the merge result independent of RSpec's random execution order.
12
+ { _fallback_description: record.description }
13
+ else
14
+ { description: record.description }
15
+ end
10
16
 
11
17
  response_headers = build_response_headers(record)
12
18
  response[:headers] = response_headers unless response_headers.empty?
@@ -52,7 +58,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
52
58
  # If examples are globally disabled, always return schema-only content.
53
59
  return { content_type => { schema: schema }.compact } unless example_enabled?(record)
54
60
 
55
- case record.example_mode
61
+ case record.response_example_mode
56
62
  when :none
57
63
  # Only schema, no examples
58
64
  {
@@ -74,8 +80,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
74
80
  content_type => {
75
81
  schema: schema,
76
82
  example: response_example(record, disposition: disposition),
77
- _example_key: record.example_key,
78
- _example_summary: example_summary(record),
83
+ **example_metadata(record),
79
84
  }.compact,
80
85
  }
81
86
  end
@@ -93,13 +98,21 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
93
98
  end
94
99
 
95
100
  def build_example_object(record, disposition:)
101
+ build_named_example(record, response_example(record, disposition: disposition))
102
+ end
103
+
104
+ def build_named_example(record, value)
96
105
  summary = example_summary(record)
97
106
  example = {}
98
107
  example[:summary] = summary if summary
99
- example[:value] = response_example(record, disposition: disposition)
108
+ example[:value] = value
100
109
  example
101
110
  end
102
111
 
112
+ def example_metadata(record)
113
+ { _example_key: record.example_key, _example_summary: example_summary(record) }
114
+ end
115
+
103
116
  def example_summary(record)
104
117
  return nil unless example_summary_enabled?
105
118
  return nil if record.example_name.nil? || record.example_name.empty?
@@ -191,16 +204,33 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
191
204
 
192
205
  def build_request_body(record)
193
206
  return nil if record.request_content_type.nil?
194
- return nil if record.status >= 400
207
+ return nil if record.status >= 400 && record.request_example_mode != :multiple
195
208
 
196
- {
197
- content: {
198
- normalize_content_type(record.request_content_type) => {
199
- schema: build_property(record.request_params, record: record, context: :request),
200
- example: (build_example(record.request_params) if example_enabled?(record)),
201
- }.compact,
202
- },
203
- }
209
+ content_type = normalize_content_type(record.request_content_type)
210
+ schema = build_property(record.request_params, record: record, context: :request)
211
+
212
+ return { content: { content_type => { schema: schema }.compact } } unless example_enabled?(record)
213
+
214
+ example = build_example(record.request_params)
215
+
216
+ body =
217
+ case record.request_example_mode
218
+ when :none
219
+ { schema: schema }
220
+ when :multiple
221
+ {
222
+ schema: schema,
223
+ examples: { record.example_key => build_named_example(record, example) },
224
+ }
225
+ else # :single (default)
226
+ {
227
+ schema: schema,
228
+ example: example,
229
+ **example_metadata(record),
230
+ }
231
+ end
232
+
233
+ { content: { content_type => body.compact } }
204
234
  end
205
235
 
206
236
  def build_property(value, disposition: nil, key: nil, record: nil, path: nil, context: nil)
@@ -217,13 +247,32 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
217
247
  build_array_items_schema(value, record: record, path: path, context: context)
218
248
  end
219
249
  when Hash
220
- property[:properties] = {}.tap do |properties|
221
- value.each do |k, v|
222
- child_path = path ? "#{path}.#{k}" : k.to_s
223
- properties[k] = build_property(v, record: record, key: k, path: child_path, context: context)
250
+ override = infer_additional_properties(path, record, context)
251
+ hybrid_override = infer_hybrid_additional_properties(path, record, context)
252
+ if override.is_a?(Hash) && !override.empty?
253
+ # Schema override: the object's keys are dynamic replace captured
254
+ # `properties` / `required` with the supplied dictionary value schema.
255
+ property[:additionalProperties] = override
256
+ else
257
+ property[:properties] = {}.tap do |properties|
258
+ value.each do |k, v|
259
+ child_path = path ? "#{path}.#{k}" : k.to_s
260
+ properties[k] = build_property(v, record: record, key: k, path: child_path, context: context)
261
+ end
262
+ end
263
+ property = enrich_with_required_keys(property)
264
+ # Hybrid: keep the observed `properties` / `required` and attach
265
+ # `additionalProperties` alongside.
266
+ # - Boolean values are constraints (`false` forbids extras, `true`
267
+ # explicitly allows them).
268
+ # - Hash schema values come from the dedicated `hybrid_additional_properties`
269
+ # metadata, expressing "known keys + extras of this type".
270
+ if override == true || override == false
271
+ property[:additionalProperties] = override
272
+ elsif hybrid_override.is_a?(Hash) && !hybrid_override.empty?
273
+ property[:additionalProperties] = hybrid_override
224
274
  end
225
275
  end
226
- property = enrich_with_required_keys(property)
227
276
  end
228
277
  property
229
278
  end
@@ -274,6 +323,36 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new
274
323
  enum_hash[path.to_s]
275
324
  end
276
325
 
326
+ def infer_additional_properties(path, record, context)
327
+ return nil unless record
328
+
329
+ overrides = if context == :request
330
+ record.request_additional_properties
331
+ else
332
+ record.response_additional_properties
333
+ end
334
+ return nil unless overrides
335
+
336
+ # path is nil at the body root; nil.to_s == '' lets users target it via { '' => ... }.
337
+ # Use `key?` so a literal `false` override is distinguishable from "no override".
338
+ return nil unless overrides.key?(path.to_s)
339
+
340
+ overrides[path.to_s]
341
+ end
342
+
343
+ def infer_hybrid_additional_properties(path, record, context)
344
+ return nil unless record
345
+
346
+ overrides = if context == :request
347
+ record.request_hybrid_additional_properties
348
+ else
349
+ record.response_hybrid_additional_properties
350
+ end
351
+ return nil unless overrides
352
+
353
+ overrides[path.to_s]
354
+ end
355
+
277
356
  # Convert an always-String param to an appropriate type
278
357
  def try_cast(value)
279
358
  Integer(value)
@@ -86,6 +86,9 @@ class << RSpec::OpenAPI::SchemaCleaner = Object.new
86
86
  hash.delete(:_example_key)
87
87
  hash.delete(:_example_summary)
88
88
  hash.delete(:_example_name)
89
+ if (fallback = hash.delete(:_fallback_description))
90
+ hash[:description] ||= fallback
91
+ end
89
92
 
90
93
  hash.each_value do |value|
91
94
  case value
@@ -25,6 +25,17 @@ 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)
37
+ end
38
+
28
39
  spec.each do |key, value|
29
40
  if base[key].is_a?(Hash) && value.is_a?(Hash)
30
41
  # Handle example/examples conflict - convert to examples when mixed
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module OpenAPI
5
- VERSION = '0.25.1'
5
+ VERSION = '0.26.0'
6
6
  end
7
7
  end
data/redocly.yaml ADDED
@@ -0,0 +1,31 @@
1
+ # Config for `redocly lint` used by .github/workflows/validate-openapi.yml
2
+ # to validate test-fixture OpenAPI documents shipped under spec/apps/.
3
+ #
4
+ # spec/apps/rails/doc/smart/openapi.yaml is intentionally excluded:
5
+ # it is the input fixture for the smart-merge feature test (it contains
6
+ # unresolved $refs by design), not a complete API description.
7
+ extends:
8
+ - minimal
9
+ apis:
10
+ rails-rspec-yaml:
11
+ root: spec/apps/rails/doc/rspec_openapi.yaml
12
+ rails-rspec-json:
13
+ root: spec/apps/rails/doc/rspec_openapi.json
14
+ rails-minitest-yaml:
15
+ root: spec/apps/rails/doc/minitest_openapi.yaml
16
+ rails-minitest-json:
17
+ root: spec/apps/rails/doc/minitest_openapi.json
18
+ rails-smart-expected:
19
+ root: spec/apps/rails/doc/smart/expected.yaml
20
+ roda-rspec-yaml:
21
+ root: spec/apps/roda/doc/rspec_openapi.yaml
22
+ roda-rspec-json:
23
+ root: spec/apps/roda/doc/rspec_openapi.json
24
+ roda-minitest-yaml:
25
+ root: spec/apps/roda/doc/minitest_openapi.yaml
26
+ roda-minitest-json:
27
+ root: spec/apps/roda/doc/minitest_openapi.json
28
+ hanami-yaml:
29
+ root: spec/apps/hanami/doc/openapi.yaml
30
+ hanami-json:
31
+ root: spec/apps/hanami/doc/openapi.json
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.25.1
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
@@ -67,6 +67,7 @@ files:
67
67
  - ".github/workflows/publish.yml"
68
68
  - ".github/workflows/rubocop.yml"
69
69
  - ".github/workflows/test.yml"
70
+ - ".github/workflows/validate-openapi.yml"
70
71
  - ".gitignore"
71
72
  - ".rspec"
72
73
  - ".rubocop.yml"
@@ -102,6 +103,7 @@ files:
102
103
  - lib/rspec/openapi/schema_sorter.rb
103
104
  - lib/rspec/openapi/shared_hooks.rb
104
105
  - lib/rspec/openapi/version.rb
106
+ - redocly.yaml
105
107
  - rspec-openapi.gemspec
106
108
  - scripts/rspec
107
109
  - scripts/rspec_with_simplecov
@@ -112,7 +114,7 @@ licenses:
112
114
  metadata:
113
115
  homepage_uri: https://github.com/exoego/rspec-openapi
114
116
  source_code_uri: https://github.com/exoego/rspec-openapi
115
- changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.25.1
117
+ changelog_uri: https://github.com/exoego/rspec-openapi/releases/tag/v0.26.0
116
118
  rubygems_mfa_required: 'true'
117
119
  rdoc_options: []
118
120
  require_paths: