rspec-openapi 0.25.1 → 0.27.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 +4 -4
- data/.github/workflows/create_release.yml +1 -1
- data/.github/workflows/publish.yml +1 -1
- data/.github/workflows/validate-openapi.yml +21 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +13 -13
- data/README.md +337 -36
- data/lib/rspec/openapi/extractors/hanami.rb +10 -29
- data/lib/rspec/openapi/extractors/rack.rb +7 -24
- data/lib/rspec/openapi/extractors/rails.rb +14 -27
- data/lib/rspec/openapi/extractors/shared_extractor.rb +102 -18
- data/lib/rspec/openapi/record.rb +6 -1
- data/lib/rspec/openapi/record_builder.rb +8 -22
- data/lib/rspec/openapi/schema_builder/build_context.rb +20 -0
- data/lib/rspec/openapi/schema_builder.rb +288 -286
- data/lib/rspec/openapi/schema_cleaner.rb +4 -1
- data/lib/rspec/openapi/schema_file.rb +4 -6
- data/lib/rspec/openapi/schema_merger.rb +81 -40
- data/lib/rspec/openapi/version.rb +1 -1
- data/lib/rspec/openapi.rb +2 -2
- data/redocly.yaml +31 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48caaf42c5ceaca30f18251c5bf29f427e71385d188f5abcc01295de3eab55b4
|
|
4
|
+
data.tar.gz: 171aacce0afccc800c6f5fb1e696dcd2f7d124ad0d09044e7797eb0a353452da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a6f77ff9446f74a56bc49576c6340176a58a6a0097e5d96a6b0a5f4334e06dfae55704a2e7dee3f1e12e160cea2a59933616ce2be8e60466f2c9ff10df460ac
|
|
7
|
+
data.tar.gz: f75d0f697a19d72408033e8e538643334649c2a7dab832c09e96d47f53f5be2660571d7a2b8b05cc5fdcb1d43fe86e9851f9d5d8e33a4fde8375d12120aa1c2f
|
|
@@ -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
data/.rubocop.yml
CHANGED
|
@@ -20,6 +20,10 @@ Style/ClassAndModuleChildren:
|
|
|
20
20
|
- 'lib/rspec/openapi/version.rb'
|
|
21
21
|
Layout/FirstArrayElementIndentation:
|
|
22
22
|
EnforcedStyle: consistent
|
|
23
|
+
Style/SymbolArray:
|
|
24
|
+
EnforcedStyle: brackets
|
|
25
|
+
Style/WordArray:
|
|
26
|
+
EnforcedStyle: brackets
|
|
23
27
|
Metrics/BlockLength:
|
|
24
28
|
Exclude:
|
|
25
29
|
- 'spec/**/*'
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on
|
|
3
|
+
# on 2026-05-25 01:13:29 UTC using RuboCop version 1.80.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
|
8
8
|
|
|
9
|
-
# Offense count:
|
|
9
|
+
# Offense count: 15
|
|
10
10
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
11
11
|
Metrics/AbcSize:
|
|
12
|
-
Max:
|
|
12
|
+
Max: 35
|
|
13
13
|
|
|
14
|
-
# Offense count:
|
|
14
|
+
# Offense count: 4
|
|
15
15
|
# Configuration parameters: CountComments, CountAsOne.
|
|
16
16
|
Metrics/ClassLength:
|
|
17
|
-
Max:
|
|
17
|
+
Max: 319
|
|
18
18
|
|
|
19
|
-
# Offense count:
|
|
19
|
+
# Offense count: 10
|
|
20
20
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
21
21
|
Metrics/CyclomaticComplexity:
|
|
22
|
-
Max:
|
|
22
|
+
Max: 11
|
|
23
23
|
|
|
24
|
-
# Offense count:
|
|
24
|
+
# Offense count: 29
|
|
25
25
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
26
26
|
Metrics/MethodLength:
|
|
27
|
-
Max:
|
|
27
|
+
Max: 26
|
|
28
28
|
|
|
29
|
-
# Offense count:
|
|
29
|
+
# Offense count: 3
|
|
30
30
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
31
31
|
Metrics/PerceivedComplexity:
|
|
32
|
-
Max:
|
|
32
|
+
Max: 11
|
|
33
33
|
|
|
34
34
|
# Offense count: 1
|
|
35
35
|
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
36
36
|
# SupportedStyles: snake_case, normalcase, non_integer
|
|
37
|
-
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
37
|
+
# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
38
38
|
Naming/VariableNumber:
|
|
39
39
|
Exclude:
|
|
40
40
|
- 'spec/integration_tests/roda_test.rb'
|
|
41
41
|
|
|
42
|
-
# Offense count:
|
|
42
|
+
# Offense count: 8
|
|
43
43
|
# Configuration parameters: AllowedConstants.
|
|
44
44
|
Style/Documentation:
|
|
45
45
|
Exclude:
|
data/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# rspec-openapi [](https://rubygems.org/gems/rspec-openapi) [](https://github.com/exoego/rspec-openapi/actions/workflows/test.yml) [](https://codecov.io/gh/exoego/rspec-openapi) [](https://www.ruby-toolbox.com/projects/rspec-openapi) [](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
|
|
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
|
|
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
|
-
|
|
72
|
+
- Table
|
|
71
73
|
parameters:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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)
|
|
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
|

|
|
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)
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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
|
|
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),
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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 (
|
|
588
|
-
|
|
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
|
|
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
|
|