grape-oas 1.4.0 → 1.5.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/CHANGELOG.md +43 -0
- data/README.md +27 -10
- data/UPGRADING.md +104 -0
- data/grape-oas.gemspec +2 -1
- data/lib/grape_oas/api_model/api.rb +2 -1
- data/lib/grape_oas/api_model/schema.rb +1 -1
- data/lib/grape_oas/api_model_builder.rb +3 -2
- data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +9 -1
- data/lib/grape_oas/api_model_builders/concerns/oas_utilities.rb +13 -0
- data/lib/grape_oas/api_model_builders/concerns/route_validations.rb +26 -0
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +4 -2
- data/lib/grape_oas/api_model_builders/operation.rb +3 -4
- data/lib/grape_oas/api_model_builders/path.rb +33 -6
- data/lib/grape_oas/api_model_builders/request.rb +14 -16
- data/lib/grape_oas/api_model_builders/request_params.rb +83 -3
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +1 -1
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +1 -1
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +3 -9
- data/lib/grape_oas/api_model_builders/response.rb +28 -4
- data/lib/grape_oas/constants.rb +16 -4
- data/lib/grape_oas/documentation_extension.rb +7 -9
- data/lib/grape_oas/exporter/concerns/schema_indexer.rb +14 -3
- data/lib/grape_oas/exporter/oas2/operation.rb +4 -2
- data/lib/grape_oas/exporter/oas2/parameter.rb +73 -13
- data/lib/grape_oas/exporter/oas2/paths.rb +1 -0
- data/lib/grape_oas/exporter/oas2/response.rb +6 -4
- data/lib/grape_oas/exporter/oas2/schema.rb +56 -28
- data/lib/grape_oas/exporter/oas2_schema.rb +7 -5
- data/lib/grape_oas/exporter/oas3/operation.rb +5 -3
- data/lib/grape_oas/exporter/oas3/parameter.rb +4 -2
- data/lib/grape_oas/exporter/oas3/paths.rb +1 -0
- data/lib/grape_oas/exporter/oas3/request_body.rb +5 -8
- data/lib/grape_oas/exporter/oas3/response.rb +6 -9
- data/lib/grape_oas/exporter/oas3/schema.rb +232 -84
- data/lib/grape_oas/exporter/oas31/schema.rb +12 -2
- data/lib/grape_oas/exporter/oas31_schema.rb +1 -1
- data/lib/grape_oas/exporter/oas3_schema.rb +3 -2
- data/lib/grape_oas/introspectors/entity_introspector.rb +2 -1
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +55 -20
- data/lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb +2 -8
- data/lib/grape_oas/introspectors/entity_introspector_support/type_schema_resolver.rb +4 -20
- data/lib/grape_oas/range_utils.rb +25 -2
- data/lib/grape_oas/type_resolvers/array_resolver.rb +53 -19
- data/lib/grape_oas/type_resolvers/base.rb +2 -2
- data/lib/grape_oas/type_resolvers/default_resolver.rb +23 -0
- data/lib/grape_oas/type_resolvers/dry_type_resolver.rb +1 -1
- data/lib/grape_oas/type_resolvers/primitive_resolver.rb +29 -46
- data/lib/grape_oas/type_resolvers/registry.rb +31 -15
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +37 -7
- metadata +5 -4
- data/CONTRIBUTING.md +0 -87
- data/RELEASING.md +0 -109
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e76945cff9198556752b881ff62da8c288a18e3f6447a36b7668d516ed575f4f
|
|
4
|
+
data.tar.gz: 8f36610d66d2d5935004972fdc063e74316b2a576be030ce7d8a0a903fa43f79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df261c7d71e8c228c096484cda70c821e4cdd58f1aad04b31bf402513dd414eff69512a38d9c6ef131de418fd19403c0ddadfc4ae7fe0bd8e12078ffc66e285a
|
|
7
|
+
data.tar.gz: 233f3e233f053f12f7b9fb2eb4635c8969233b4d8848b61048bf273512cffed54ce9499f473b90b56875c0d429f4d12a46f74148689fa122002a7e049fc1f383
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0] - 2026-09-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- [#123](https://github.com/numbata/grape-oas/pull/123): Add opt-in OAS2 composition extensions and include missing referenced alternatives in definitions - [@numbata](https://github.com/numbata).
|
|
13
|
+
- [#96](https://github.com/numbata/grape-oas/pull/96): Pin rubocop versions and add performance/packaging/rake plugins - [@numbata](https://github.com/numbata).
|
|
14
|
+
- [#92](https://github.com/numbata/grape-oas/pull/92): Add cross-tool AI-agent contributor guidance and tighten gem file packaging - [@numbata](https://github.com/numbata).
|
|
15
|
+
- [#88](https://github.com/numbata/grape-oas/pull/88): Add configurable `GrapeOAS.schema_ref_name` callable that controls how a schema's canonical class name is mangled into its OAS component/definition ref name. Default behavior (`name.gsub("::", "_")`) is byte-identical to prior output; applies uniformly across OAS 2.0 and OAS 3 ref emission paths - [@abeljim8am](https://github.com/abeljim8am).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- [#128](https://github.com/numbata/grape-oas/pull/128): Honor string keys in entity-root documentation - [@numbata](https://github.com/numbata).
|
|
20
|
+
- [#127](https://github.com/numbata/grape-oas/pull/127): Drop stale OAS2 `x-anyOf`/`x-oneOf` compatibility extensions when native OAS3 composition is present - [@numbata](https://github.com/numbata).
|
|
21
|
+
- [#126](https://github.com/numbata/grape-oas/pull/126): Prefer bare `$ref` in nullable OAS3 null-union alternatives (drop redundant single-element `allOf`) - [@numbata](https://github.com/numbata).
|
|
22
|
+
- [#125](https://github.com/numbata/grape-oas/pull/125): Preserve nullability on root OAS3 request and response `$ref` schemas - [@numbata](https://github.com/numbata).
|
|
23
|
+
- [#122](https://github.com/numbata/grape-oas/pull/122): Treat params declared inside Grape `given` blocks as optional — no longer emitted as unconditionally required - [@numbata](https://github.com/numbata).
|
|
24
|
+
- [#121](https://github.com/numbata/grape-oas/pull/121): Preserve nullable references, array items, and polymorphic discriminators in OAS 3.0/3.1 - [@numbata](https://github.com/numbata).
|
|
25
|
+
- [#120](https://github.com/numbata/grape-oas/pull/120): Honor explicit route `consumes:`/`produces:` independently so form request bodies are documented with the declared media type - [@numbata](https://github.com/numbata).
|
|
26
|
+
- [#119](https://github.com/numbata/grape-oas/pull/119): Stop inferring `format: int32` for generic `Integer`/`"integer"` declarations; explicit int32/int64 widths are still preserved - [@numbata](https://github.com/numbata).
|
|
27
|
+
- [#113](https://github.com/numbata/grape-oas/pull/113): Use OAS 3.1 schema rendering for inline parameters, request bodies, and responses - [@numbata](https://github.com/numbata).
|
|
28
|
+
- [#112](https://github.com/numbata/grape-oas/pull/112): Exclude format extensions from path parameter discovery - [@numbata](https://github.com/numbata).
|
|
29
|
+
- [#100](https://github.com/numbata/grape-oas/pull/100): Preserve nil enum members when the selected nullable strategy can express them - [@numbata](https://github.com/numbata).
|
|
30
|
+
- [#104](https://github.com/numbata/grape-oas/pull/104): Preserve array-valued schema examples and Boolean false examples from parameter/entity documentation in OAS 3.0/3.1; omit incompatible scalar examples - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
31
|
+
- [#109](https://github.com/numbata/grape-oas/pull/109): Fix Grape 3.3 variant collections, Grape 4.0 route metadata, and OAS 2.0 query array items - [@numbata](https://github.com/numbata).
|
|
32
|
+
- [#97](https://github.com/numbata/grape-oas/pull/97): Default OAS 2.0 nullable strategy to EXTENSION so nullable fields emit `x-nullable: true` without explicit opt-in - [@numbata](https://github.com/numbata).
|
|
33
|
+
- [#79](https://github.com/numbata/grape-oas/pull/79): Fix docs: `nickname` is supported in grape-oas - [@bogdan](https://github.com/bogdan).
|
|
34
|
+
- [#78](https://github.com/numbata/grape-oas/pull/78): Fix: use empty schema for undocumented responses instead of `{ type: string }` - [@bogdan](https://github.com/bogdan).
|
|
35
|
+
- [#74](https://github.com/numbata/grape-oas/pull/74): Fix BigDecimal range bounds serializing as JSON strings - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
36
|
+
- [#76](https://github.com/numbata/grape-oas/pull/76): Emit OAS-version-correct schema for file types - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
37
|
+
- [#82](https://github.com/numbata/grape-oas/pull/82): Fix: respect `documentation: { hidden: true }` on entity exposures - [@bogdan](https://github.com/bogdan).
|
|
38
|
+
- [#80](https://github.com/numbata/grape-oas/pull/80): Fix: hide documentation routes from generated spec by default - [@bogdan](https://github.com/bogdan).
|
|
39
|
+
- [#98](https://github.com/numbata/grape-oas/pull/98): Honor `documentation: { x: { nullable: true } }` on entity exposures - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
40
|
+
- [#85](https://github.com/numbata/grape-oas/pull/85): Honor `is_array: true` on the plain-entity response branch - [@abeljim8am](https://github.com/abeljim8am).
|
|
41
|
+
- [#87](https://github.com/numbata/grape-oas/pull/87): Fix `SchemaIndexer#index_schema` to recurse into `schema.items` so entities reachable only through an array wrapper (e.g. a property declared as `Array<OtherEntity>`) are included in the indexed schemas set - [@abeljim8am](https://github.com/abeljim8am).
|
|
42
|
+
- [#102](https://github.com/numbata/grape-oas/pull/102): Fix: nullable entity ref must not mutate shared cached schema - [@bogdan](https://github.com/bogdan).
|
|
43
|
+
- [#105](https://github.com/numbata/grape-oas/pull/105): Place array-valued `example` on the array schema instead of `items` for entity `is_array:` exposures without mutating shared entity schemas - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
44
|
+
- [#86](https://github.com/numbata/grape-oas/pull/86): Omit response content for bodyless HTTP statuses (`1xx`, `204`, `205`, `304`), including declared entities and examples, so generated OpenAPI responses remain valid - [@abeljim8am](https://github.com/abeljim8am).
|
|
45
|
+
- [#84](https://github.com/numbata/grape-oas/pull/84): Substitute the concrete path version into route templates, including prefixed and mounted routes, for `version "...", using: :path` while preserving user-defined version parameters with other versioning strategies - [@abeljim8am](https://github.com/abeljim8am).
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- [#95](https://github.com/numbata/grape-oas/pull/95): Entity exposures now consult `GrapeOAS.type_resolvers` - [@numbata](https://github.com/numbata).
|
|
50
|
+
|
|
8
51
|
## [1.4.0] - 2026-04-23
|
|
9
52
|
|
|
10
53
|
### Fixed
|
data/README.md
CHANGED
|
@@ -36,15 +36,17 @@ Grape::OAS is built around a **DTO (Data Transfer Object) architecture** that se
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
38
|
- **Multi-version support**: Generate OAS 2.0, 3.0, or 3.1 from the same API
|
|
39
|
-
- **Entity integration**: Works with [grape-entity](https://github.com/ruby-grape/grape-entity) and [dry-
|
|
39
|
+
- **Entity and contract integration**: Works with [grape-entity](https://github.com/ruby-grape/grape-entity), [dry-validation](https://dry-rb.org/gems/dry-validation/), and [dry-schema](https://dry-rb.org/gems/dry-schema/)
|
|
40
40
|
- **Automatic type inference**: Derives OpenAPI types from Grape parameter definitions
|
|
41
41
|
- **Flexible output**: Mount as an endpoint or generate programmatically
|
|
42
42
|
|
|
43
43
|
## Compatibility
|
|
44
44
|
|
|
45
|
-
| grape-oas | grape |
|
|
46
|
-
|
|
47
|
-
|
|
|
45
|
+
| grape-oas | grape | Ruby |
|
|
46
|
+
|-----------|-------|------|
|
|
47
|
+
| 1.5.x | >= 3.0 | >= 3.2 |
|
|
48
|
+
|
|
49
|
+
Entity and contract integrations are optional; install the gems your API uses.
|
|
48
50
|
|
|
49
51
|
## Installation
|
|
50
52
|
|
|
@@ -52,11 +54,12 @@ Grape::OAS is built around a **DTO (Data Transfer Object) architecture** that se
|
|
|
52
54
|
gem 'grape-oas'
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
For entity support:
|
|
57
|
+
For entity or contract support, add the relevant gems:
|
|
56
58
|
|
|
57
59
|
```ruby
|
|
58
|
-
gem 'grape-entity'
|
|
59
|
-
gem 'dry-
|
|
60
|
+
gem 'grape-entity' # Grape::Entity response schemas
|
|
61
|
+
gem 'dry-validation' # Dry::Validation::Contract request schemas
|
|
62
|
+
gem 'dry-schema' # Standalone Dry::Schema request schemas
|
|
60
63
|
```
|
|
61
64
|
|
|
62
65
|
## Quick Start
|
|
@@ -82,10 +85,15 @@ end
|
|
|
82
85
|
```
|
|
83
86
|
|
|
84
87
|
Documentation available at:
|
|
88
|
+
|
|
85
89
|
- `/swagger_doc` - OpenAPI 3.0 (default)
|
|
86
90
|
- `/swagger_doc?oas=2` - OpenAPI 2.0
|
|
87
91
|
- `/swagger_doc?oas=3.1` - OpenAPI 3.1
|
|
88
92
|
|
|
93
|
+
Documentation routes are accessible over HTTP but excluded from the generated
|
|
94
|
+
specification by default. Set `hide_documentation_path: false` to include them.
|
|
95
|
+
To disable the endpoints, omit or conditionally call `add_oas_documentation`.
|
|
96
|
+
|
|
89
97
|
### Manual Generation
|
|
90
98
|
|
|
91
99
|
```ruby
|
|
@@ -96,17 +104,26 @@ puts JSON.pretty_generate(spec)
|
|
|
96
104
|
|
|
97
105
|
### Rake Tasks
|
|
98
106
|
|
|
107
|
+
Load your API before registering the tasks:
|
|
108
|
+
|
|
99
109
|
```ruby
|
|
100
110
|
# In Rakefile
|
|
101
|
-
require 'grape_oas
|
|
111
|
+
require 'grape_oas'
|
|
112
|
+
require 'grape_oas/rake/oas_tasks'
|
|
113
|
+
require_relative 'app/api' # Defines MyAPI; adjust to your application's path
|
|
114
|
+
|
|
115
|
+
GrapeOAS::Rake::OasTasks.new(MyAPI)
|
|
102
116
|
```
|
|
103
117
|
|
|
104
118
|
```bash
|
|
105
|
-
|
|
119
|
+
mkdir -p spec
|
|
120
|
+
bundle exec rake oas:generate version=oas31 output=spec/openapi.json
|
|
106
121
|
```
|
|
107
122
|
|
|
108
123
|
## Documentation
|
|
109
124
|
|
|
125
|
+
Upgrading an existing application? Read [UPGRADING.md](UPGRADING.md).
|
|
126
|
+
|
|
110
127
|
| Document | Description |
|
|
111
128
|
|----------|-------------|
|
|
112
129
|
| [Configuration](docs/CONFIGURATION.md) | All configuration options |
|
|
@@ -202,7 +219,7 @@ schema = GrapeOAS.generate(app: API, schema_type: :custom)
|
|
|
202
219
|
git clone https://github.com/numbata/grape-oas.git
|
|
203
220
|
cd grape-oas
|
|
204
221
|
bin/setup
|
|
205
|
-
bundle exec rake
|
|
222
|
+
bundle exec rake
|
|
206
223
|
```
|
|
207
224
|
|
|
208
225
|
## Contributing
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Upgrading grape-oas
|
|
2
|
+
|
|
3
|
+
### Upgrading to >= 1.5.0
|
|
4
|
+
|
|
5
|
+
When upgrading from 1.4.0, regenerate your OpenAPI documents and review the diff before regenerating clients.
|
|
6
|
+
This release corrects several schema shapes and changes how documentation routes
|
|
7
|
+
are mounted. See [CHANGELOG.md](CHANGELOG.md) for the full list of changes.
|
|
8
|
+
|
|
9
|
+
#### Documentation routes are mounted even when hidden
|
|
10
|
+
|
|
11
|
+
`hide_documentation_path: true` previously prevented documentation routes from
|
|
12
|
+
being mounted. It now mounts them and hides them from the generated specification.
|
|
13
|
+
It is also the new default.
|
|
14
|
+
|
|
15
|
+
If you used this option to disable documentation in an environment, conditionally
|
|
16
|
+
omit the DSL call instead:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
class API < Grape::API
|
|
20
|
+
# Declare your API routes here.
|
|
21
|
+
|
|
22
|
+
add_oas_documentation if ENV["ENABLE_API_DOCS"] == "true"
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To include the documentation routes in the generated specification, use
|
|
27
|
+
`add_oas_documentation(hide_documentation_path: false)`. Hiding a route from the
|
|
28
|
+
specification does not restrict HTTP access to it.
|
|
29
|
+
|
|
30
|
+
#### Generic integers no longer imply a 32-bit format
|
|
31
|
+
|
|
32
|
+
Generic `Integer` and `"integer"` declarations now emit `type: integer` without
|
|
33
|
+
the previously inferred `format: int32`. This can change the integer type selected
|
|
34
|
+
by a client generator. Declare a width explicitly when your API requires it:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
params do
|
|
38
|
+
requires :count, type: Integer, documentation: { format: "int32" }
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For entity exposures, use
|
|
43
|
+
`documentation: { type: "integer", format: "int64" }` (or `"int32"`).
|
|
44
|
+
|
|
45
|
+
#### Entity exposures use the type resolver registry
|
|
46
|
+
|
|
47
|
+
Entity exposures now consult `GrapeOAS.type_resolvers`. Existing custom resolvers
|
|
48
|
+
can therefore affect response schemas as well as request parameters. Review their
|
|
49
|
+
`handles?` predicates and compare generated entity schemas after upgrading.
|
|
50
|
+
|
|
51
|
+
The registry also has migration-relevant changes:
|
|
52
|
+
|
|
53
|
+
- Replace registry calls to `handles?(type)` with
|
|
54
|
+
`registered_resolver_for?(type)`; `handles?` now emits a deprecation warning.
|
|
55
|
+
Custom resolver classes still implement their own `handles?` method.
|
|
56
|
+
- `build_schema(type)` now returns a fallback string schema when no registered
|
|
57
|
+
resolver supplies a schema, including after `clear`. Do not use a `nil` result
|
|
58
|
+
to detect unsupported types. `registered_resolver_for?` checks whether a
|
|
59
|
+
registered resolver claims the type; it does not guarantee that the resolver
|
|
60
|
+
will return a schema.
|
|
61
|
+
- A resolver returning `nil` from `build_schema` now lets the registry try later
|
|
62
|
+
resolvers before using the fallback.
|
|
63
|
+
- `find` is now private. Use `registered_resolver_for?` for a membership check or
|
|
64
|
+
`build_schema` to resolve a type. To inspect the first matching resolver, use
|
|
65
|
+
`GrapeOAS.type_resolvers.to_a.find { |resolver| resolver.handles?(type) }`.
|
|
66
|
+
|
|
67
|
+
#### Explicit media types affect OAS2 form generation
|
|
68
|
+
|
|
69
|
+
Route `consumes:` and `produces:` declarations now override inferred media types
|
|
70
|
+
independently. Check existing declarations for stale values.
|
|
71
|
+
|
|
72
|
+
For OAS2 operations consuming only `application/x-www-form-urlencoded` or
|
|
73
|
+
`multipart/form-data`, request properties now become `formData` parameters.
|
|
74
|
+
Complex form fields (objects, compositions, or referenced array items) raise
|
|
75
|
+
`ArgumentError` during generation. Use OAS3 to describe those requests, or simplify
|
|
76
|
+
the form schema if that accurately reflects your API. Primitive fields, files,
|
|
77
|
+
and arrays of primitives remain supported.
|
|
78
|
+
|
|
79
|
+
#### Generated schemas reflect corrected types and requiredness
|
|
80
|
+
|
|
81
|
+
Review schema snapshots and generated clients for these corrections:
|
|
82
|
+
|
|
83
|
+
- **Nullability:** OAS2 emits `x-nullable: true` by default for nullable schemas.
|
|
84
|
+
OAS3.1 inline parameters, request bodies, and responses now use its JSON Schema
|
|
85
|
+
null representation. Nullable OAS3 references and compositions retain
|
|
86
|
+
nullability, and null-union alternatives may now contain a bare `$ref` instead
|
|
87
|
+
of a single-element `allOf`. Update tooling that assumes the old wrapper shape.
|
|
88
|
+
- **Conditional parameters:** fields declared inside Grape `given` blocks are
|
|
89
|
+
optional in the generated schema unless also required unconditionally.
|
|
90
|
+
Conditional fields alone no longer make the request body required. Grape still
|
|
91
|
+
enforces conditions at runtime; the generated schema does not encode them.
|
|
92
|
+
- **Responses:** undocumented response schemas are now empty (`{}`) instead of
|
|
93
|
+
`type: string`. Declare a response entity if consumers need a concrete type.
|
|
94
|
+
Responses with status `1xx`, `204`, `205`, or `304` omit body schemas/content,
|
|
95
|
+
including declared entities and examples. Plain-entity responses with
|
|
96
|
+
`is_array: true` now correctly emit arrays.
|
|
97
|
+
- **Hidden entity fields:** exposures with `documentation: { hidden: true }` are
|
|
98
|
+
omitted. Remove that flag if the field should remain documented.
|
|
99
|
+
- **Versioned paths:** path-based Grape versions now appear as concrete path
|
|
100
|
+
segments instead of unresolved version parameters. Regenerate clients that
|
|
101
|
+
previously accepted the version as an argument.
|
|
102
|
+
|
|
103
|
+
New `schema_ref_name` and `oas2_composition_extensions` options are opt-in and
|
|
104
|
+
require no configuration changes for existing applications.
|
data/grape-oas.gemspec
CHANGED
|
@@ -24,7 +24,8 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
"rubygems_mfa_required" => "true"
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
spec.files = Dir["lib/**/*"
|
|
27
|
+
spec.files = Dir["lib/**/*"].reject { |path| File.directory?(path) } +
|
|
28
|
+
%w[README.md CHANGELOG.md UPGRADING.md LICENSE.txt grape-oas.gemspec]
|
|
28
29
|
|
|
29
30
|
spec.add_dependency "grape", ">= 3.0"
|
|
30
31
|
spec.add_dependency "zeitwerk"
|
|
@@ -11,7 +11,7 @@ module GrapeOAS
|
|
|
11
11
|
class API < Node
|
|
12
12
|
attr_accessor :title, :version, :paths, :servers, :tag_defs, :components,
|
|
13
13
|
:host, :base_path, :schemes, :security_definitions, :security,
|
|
14
|
-
:registered_schemas, :suppress_default_error_response, :nullable_strategy
|
|
14
|
+
:registered_schemas, :suppress_default_error_response, :nullable_strategy, :oas2_composition_extensions
|
|
15
15
|
|
|
16
16
|
def initialize(title:, version:)
|
|
17
17
|
super()
|
|
@@ -29,6 +29,7 @@ module GrapeOAS
|
|
|
29
29
|
@registered_schemas = []
|
|
30
30
|
@suppress_default_error_response = false
|
|
31
31
|
@nullable_strategy = nil
|
|
32
|
+
@oas2_composition_extensions = false
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def add_path(path)
|
|
@@ -20,6 +20,7 @@ module GrapeOAS
|
|
|
20
20
|
@api.registered_schemas = build_registered_schemas(options[:models])
|
|
21
21
|
@api.suppress_default_error_response = options[:suppress_default_error_response] || false
|
|
22
22
|
@api.nullable_strategy = options[:nullable_strategy]
|
|
23
|
+
@api.oas2_composition_extensions = options[:oas2_composition_extensions] == true
|
|
23
24
|
|
|
24
25
|
@namespace_filter = options[:namespace]
|
|
25
26
|
@apis = []
|
|
@@ -53,12 +54,12 @@ module GrapeOAS
|
|
|
53
54
|
def build_registered_schemas(models)
|
|
54
55
|
return [] unless models
|
|
55
56
|
|
|
56
|
-
Array(models).
|
|
57
|
+
Array(models).filter_map do |model|
|
|
57
58
|
model = model.constantize if model.is_a?(String)
|
|
58
59
|
GrapeOAS.introspectors.build_schema(model, stack: [], registry: {})
|
|
59
60
|
rescue StandardError
|
|
60
61
|
nil
|
|
61
|
-
end
|
|
62
|
+
end
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
end
|
|
@@ -8,6 +8,14 @@ module GrapeOAS
|
|
|
8
8
|
module ContentTypeResolver
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
def explicit_media_types(key)
|
|
12
|
+
declared = route.options[key] || route.options.dig(:documentation, key)
|
|
13
|
+
mimes = Array(declared).filter_map do |mime|
|
|
14
|
+
mime.to_s.include?("/") ? mime.to_s : Grape::ContentTypes.content_types_for(nil)[mime.to_s.to_sym]
|
|
15
|
+
end
|
|
16
|
+
mimes.empty? ? nil : mimes.uniq
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
def resolve_content_types
|
|
12
20
|
default_format = route_default_format_from_route || default_format_from_app_or_api
|
|
13
21
|
content_types = route_content_types_from_route
|
|
@@ -24,7 +32,7 @@ module GrapeOAS
|
|
|
24
32
|
|
|
25
33
|
mimes << mime_for_format(default_format) if mimes.empty? && default_format
|
|
26
34
|
|
|
27
|
-
mimes = mimes.
|
|
35
|
+
mimes = mimes.filter_map { |m| normalize_mime(m) }
|
|
28
36
|
mimes.empty? ? [Constants::MimeTypes::JSON] : mimes.uniq
|
|
29
37
|
end
|
|
30
38
|
|
|
@@ -24,6 +24,19 @@ module GrapeOAS
|
|
|
24
24
|
OasUtilities.extract_extensions(hash)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Extracts nullable flag from a documentation hash.
|
|
28
|
+
#
|
|
29
|
+
# @param doc [Hash] the documentation hash
|
|
30
|
+
# @return [Boolean] true if nullable
|
|
31
|
+
def self.extract_nullable(doc)
|
|
32
|
+
doc[:nullable] || (doc[:x].is_a?(Hash) && doc[:x][:nullable]) || false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Instance method version
|
|
36
|
+
def extract_nullable(doc)
|
|
37
|
+
OasUtilities.extract_nullable(doc)
|
|
38
|
+
end
|
|
39
|
+
|
|
27
40
|
# Converts a CamelCase string to snake_case.
|
|
28
41
|
#
|
|
29
42
|
# @param str [String] the string to convert
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModelBuilders
|
|
5
|
+
module Concerns
|
|
6
|
+
module RouteValidations
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Grape 3.x snapshots validators on `inheritable_setting.route[:saved_validations]`.
|
|
10
|
+
# Grape 4.0 renamed that to `#route_validations` / `route[:validations]` (grape#2811).
|
|
11
|
+
def grape_route_validations(setting)
|
|
12
|
+
if setting.respond_to?(:route_validations)
|
|
13
|
+
validations = setting.route_validations
|
|
14
|
+
return validations if validations.is_a?(Array)
|
|
15
|
+
end
|
|
16
|
+
return unless setting.respond_to?(:route)
|
|
17
|
+
|
|
18
|
+
route_store = setting.route
|
|
19
|
+
return unless route_store.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
route_store[:saved_validations] || route_store[:validations]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -14,6 +14,7 @@ module GrapeOAS
|
|
|
14
14
|
module TypeResolver
|
|
15
15
|
TYPED_ARRAY_PATTERN = Constants::TypePatterns::TYPED_ARRAY
|
|
16
16
|
MULTI_TYPE_PATTERN = Constants::TypePatterns::MULTI_TYPE
|
|
17
|
+
VARIANT_COLLECTION_PATTERN = Constants::TypePatterns::VARIANT_COLLECTION
|
|
17
18
|
|
|
18
19
|
# Resolves a Ruby class or type name to its OpenAPI schema type string.
|
|
19
20
|
# Handles both Ruby classes (Integer, Float) and string type names ("integer", "float").
|
|
@@ -39,8 +40,9 @@ module GrapeOAS
|
|
|
39
40
|
|
|
40
41
|
type_str = type.to_s
|
|
41
42
|
|
|
42
|
-
# Handle Grape's typed array notation like "[String]"
|
|
43
|
-
|
|
43
|
+
# Handle Grape's typed array notation like "[String]" / "Array[String]"
|
|
44
|
+
# and Grape 3.3+ variant collections like "Array[Integer, String]".
|
|
45
|
+
return Constants::SchemaTypes::ARRAY if type_str.match?(TYPED_ARRAY_PATTERN) || type_str.match?(VARIANT_COLLECTION_PATTERN)
|
|
44
46
|
|
|
45
47
|
# Handle string/symbol type names
|
|
46
48
|
Constants.primitive_type(type_str) || Constants::SchemaTypes::STRING
|
|
@@ -49,8 +49,7 @@ module GrapeOAS
|
|
|
49
49
|
slug = route
|
|
50
50
|
.pattern
|
|
51
51
|
.origin
|
|
52
|
-
.gsub(/[^a-z0-9]+/i, "_")
|
|
53
|
-
.gsub(/_+/, "_")
|
|
52
|
+
.gsub(/[^a-z0-9]+/i, "_").squeeze("_")
|
|
54
53
|
.sub(/^_|_$/, "")
|
|
55
54
|
|
|
56
55
|
"#{http_method}_#{slug}"
|
|
@@ -125,11 +124,11 @@ module GrapeOAS
|
|
|
125
124
|
end
|
|
126
125
|
|
|
127
126
|
def consumes
|
|
128
|
-
resolve_content_types
|
|
127
|
+
explicit_media_types(:consumes) || resolve_content_types
|
|
129
128
|
end
|
|
130
129
|
|
|
131
130
|
def produces
|
|
132
|
-
resolve_content_types
|
|
131
|
+
explicit_media_types(:produces) || resolve_content_types
|
|
133
132
|
end
|
|
134
133
|
|
|
135
134
|
def operation_extensions
|
|
@@ -10,6 +10,11 @@ module GrapeOAS
|
|
|
10
10
|
PATH_PARAMETER_PATTERN = %r{(?<=/):(?<param>[^/]+)}
|
|
11
11
|
private_constant :PATH_PARAMETER_PATTERN
|
|
12
12
|
|
|
13
|
+
# Matches Grape's wildcard segments: /?*param or /*param
|
|
14
|
+
# The optional ? before * is Grape's syntax for an optional leading slash
|
|
15
|
+
WILDCARD_PARAMETER_PATTERN = %r{\??\*(?<param>[^/()]+)}
|
|
16
|
+
private_constant :WILDCARD_PARAMETER_PATTERN
|
|
17
|
+
|
|
13
18
|
NORMALIZED_PLACEHOLDER = /\{[^}]+\}/
|
|
14
19
|
private_constant :NORMALIZED_PLACEHOLDER
|
|
15
20
|
|
|
@@ -28,7 +33,7 @@ module GrapeOAS
|
|
|
28
33
|
@routes.each_with_object({}) do |route, api_routes|
|
|
29
34
|
next if skip_route?(route)
|
|
30
35
|
|
|
31
|
-
route_path = sanitize_path(route.path)
|
|
36
|
+
route_path = sanitize_path(route.path, route: route)
|
|
32
37
|
normalized = normalize_template(route_path)
|
|
33
38
|
|
|
34
39
|
canonical_info = canonical_paths[normalized]
|
|
@@ -79,7 +84,7 @@ module GrapeOAS
|
|
|
79
84
|
def filtered_by_namespace?(route)
|
|
80
85
|
return false unless namespace_filter
|
|
81
86
|
|
|
82
|
-
route_path = sanitize_path(route.path)
|
|
87
|
+
route_path = sanitize_path(route.path, route: route)
|
|
83
88
|
namespace_prefix = namespace_filter.start_with?("/") ? namespace_filter : "/#{namespace_filter}"
|
|
84
89
|
|
|
85
90
|
# Match exact namespace or namespace followed by / or {
|
|
@@ -95,10 +100,32 @@ module GrapeOAS
|
|
|
95
100
|
.build
|
|
96
101
|
end
|
|
97
102
|
|
|
98
|
-
def sanitize_path(path)
|
|
99
|
-
path
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
def sanitize_path(path, route: nil)
|
|
104
|
+
sanitized = path.gsub(EXTENSION_PATTERN, "") # Remove format extensions like (.json)
|
|
105
|
+
.gsub(WILDCARD_PARAMETER_PATTERN, "{\\k<param>}") # Replace *param / /?*param with {param}
|
|
106
|
+
.gsub(PATH_PARAMETER_PATTERN, "{\\k<param>}") # Replace :param with {param}
|
|
107
|
+
|
|
108
|
+
concrete_version = concrete_path_version(route) if route
|
|
109
|
+
return sanitized unless concrete_version
|
|
110
|
+
|
|
111
|
+
# Path versioning owns {version}; Grape rejects duplicate capture names.
|
|
112
|
+
sanitized.sub("{version}", concrete_version)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def concrete_path_version(route)
|
|
116
|
+
setting = route.app.inheritable_setting
|
|
117
|
+
version_options = if setting.respond_to?(:version_options)
|
|
118
|
+
setting.version_options
|
|
119
|
+
else
|
|
120
|
+
setting.namespace_inheritable[:version_options]
|
|
121
|
+
end
|
|
122
|
+
return nil unless version_options&.to_h&.dig(:using) == :path
|
|
123
|
+
|
|
124
|
+
values = Array(route.version)
|
|
125
|
+
return nil unless values.length == 1
|
|
126
|
+
|
|
127
|
+
value = values.first.to_s
|
|
128
|
+
value.empty? ? nil : value
|
|
102
129
|
end
|
|
103
130
|
|
|
104
131
|
def normalize_template(path)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module GrapeOAS
|
|
4
4
|
module ApiModelBuilders
|
|
5
5
|
class Request
|
|
6
|
+
include Concerns::RouteValidations
|
|
6
7
|
include Concerns::TypeResolver
|
|
7
8
|
include Concerns::OasUtilities
|
|
8
9
|
|
|
@@ -96,7 +97,9 @@ module GrapeOAS
|
|
|
96
97
|
|
|
97
98
|
# Find contract from Grape's contract storage locations.
|
|
98
99
|
# Contracts can be defined in several ways:
|
|
99
|
-
# 1. Via `contract MyContract` DSL -
|
|
100
|
+
# 1. Via `contract MyContract` DSL - stored on the endpoint's inheritable
|
|
101
|
+
# settings (`route_validations` on Grape 4.0, `route[:saved_validations]`
|
|
102
|
+
# on Grape 3.x)
|
|
100
103
|
# 2. Via `desc "...", contract: MyContract` - stores in route.options[:contract]
|
|
101
104
|
# 3. Via `desc "...", schema: MySchema` - stores in route.options[:schema]
|
|
102
105
|
# 4. Via route.settings[:contract] - used by mounted APIs or legacy configuration
|
|
@@ -116,22 +119,15 @@ module GrapeOAS
|
|
|
116
119
|
end
|
|
117
120
|
|
|
118
121
|
# Extract contract from Grape's native contract() DSL storage location.
|
|
119
|
-
# When using `contract MyContract` in Grape DSL, the contract is stored
|
|
120
|
-
#
|
|
121
|
-
# This is a point-in-time copy specific to this endpoint, ensuring each route
|
|
122
|
+
# When using `contract MyContract` in Grape DSL, the contract is stored as
|
|
123
|
+
# a point-in-time copy specific to this endpoint, ensuring each route
|
|
122
124
|
# gets only its own contract even when multiple routes define different contracts.
|
|
123
125
|
#
|
|
124
126
|
# @return [Object, nil] The contract instance or nil if not found
|
|
125
127
|
def extract_contract_from_grape_validations
|
|
126
128
|
return unless route.respond_to?(:app) && route.app.respond_to?(:inheritable_setting)
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
return unless setting.respond_to?(:route)
|
|
130
|
-
|
|
131
|
-
# Use route[:saved_validations] which contains only the validations
|
|
132
|
-
# for this specific endpoint (point-in-time copy), not the shared
|
|
133
|
-
# namespace_stackable[:validations] which contains all validators for the API class
|
|
134
|
-
validations = setting.route[:saved_validations]
|
|
130
|
+
validations = grape_route_validations(route.app.inheritable_setting)
|
|
135
131
|
return unless validations.is_a?(Array)
|
|
136
132
|
|
|
137
133
|
# Find ContractScopeValidator which holds the Dry contract/schema.
|
|
@@ -147,10 +143,7 @@ module GrapeOAS
|
|
|
147
143
|
|
|
148
144
|
return v.dig(:opts, :schema)
|
|
149
145
|
when Grape::Validations::Validators::ContractScopeValidator
|
|
150
|
-
|
|
151
|
-
# so instance_variable_get is the only way to access the schema.
|
|
152
|
-
# TODO: use v.schema once ruby-grape/grape#2657 restores the accessor.
|
|
153
|
-
schema = v.instance_variable_get(:@schema)
|
|
146
|
+
schema = contract_schema_from(v)
|
|
154
147
|
GrapeOAS.logger&.warn("ContractScopeValidator found but @schema is nil") if schema.nil?
|
|
155
148
|
return schema
|
|
156
149
|
end
|
|
@@ -159,6 +152,11 @@ module GrapeOAS
|
|
|
159
152
|
nil
|
|
160
153
|
end
|
|
161
154
|
|
|
155
|
+
def contract_schema_from(validator)
|
|
156
|
+
schema = validator.schema if validator.respond_to?(:schema)
|
|
157
|
+
schema || validator.instance_variable_get(:@schema)
|
|
158
|
+
end
|
|
159
|
+
|
|
162
160
|
def build_contract_schema
|
|
163
161
|
contract = find_contract
|
|
164
162
|
return unless contract
|
|
@@ -265,7 +263,7 @@ module GrapeOAS
|
|
|
265
263
|
end
|
|
266
264
|
|
|
267
265
|
def path_param_names
|
|
268
|
-
names = route.path
|
|
266
|
+
names = RequestParams.path_param_names(route.path)
|
|
269
267
|
mapped_names = path_param_name_map ? path_param_name_map.values : []
|
|
270
268
|
(names + mapped_names).map(&:to_s).uniq
|
|
271
269
|
end
|