rspec-openapi 0.25.0 → 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: 65e97b91c81a4798c7907d907174d8c7efe874ad26dd3d630db2c86db97f913d
4
- data.tar.gz: a593eaf58214a7bc5578f3cee065d263d7b494507727ac05c60111d01b0a2a7d
3
+ metadata.gz: 254e5e91a3965a417ad69c8b61d4bfdde6af42ab7aba14854d5531d73f4a793d
4
+ data.tar.gz: 3fa43709cf02b95bb1f4815eb4eebefde3ad5e5e2aabf0439267dce2c06cba36
5
5
  SHA512:
6
- metadata.gz: 3e88fae82f5452b748d2ade83adc141fff0e888946c48ec3219999012bd75a7162e678d89af1e1ccb584e14250739715e0ffd53ec1b1ff101080ca2abd44ba82
7
- data.tar.gz: e49fdd879b02d43fe0f8ac4ef2d443b7a0366c5f133d5012f771be380c706b9fa28597f6bd6e6e111bee599bee26e5ae71f8dec262e4bf6070c679790e42d654
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.1 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 }}
@@ -42,7 +42,7 @@ jobs:
42
42
  - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
43
43
  name: codecov-action@v4 workaround
44
44
  - name: Upload coverage reports
45
- uses: codecov/codecov-action@v5
45
+ uses: codecov/codecov-action@v6
46
46
  if: matrix.coverage == 'coverage'
47
47
  with:
48
48
  fail_ci_if_error: true
@@ -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
 
@@ -3,6 +3,8 @@
3
3
  require_relative 'hash_helper'
4
4
 
5
5
  class << RSpec::OpenAPI::ComponentsUpdater = Object.new
6
+ SCHEMA_REF_PREFIX = '#/components/schemas/'
7
+
6
8
  # @param [Hash] base
7
9
  # @param [Hash] fresh
8
10
  def update!(base, fresh)
@@ -30,7 +32,7 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
30
32
  # Skip if the property using $ref is not found in the parent schema. The property may be removed.
31
33
  next if nested_schema.nil?
32
34
 
33
- schema_name = base.dig(*paths)&.gsub('#/components/schemas/', '')&.to_sym
35
+ schema_name = extract_schema_name(base.dig(*paths))&.to_sym
34
36
  fresh_schemas[schema_name] ||= {}
35
37
  RSpec::OpenAPI::SchemaMerger.merge!(fresh_schemas[schema_name], nested_schema)
36
38
  end
@@ -44,8 +46,8 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
44
46
  def build_fresh_schemas(references, base, fresh)
45
47
  references.inject({}) do |acc, paths|
46
48
  ref_link = dig_schema(base, paths)[:$ref]
47
- schema_name = ref_link.to_s.gsub('#/components/schemas/', '')
48
- schema_body = dig_schema(fresh, paths.reject { |path| path.is_a?(Integer) })
49
+ schema_name = extract_schema_name(ref_link)
50
+ schema_body = dig_schema(fresh, paths.grep_v(Integer))
49
51
 
50
52
  RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
51
53
  end
@@ -81,18 +83,29 @@ class << RSpec::OpenAPI::ComponentsUpdater = Object.new
81
83
  # Reject already-generated schemas to reduce unnecessary loop
82
84
  nested_refs.reject do |paths|
83
85
  ref_link = base.dig(*paths)
84
- schema_name = ref_link.gsub('#/components/schemas/', '')
86
+ schema_name = extract_schema_name(ref_link)
85
87
  generated_names.include?(schema_name)
86
88
  end
87
89
  end
88
90
 
89
91
  def find_one_of_refs(base, paths)
90
- dig_schema(base, paths)&.dig(:oneOf)&.map&.with_index do |schema, index|
91
- paths + [index] if schema&.dig(:$ref)&.start_with?('#/components/schemas/')
92
- end&.compact
92
+ one_of = dig_schema(base, paths)&.dig(:oneOf)
93
+ return unless one_of
94
+
95
+ one_of.each_with_index.filter_map do |schema, index|
96
+ paths + [index] if schema_ref?(schema&.dig(:$ref))
97
+ end
93
98
  end
94
99
 
95
100
  def find_object_refs(base, paths)
96
- [paths] if dig_schema(base, paths)&.dig(:$ref)&.start_with?('#/components/schemas/')
101
+ [paths] if schema_ref?(dig_schema(base, paths)&.dig(:$ref))
102
+ end
103
+
104
+ def extract_schema_name(ref_link)
105
+ ref_link&.delete_prefix(SCHEMA_REF_PREFIX)
106
+ end
107
+
108
+ def schema_ref?(ref_link)
109
+ ref_link&.start_with?(SCHEMA_REF_PREFIX)
97
110
  end
98
111
  end