grape-oas 1.0.2 → 1.1.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 +92 -75
- data/README.md +25 -1
- data/lib/grape_oas/api_model/parameter.rb +5 -2
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +5 -2
- data/lib/grape_oas/api_model_builders/request.rb +125 -9
- data/lib/grape_oas/api_model_builders/request_params.rb +6 -5
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +59 -8
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +77 -2
- data/lib/grape_oas/api_model_builders/response.rb +63 -6
- data/lib/grape_oas/api_model_builders/response_parsers/http_codes_parser.rb +114 -10
- data/lib/grape_oas/constants.rb +32 -19
- data/lib/grape_oas/exporter/oas2_schema.rb +0 -3
- data/lib/grape_oas/exporter/oas3/parameter.rb +2 -0
- data/lib/grape_oas/exporter/oas3_schema.rb +0 -3
- data/lib/grape_oas/introspectors/dry_introspector.rb +19 -10
- data/lib/grape_oas/introspectors/dry_introspector_support/argument_extractor.rb +57 -6
- data/lib/grape_oas/introspectors/dry_introspector_support/inheritance_handler.rb +17 -5
- data/lib/grape_oas/introspectors/dry_introspector_support/predicate_handler.rb +38 -12
- data/lib/grape_oas/introspectors/dry_introspector_support/rule_index.rb +196 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/type_schema_builder.rb +89 -17
- data/lib/grape_oas/introspectors/dry_introspector_support/type_unwrapper.rb +19 -0
- data/lib/grape_oas/introspectors/entity_introspector.rb +0 -8
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +6 -3
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d956b648ef9686c13b90db7a16bba4038bf505ed069634131ade6cac17b3f8bf
|
|
4
|
+
data.tar.gz: 740148b7d4a9cb0d09675d53465a6e4b798b5c042245070b7de145401ac47820
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9e134fc9ad362b0b2aa505d7dcab8b24cf35697356365fda1d0bc20f081893b675811b2205aca46507aeb6da66ea547cee8bb3a01a5c6de301f097bb7990948
|
|
7
|
+
data.tar.gz: 3a2c13601c299600272d5ff3e5cccb648f658b61b0a3d1a8cf43c66b0bb35cadae8979f74bffd820ec217ccbd18e8eb65b25c28de9f6b4b67a1972914b524f92
|
data/CHANGELOG.md
CHANGED
|
@@ -2,22 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.
|
|
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.1.0] - 2026-01-23
|
|
8
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- [#30](https://github.com/numbata/grape-oas/pull/30): Add support for Grape's native `contract` DSL resolution when building request schemas - [@numbata](https://github.com/numbata).
|
|
13
|
+
- [#28](https://github.com/numbata/grape-oas/pull/28): support nested dry-contracts query parameters with style & explode - [@slbug](https://github.com/slbug).
|
|
14
|
+
- [#24](https://github.com/numbata/grape-oas/pull/24): Properly parse desc blocks with responses [@slbug](https://github.com/slbug).
|
|
15
|
+
- [#27](https://github.com/numbata/grape-oas/pull/27): Add release workflow - [@numbata](https://github.com/numbata).
|
|
16
|
+
- [#26](https://github.com/numbata/grape-oas/pull/26): Add danger validation - [@numbata](https://github.com/numbata).
|
|
17
|
+
- [#23](https://github.com/numbata/grape-oas/pull/23): Add oneOf support for response schemas - [@slbug](https://github.com/slbug).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- [#33](https://github.com/numbata/grape-oas/pull/33): Improve schema generation: add format hints, optimize nullable types, fix enum handling for arrays and oneOf - [@numbata](https://github.com/numbata).
|
|
22
|
+
- [#34](https://github.com/numbata/grape-oas/pull/34): Convert numeric `included_in?` ranges to min/max constraints instead of enum - [@numbata](https://github.com/numbata).
|
|
23
|
+
- [#31](https://github.com/numbata/grape-oas/pull/31): Fix: prefer `using:` option over `documentation: { type: "object" }` - [@numbata](https://github.com/numbata).
|
|
24
|
+
- [#22](https://github.com/numbata/grape-oas/pull/22): Handle boolean types in dry introspector - [@slbug](https://github.com/slbug).
|
|
25
|
+
|
|
26
|
+
## [1.0.3] - 2025-12-23
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- [#21](https://github.com/numbata/grape-oas/pull/21): Remove unnecessary require\_relative in favor of Zeitwerk autoloadin - [@numbata](https://github.com/numbata).
|
|
31
|
+
- [#17](https://github.com/numbata/grape-oas/pull/17): Support for nested rules and predicates in dry-schema introspection - [@slbug](https://github.com/slbug).
|
|
32
|
+
- [#20](https://github.com/numbata/grape-oas/pull/20): Use annotation for coverage report - [@numbata](https://github.com/numbata).
|
|
33
|
+
- [#18](https://github.com/numbata/grape-oas/pull/18): Support for range in size? predicate `required(:tags).value(:array, size?: 1..10).each(:string)` - [@slbug](https://github.com/slbug).
|
|
34
|
+
- [#19](https://github.com/numbata/grape-oas/pull/19): Temporary disable memory profiler workflow for PRs - [@numbata](https://github.com/numbata).
|
|
9
35
|
|
|
10
36
|
## [1.0.2] - 2025-12-15
|
|
11
37
|
|
|
12
|
-
###
|
|
38
|
+
### Fixed
|
|
13
39
|
|
|
14
40
|
- [#14](https://github.com/numbata/grape-oas/pull/14): Fix Response and ParamSchemaBuilder to use introspector registry instead of directly instantiating EntityIntrospector - [@numbata](https://github.com/numbata).
|
|
15
41
|
|
|
16
42
|
## [1.0.1] - 2025-12-15
|
|
17
43
|
|
|
18
|
-
###
|
|
44
|
+
### Fixed
|
|
19
45
|
|
|
20
|
-
- [#8](https://github.com/numbata/grape-oas/pull/8): Add OAS2 parameter schema constraint export with enum normalization and retain zero-valued constraints across OAS exporters
|
|
46
|
+
- [#8](https://github.com/numbata/grape-oas/pull/8): Add OAS2 parameter schema constraint export with enum normalization and retain zero-valued constraints across OAS exporters - [@numbata](https://github.com/numbata).
|
|
21
47
|
- [#9](https://github.com/numbata/grape-oas/pull/9): Treat GET/HEAD/DELETE as bodyless by default via shared constants and tests - [@numbata](https://github.com/numbata).
|
|
22
48
|
- [#10](https://github.com/numbata/grape-oas/pull/10): Add grape-swagger compatible `in:` location syntax for parameters alongside `param_type` - [@numbata](https://github.com/numbata).
|
|
23
49
|
- [#11](https://github.com/numbata/grape-oas/pull/11): Flatten nested Hash params to bracket-notation query params for GET/HEAD/DELETE requests - [@numbata](https://github.com/numbata).
|
|
@@ -27,74 +53,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
27
53
|
|
|
28
54
|
### Added
|
|
29
55
|
|
|
30
|
-
|
|
31
|
-
- OpenAPI specification generation for Grape APIs
|
|
32
|
-
- Support for OpenAPI 2.0 (Swagger), 3.0, and 3.1 specifications
|
|
33
|
-
- `GrapeOAS.generate(app:, schema_type:)` for programmatic generation
|
|
34
|
-
- `add_oas_documentation` DSL for mounting documentation endpoint
|
|
35
|
-
- `add_swagger_documentation` compatibility shim for grape-swagger migration
|
|
36
|
-
- Query parameter `?oas=2|3|3.1` for version selection at runtime
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- `
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
- Multiple
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
- `
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
- `
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
- Exporter registry - register custom exporters via `GrapeOAS.exporters.register(ExporterClass, as: :alias)`
|
|
93
|
-
|
|
94
|
-
### Documentation
|
|
95
|
-
- README with full usage examples
|
|
96
|
-
- `docs/MIGRATING_FROM_GRAPE_SWAGGER.md` - detailed migration guide
|
|
97
|
-
- `docs/ARCHITECTURE.md` - system architecture overview
|
|
98
|
-
- `docs/INTROSPECTORS.md` - introspector system documentation
|
|
99
|
-
- `docs/EXPORTERS.md` - exporter system documentation
|
|
100
|
-
- `docs/API_MODEL.md` - internal API model reference
|
|
56
|
+
- Core Features
|
|
57
|
+
- OpenAPI specification generation for Grape APIs
|
|
58
|
+
- Support for OpenAPI 2.0 (Swagger), 3.0, and 3.1 specifications
|
|
59
|
+
- `GrapeOAS.generate(app:, schema_type:)` for programmatic generation
|
|
60
|
+
- `add_oas_documentation` DSL for mounting documentation endpoint
|
|
61
|
+
- `add_swagger_documentation` compatibility shim for grape-swagger migration
|
|
62
|
+
- Query parameter `?oas=2|3|3.1` for version selection at runtime
|
|
63
|
+
- Entity Support
|
|
64
|
+
- Built-in Grape::Entity introspection (no separate gem needed)
|
|
65
|
+
- Dry::Validation::Contract and Dry::Struct support
|
|
66
|
+
- Entity inheritance with `allOf` composition
|
|
67
|
+
- Polymorphism support with `discriminator`
|
|
68
|
+
- Sum types (`|`) converted to `anyOf`
|
|
69
|
+
- Circular reference handling with `$ref`
|
|
70
|
+
- Parameter Documentation
|
|
71
|
+
- All Grape parameter types (String, Integer, Float, Boolean, Date, DateTime, Array, Hash, File)
|
|
72
|
+
- Nested parameters (Hash with block)
|
|
73
|
+
- Array parameters with item types (`Array[String]`, `Array[Integer]`)
|
|
74
|
+
- Multi-type parameters (`types: [String, Integer]`)
|
|
75
|
+
- Parameter validation constraints (values, regexp, minimum, maximum, etc.)
|
|
76
|
+
- Parameter hiding (`documentation: { hidden: true }`)
|
|
77
|
+
- `collectionFormat` support for OAS2 arrays
|
|
78
|
+
- Response Documentation
|
|
79
|
+
- `success` and `failure` response definitions
|
|
80
|
+
- Multiple success/failure status codes
|
|
81
|
+
- Response headers
|
|
82
|
+
- Response examples
|
|
83
|
+
- Multiple present responses with `as:` key combination
|
|
84
|
+
- Root element wrapping support
|
|
85
|
+
- `suppress_default_error_response` option
|
|
86
|
+
- Endpoint Documentation
|
|
87
|
+
- `desc` block syntax with detail, tags, deprecated, consumes, produces
|
|
88
|
+
- Endpoint hiding (`hidden: true` or lambda)
|
|
89
|
+
- `body_name` for custom body parameter naming (OAS2)
|
|
90
|
+
- Request body for GET/HEAD/DELETE when explicitly enabled
|
|
91
|
+
- Operation extensions (`x-*` properties)
|
|
92
|
+
- Configuration
|
|
93
|
+
- Global options: host, base\_path, schemes, servers, consumes, produces
|
|
94
|
+
- Info object: title, version, description, contact, license, terms\_of\_service
|
|
95
|
+
- Security definitions (API key, OAuth2, Bearer)
|
|
96
|
+
- Tag definitions with descriptions
|
|
97
|
+
- `models` option to pre-register entities
|
|
98
|
+
- Namespace filtering for partial schema generation
|
|
99
|
+
- URL-based namespace filtering for mounted docs (`/swagger_doc/users`)
|
|
100
|
+
- Tag filtering to only include used tags
|
|
101
|
+
- Rake Tasks
|
|
102
|
+
- `grape_oas:generate[API,schema_type,output_path]` for file generation
|
|
103
|
+
- `grape_oas:validate[file_path]` for spec validation
|
|
104
|
+
- Migration Support
|
|
105
|
+
- Comprehensive migration guide from grape-swagger
|
|
106
|
+
- Feature parity documentation
|
|
107
|
+
- Compatibility shim for `add_swagger_documentation`
|
|
108
|
+
- Extensibility
|
|
109
|
+
- Introspector registry - register custom introspectors via `GrapeOAS.introspectors.register()`
|
|
110
|
+
- Exporter registry - register custom exporters via `GrapeOAS.exporters.register(ExporterClass, as: :alias)`
|
|
111
|
+
- Documentation
|
|
112
|
+
- README with full usage examples
|
|
113
|
+
- `docs/MIGRATING_FROM_GRAPE_SWAGGER.md` - detailed migration guide
|
|
114
|
+
- `docs/ARCHITECTURE.md` - system architecture overview
|
|
115
|
+
- `docs/INTROSPECTORS.md` - introspector system documentation
|
|
116
|
+
- `docs/EXPORTERS.md` - exporter system documentation
|
|
117
|
+
- `docs/API_MODEL.md` - internal API model reference
|
data/README.md
CHANGED
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
|
|
6
6
|
OpenAPI Specification (OAS) documentation generator for [Grape](https://github.com/ruby-grape/grape) APIs. Supports OpenAPI 2.0 (Swagger), 3.0, and 3.1 specifications.
|
|
7
7
|
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
- [Why Grape::OAS?](#why-grapeoas)
|
|
11
|
+
- [Features](#features)
|
|
12
|
+
- [Compatibility](#compatibility)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Mount Documentation Endpoint](#mount-documentation-endpoint)
|
|
16
|
+
- [Manual Generation](#manual-generation)
|
|
17
|
+
- [Rake Tasks](#rake-tasks)
|
|
18
|
+
- [Documentation](#documentation)
|
|
19
|
+
- [Basic Usage](#basic-usage)
|
|
20
|
+
- [Documenting Endpoints](#documenting-endpoints)
|
|
21
|
+
- [Response Documentation](#response-documentation)
|
|
22
|
+
- [Entity Definition](#entity-definition)
|
|
23
|
+
- [Extensibility](#extensibility)
|
|
24
|
+
- [Custom Introspectors](#custom-introspectors)
|
|
25
|
+
- [Custom Exporters](#custom-exporters)
|
|
26
|
+
- [Related Projects](#related-projects)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
- [Contributing](#contributing)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
|
|
8
32
|
## Why Grape::OAS?
|
|
9
33
|
|
|
10
34
|
Grape::OAS is built around a **DTO (Data Transfer Object) architecture** that separates collecting API metadata from generating schemas. This clean separation makes the codebase easier to reason about and enables support for multiple output formats (OAS 2.0, 3.0, 3.1) from the same API definition.
|
|
@@ -164,7 +188,7 @@ schema = GrapeOAS.generate(app: API, schema_type: :custom)
|
|
|
164
188
|
| [grape-entity](https://github.com/ruby-grape/grape-entity) | Entity exposure for Grape APIs |
|
|
165
189
|
| [grape-swagger](https://github.com/ruby-grape/grape-swagger) | OpenAPI documentation for Grape APIs |
|
|
166
190
|
| [grape-swagger-entity](https://github.com/ruby-grape/grape-swagger-entity) | grape-swagger adapter for grape-entity |
|
|
167
|
-
| [
|
|
191
|
+
| [oas\_grape](https://github.com/a-chacon/oas_grape) | Another OpenAPI 3.1 generator for Grape |
|
|
168
192
|
|
|
169
193
|
## Development
|
|
170
194
|
|
|
@@ -8,9 +8,10 @@ module GrapeOAS
|
|
|
8
8
|
# @see https://swagger.io/specification/
|
|
9
9
|
# @see GrapeOAS::ApiModel::Operation
|
|
10
10
|
class Parameter < Node
|
|
11
|
-
attr_accessor :location, :name, :required, :description, :schema, :collection_format
|
|
11
|
+
attr_accessor :location, :name, :required, :description, :schema, :collection_format, :style, :explode
|
|
12
12
|
|
|
13
|
-
def initialize(location:, name:, schema:, required: false, description: nil, collection_format: nil
|
|
13
|
+
def initialize(location:, name:, schema:, required: false, description: nil, collection_format: nil, style: nil,
|
|
14
|
+
explode: nil)
|
|
14
15
|
super()
|
|
15
16
|
@location = location.to_s
|
|
16
17
|
@name = name
|
|
@@ -18,6 +19,8 @@ module GrapeOAS
|
|
|
18
19
|
@schema = schema
|
|
19
20
|
@description = description
|
|
20
21
|
@collection_format = collection_format
|
|
22
|
+
@style = style&.to_s
|
|
23
|
+
@explode = explode
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
end
|
|
@@ -41,7 +41,7 @@ module GrapeOAS
|
|
|
41
41
|
return Constants::SchemaTypes::ARRAY if type_str.match?(TYPED_ARRAY_PATTERN)
|
|
42
42
|
|
|
43
43
|
# Handle string/symbol type names
|
|
44
|
-
Constants
|
|
44
|
+
Constants.primitive_type(type_str) || Constants::SchemaTypes::STRING
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Checks if type is Grape's Boolean class (handles dynamic loading)
|
|
@@ -102,7 +102,10 @@ module GrapeOAS
|
|
|
102
102
|
elsif primitive == Hash
|
|
103
103
|
ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
104
104
|
else
|
|
105
|
-
ApiModel::Schema.new(
|
|
105
|
+
ApiModel::Schema.new(
|
|
106
|
+
type: resolve_schema_type(primitive),
|
|
107
|
+
format: Constants.format_for_type(primitive),
|
|
108
|
+
)
|
|
106
109
|
end
|
|
107
110
|
end
|
|
108
111
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bigdecimal"
|
|
4
|
-
require_relative "concerns/type_resolver"
|
|
5
4
|
|
|
6
5
|
module GrapeOAS
|
|
7
6
|
module ApiModelBuilders
|
|
@@ -24,7 +23,15 @@ module GrapeOAS
|
|
|
24
23
|
.build
|
|
25
24
|
|
|
26
25
|
contract_schema = build_contract_schema
|
|
27
|
-
|
|
26
|
+
|
|
27
|
+
# For GET/HEAD/DELETE requests, convert contract schema to query parameters
|
|
28
|
+
# instead of putting it in request body, UNLESS request_body is explicitly enabled
|
|
29
|
+
if contract_schema && should_convert_contract_to_query_params?
|
|
30
|
+
contract_params = convert_contract_schema_to_params(contract_schema)
|
|
31
|
+
operation.add_parameters(*contract_params)
|
|
32
|
+
elsif contract_schema
|
|
33
|
+
body_schema = contract_schema
|
|
34
|
+
end
|
|
28
35
|
|
|
29
36
|
operation.add_parameters(*route_params)
|
|
30
37
|
append_request_body(body_schema) unless body_schema.empty?
|
|
@@ -46,12 +53,10 @@ module GrapeOAS
|
|
|
46
53
|
|
|
47
54
|
# Set canonical_name if not already set (e.g., DryIntrospector may have set it for polymorphism)
|
|
48
55
|
if body_schema.respond_to?(:canonical_name) && body_schema.canonical_name.nil?
|
|
49
|
-
|
|
50
|
-
contract_class = route.options[:contract] || route.options[:schema] || settings[:contract]
|
|
56
|
+
contract = find_contract
|
|
51
57
|
|
|
52
|
-
if
|
|
53
|
-
|
|
54
|
-
elsif contract_class # some other contract (e.g., Dry); keep inline
|
|
58
|
+
if contract
|
|
59
|
+
# Dry contracts are kept inline (no canonical_name)
|
|
55
60
|
# no-op
|
|
56
61
|
elsif body_schema.properties.values.any? { |prop| prop.respond_to?(:canonical_name) && prop.canonical_name }
|
|
57
62
|
# keep entity/property refs intact; don't override
|
|
@@ -91,9 +96,64 @@ module GrapeOAS
|
|
|
91
96
|
extract_extensions(mt)
|
|
92
97
|
end
|
|
93
98
|
|
|
99
|
+
# Find contract from Grape's contract storage locations.
|
|
100
|
+
# Contracts can be defined in several ways:
|
|
101
|
+
# 1. Via `contract MyContract` DSL - stores in inheritable_setting.route[:saved_validations]
|
|
102
|
+
# 2. Via `desc "...", contract: MyContract` - stores in route.options[:contract]
|
|
103
|
+
# 3. Via `desc "...", schema: MySchema` - stores in route.options[:schema]
|
|
104
|
+
# 4. Via route.settings[:contract] - used by mounted APIs or legacy configuration
|
|
105
|
+
#
|
|
106
|
+
# @return [Object, nil] The contract instance or nil if not found
|
|
107
|
+
def find_contract
|
|
108
|
+
# Check route options first (from desc "...", contract: MyContract or schema: MySchema)
|
|
109
|
+
contract = route.options[:contract] || route.options[:schema]
|
|
110
|
+
return contract if contract
|
|
111
|
+
|
|
112
|
+
# Check route settings (mounted APIs or legacy configuration)
|
|
113
|
+
contract = route.settings[:contract] if route.respond_to?(:settings)
|
|
114
|
+
return contract if contract
|
|
115
|
+
|
|
116
|
+
# Check Grape's native contract() DSL storage
|
|
117
|
+
extract_contract_from_grape_validations
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Extract contract from Grape's native contract() DSL storage location.
|
|
121
|
+
# When using `contract MyContract` in Grape DSL, the contract is stored in
|
|
122
|
+
# route.app.inheritable_setting.route[:saved_validations] as validator options.
|
|
123
|
+
# This is a point-in-time copy specific to this endpoint, ensuring each route
|
|
124
|
+
# gets only its own contract even when multiple routes define different contracts.
|
|
125
|
+
#
|
|
126
|
+
# @return [Object, nil] The contract instance or nil if not found
|
|
127
|
+
def extract_contract_from_grape_validations
|
|
128
|
+
return unless route.respond_to?(:app) && route.app.respond_to?(:inheritable_setting)
|
|
129
|
+
|
|
130
|
+
setting = route.app.inheritable_setting
|
|
131
|
+
return unless setting.respond_to?(:route)
|
|
132
|
+
|
|
133
|
+
# Use route[:saved_validations] which contains only the validations
|
|
134
|
+
# for this specific endpoint (point-in-time copy), not the shared
|
|
135
|
+
# namespace_stackable[:validations] which contains all validators for the API class
|
|
136
|
+
validations = setting.route[:saved_validations]
|
|
137
|
+
return unless validations.is_a?(Array)
|
|
138
|
+
|
|
139
|
+
# Find ContractScopeValidator which holds the Dry contract/schema
|
|
140
|
+
contract_validation = validations.find do |v|
|
|
141
|
+
next unless v.is_a?(Hash)
|
|
142
|
+
|
|
143
|
+
validator_class = v[:validator_class]
|
|
144
|
+
validator_class.is_a?(Class) &&
|
|
145
|
+
defined?(Grape::Validations::Validators::ContractScopeValidator) &&
|
|
146
|
+
validator_class <= Grape::Validations::Validators::ContractScopeValidator
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return unless contract_validation
|
|
150
|
+
|
|
151
|
+
# The contract instance is stored in opts[:schema]
|
|
152
|
+
contract_validation.dig(:opts, :schema)
|
|
153
|
+
end
|
|
154
|
+
|
|
94
155
|
def build_contract_schema
|
|
95
|
-
|
|
96
|
-
contract = route.options[:contract] || settings[:contract]
|
|
156
|
+
contract = find_contract
|
|
97
157
|
return unless contract
|
|
98
158
|
|
|
99
159
|
schema_obj = if contract.respond_to?(:schema)
|
|
@@ -299,6 +359,62 @@ module GrapeOAS
|
|
|
299
359
|
rescue NoMethodError
|
|
300
360
|
nil
|
|
301
361
|
end
|
|
362
|
+
|
|
363
|
+
def convert_contract_schema_to_params(schema)
|
|
364
|
+
return [] unless schema.respond_to?(:properties)
|
|
365
|
+
|
|
366
|
+
params = []
|
|
367
|
+
param_docs = contract_param_documentation
|
|
368
|
+
path_params = path_param_names
|
|
369
|
+
|
|
370
|
+
schema.properties.each do |name, prop_schema|
|
|
371
|
+
name_s = name.to_s
|
|
372
|
+
next if path_params.include?(name_s)
|
|
373
|
+
|
|
374
|
+
required = schema.required&.any? { |r| r.to_s == name_s } || false
|
|
375
|
+
doc = param_docs[name_s] || {}
|
|
376
|
+
params << build_query_parameter(name_s, prop_schema, required, doc)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
params
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def should_convert_contract_to_query_params?
|
|
383
|
+
http_method = operation.http_method.to_s.downcase
|
|
384
|
+
return false unless Constants::HttpMethods::BODYLESS_HTTP_METHODS.include?(http_method)
|
|
385
|
+
|
|
386
|
+
!(route.options.dig(:documentation, :request_body) || route.options[:request_body])
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def build_query_parameter(name, schema, required, doc = {})
|
|
390
|
+
style = doc.fetch(:style) { doc["style"] }
|
|
391
|
+
explode = doc.fetch(:explode) { doc["explode"] }
|
|
392
|
+
description = doc[:desc] || doc[:description] || schema.description
|
|
393
|
+
ApiModel::Parameter.new(
|
|
394
|
+
location: "query",
|
|
395
|
+
name: name,
|
|
396
|
+
required: required,
|
|
397
|
+
schema: schema,
|
|
398
|
+
description: description,
|
|
399
|
+
style: style,
|
|
400
|
+
explode: explode,
|
|
401
|
+
)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def contract_param_documentation
|
|
405
|
+
params = documentation_options[:params]
|
|
406
|
+
return {} unless params.is_a?(Hash)
|
|
407
|
+
|
|
408
|
+
params.each_with_object({}) do |(key, value), acc|
|
|
409
|
+
acc[key.to_s] = value.is_a?(Hash) ? value : {}
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def path_param_names
|
|
414
|
+
names = route.path.scan(RequestParams::ROUTE_PARAM_REGEX)
|
|
415
|
+
mapped_names = path_param_name_map ? path_param_name_map.values : []
|
|
416
|
+
(names + mapped_names).map(&:to_s).uniq
|
|
417
|
+
end
|
|
302
418
|
end
|
|
303
419
|
end
|
|
304
420
|
end
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "request_params_support/param_location_resolver"
|
|
4
|
-
require_relative "request_params_support/param_schema_builder"
|
|
5
|
-
require_relative "request_params_support/schema_enhancer"
|
|
6
|
-
require_relative "request_params_support/nested_params_builder"
|
|
7
|
-
|
|
8
3
|
module GrapeOAS
|
|
9
4
|
module ApiModelBuilders
|
|
10
5
|
class RequestParams
|
|
@@ -137,6 +132,10 @@ module GrapeOAS
|
|
|
137
132
|
end
|
|
138
133
|
|
|
139
134
|
def build_parameter(name, location, required, schema, spec)
|
|
135
|
+
doc = spec[:documentation] || {}
|
|
136
|
+
style = doc.fetch(:style) { doc["style"] }
|
|
137
|
+
explode = doc.fetch(:explode) { doc["explode"] }
|
|
138
|
+
|
|
140
139
|
ApiModel::Parameter.new(
|
|
141
140
|
location: location,
|
|
142
141
|
name: name,
|
|
@@ -144,6 +143,8 @@ module GrapeOAS
|
|
|
144
143
|
schema: schema,
|
|
145
144
|
description: spec[:documentation]&.dig(:desc) || spec[:desc],
|
|
146
145
|
collection_format: extract_collection_format(spec),
|
|
146
|
+
style: style,
|
|
147
|
+
explode: explode,
|
|
147
148
|
)
|
|
148
149
|
end
|
|
149
150
|
|
|
@@ -55,7 +55,11 @@ module GrapeOAS
|
|
|
55
55
|
def build_entity_array_schema(spec, raw_type, doc_type)
|
|
56
56
|
entity_type = resolve_entity_class(extract_entity_type_from_array(spec, raw_type, doc_type))
|
|
57
57
|
items = entity_type ? GrapeOAS.introspectors.build_schema(entity_type, stack: [], registry: {}) : nil
|
|
58
|
-
|
|
58
|
+
fallback_type = extract_entity_type_from_array(spec, raw_type)
|
|
59
|
+
items ||= ApiModel::Schema.new(
|
|
60
|
+
type: sanitize_type(fallback_type),
|
|
61
|
+
format: Constants.format_for_type(fallback_type),
|
|
62
|
+
)
|
|
59
63
|
ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items)
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -76,7 +80,10 @@ module GrapeOAS
|
|
|
76
80
|
items_schema = if entity
|
|
77
81
|
GrapeOAS.introspectors.build_schema(entity, stack: [], registry: {})
|
|
78
82
|
else
|
|
79
|
-
ApiModel::Schema.new(
|
|
83
|
+
ApiModel::Schema.new(
|
|
84
|
+
type: sanitize_type(items_type),
|
|
85
|
+
format: Constants.format_for_type(items_type),
|
|
86
|
+
)
|
|
80
87
|
end
|
|
81
88
|
ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
82
89
|
end
|
|
@@ -88,18 +95,59 @@ module GrapeOAS
|
|
|
88
95
|
)
|
|
89
96
|
end
|
|
90
97
|
|
|
91
|
-
# Builds
|
|
98
|
+
# Builds schema for Grape's multi-type notation like "[String, Integer]"
|
|
99
|
+
# Special case: "[Type, NilClass]" becomes a nullable Type (not oneOf)
|
|
92
100
|
def build_multi_type_schema(type)
|
|
93
101
|
type_names = extract_multi_types(type)
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
|
|
103
|
+
# OPTIMIZE: [Type, Nil] becomes nullable Type instead of oneOf
|
|
104
|
+
if nullable_type_pair?(type_names)
|
|
105
|
+
non_nil_type = type_names.find { |t| !nil_type_name?(t) }
|
|
106
|
+
return ApiModel::Schema.new(
|
|
107
|
+
type: resolve_schema_type(non_nil_type),
|
|
108
|
+
format: Constants.format_for_type(non_nil_type),
|
|
109
|
+
nullable: true,
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# General case: build oneOf schema
|
|
114
|
+
# Filter out nil types - OpenAPI 3.0 uses nullable property instead
|
|
115
|
+
has_nil_type = type_names.any? { |t| nil_type_name?(t) }
|
|
116
|
+
non_nil_types = type_names.reject { |t| nil_type_name?(t) }
|
|
117
|
+
|
|
118
|
+
schemas = non_nil_types.map do |type_name|
|
|
119
|
+
ApiModel::Schema.new(
|
|
120
|
+
type: resolve_schema_type(type_name),
|
|
121
|
+
format: Constants.format_for_type(type_name),
|
|
122
|
+
)
|
|
96
123
|
end
|
|
97
|
-
ApiModel::Schema.new(one_of: schemas)
|
|
124
|
+
ApiModel::Schema.new(one_of: schemas, nullable: has_nil_type ? true : nil)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Checks if type_names is a pair of [SomeType, NilType]
|
|
128
|
+
def nullable_type_pair?(type_names)
|
|
129
|
+
return false unless type_names.size == 2
|
|
130
|
+
|
|
131
|
+
type_names.one? { |t| nil_type_name?(t) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Checks if the type name represents a nil/null type
|
|
135
|
+
def nil_type_name?(type_name)
|
|
136
|
+
normalized = type_name.to_s
|
|
137
|
+
# Match common nil type patterns:
|
|
138
|
+
# - "NilClass" (Ruby's nil type)
|
|
139
|
+
# - "Nil" (shorthand)
|
|
140
|
+
# - "Foo::Nil", "Types::Nil" (namespaced nil types)
|
|
141
|
+
normalized == "NilClass" ||
|
|
142
|
+
normalized == "Nil" ||
|
|
143
|
+
normalized.end_with?("::Nil")
|
|
98
144
|
end
|
|
99
145
|
|
|
100
146
|
def build_primitive_schema(raw_type, doc)
|
|
147
|
+
schema_type = sanitize_type(raw_type)
|
|
101
148
|
ApiModel::Schema.new(
|
|
102
|
-
type:
|
|
149
|
+
type: schema_type,
|
|
150
|
+
format: Constants.format_for_type(raw_type),
|
|
103
151
|
description: doc[:desc],
|
|
104
152
|
)
|
|
105
153
|
end
|
|
@@ -138,7 +186,10 @@ module GrapeOAS
|
|
|
138
186
|
items_type = resolve_schema_type(member_type)
|
|
139
187
|
ApiModel::Schema.new(
|
|
140
188
|
type: Constants::SchemaTypes::ARRAY,
|
|
141
|
-
items: ApiModel::Schema.new(
|
|
189
|
+
items: ApiModel::Schema.new(
|
|
190
|
+
type: items_type,
|
|
191
|
+
format: Constants.format_for_type(member_type),
|
|
192
|
+
),
|
|
142
193
|
)
|
|
143
194
|
end
|
|
144
195
|
|