grape-oas 1.0.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 683920ecd4af815a8dd0a58b8e36b5feb4be21030566842474f33a1c804fa008
4
- data.tar.gz: c5c3035f9038878338b836cb09b096164723e012af2066b120b63535f062f5ed
3
+ metadata.gz: d956b648ef9686c13b90db7a16bba4038bf505ed069634131ade6cac17b3f8bf
4
+ data.tar.gz: 740148b7d4a9cb0d09675d53465a6e4b798b5c042245070b7de145401ac47820
5
5
  SHA512:
6
- metadata.gz: 820d58e88ffe10735ef72a72c41b4feeaedbcfb6d7a5cb3a3f5f5a50d8c91c6d7db5203fce40289511d9adc57ca605ec1ca3a2f6158ce043e58baf89c30a9d3a
7
- data.tar.gz: ac0862ed30858730c19d7afc1592a53ca2aef1f14c789c407e535ab482f4ea497ab342b2967730aed4ffdf542497f445766def91d22e19c8810c0dded172b80d
6
+ metadata.gz: a9e134fc9ad362b0b2aa505d7dcab8b24cf35697356365fda1d0bc20f081893b675811b2205aca46507aeb6da66ea547cee8bb3a01a5c6de301f097bb7990948
7
+ data.tar.gz: 3a2c13601c299600272d5ff3e5cccb648f658b61b0a3d1a8cf43c66b0bb35cadae8979f74bffd820ec217ccbd18e8eb65b25c28de9f6b4b67a1972914b524f92
data/CHANGELOG.md CHANGED
@@ -2,29 +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.0.0/),
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
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
+
8
26
  ## [1.0.3] - 2025-12-23
9
27
 
10
- * Your contribution here
11
- - [#21](https://github.com/numbata/grape-oas/pull/21): Remove unnecessary require_relative in favor of Zeitwerk autoloadin [@numbata](https://github.com/numbata)
12
- - [#17](https://github.com/numbata/grape-oas/pull/17): Support for nested rules and predicates in dry-schema introspection [@slbug](https://github.com/slbug)
13
- - [#20](https://github.com/numbata/grape-oas/pull/20): Use annotation for coverage report [@numbata](https://github.com/numbata)
14
- - [#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)
15
- - [#19](https://github.com/numbata/grape-oas/pull/19): Temporary disable memory profiler workflow for PRs [@numbata](https://github.com/numbata).
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).
16
35
 
17
36
  ## [1.0.2] - 2025-12-15
18
37
 
19
- ### Fixes
38
+ ### Fixed
20
39
 
21
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).
22
41
 
23
42
  ## [1.0.1] - 2025-12-15
24
43
 
25
- ### Fixes
44
+ ### Fixed
26
45
 
27
- - [#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).
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).
28
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).
29
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).
30
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).
@@ -34,74 +53,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
53
 
35
54
  ### Added
36
55
 
37
- #### Core Features
38
- - OpenAPI specification generation for Grape APIs
39
- - Support for OpenAPI 2.0 (Swagger), 3.0, and 3.1 specifications
40
- - `GrapeOAS.generate(app:, schema_type:)` for programmatic generation
41
- - `add_oas_documentation` DSL for mounting documentation endpoint
42
- - `add_swagger_documentation` compatibility shim for grape-swagger migration
43
- - Query parameter `?oas=2|3|3.1` for version selection at runtime
44
-
45
- #### Entity Support
46
- - Built-in Grape::Entity introspection (no separate gem needed)
47
- - Dry::Validation::Contract and Dry::Struct support
48
- - Entity inheritance with `allOf` composition
49
- - Polymorphism support with `discriminator`
50
- - Sum types (`|`) converted to `anyOf`
51
- - Circular reference handling with `$ref`
52
-
53
- #### Parameter Documentation
54
- - All Grape parameter types (String, Integer, Float, Boolean, Date, DateTime, Array, Hash, File)
55
- - Nested parameters (Hash with block)
56
- - Array parameters with item types (`Array[String]`, `Array[Integer]`)
57
- - Multi-type parameters (`types: [String, Integer]`)
58
- - Parameter validation constraints (values, regexp, minimum, maximum, etc.)
59
- - Parameter hiding (`documentation: { hidden: true }`)
60
- - `collectionFormat` support for OAS2 arrays
61
-
62
- #### Response Documentation
63
- - `success` and `failure` response definitions
64
- - Multiple success/failure status codes
65
- - Response headers
66
- - Response examples
67
- - Multiple present responses with `as:` key combination
68
- - Root element wrapping support
69
- - `suppress_default_error_response` option
70
-
71
- #### Endpoint Documentation
72
- - `desc` block syntax with detail, tags, deprecated, consumes, produces
73
- - Endpoint hiding (`hidden: true` or lambda)
74
- - `body_name` for custom body parameter naming (OAS2)
75
- - Request body for GET/HEAD/DELETE when explicitly enabled
76
- - Operation extensions (`x-*` properties)
77
-
78
- #### Configuration
79
- - Global options: host, base_path, schemes, servers, consumes, produces
80
- - Info object: title, version, description, contact, license, terms_of_service
81
- - Security definitions (API key, OAuth2, Bearer)
82
- - Tag definitions with descriptions
83
- - `models` option to pre-register entities
84
- - Namespace filtering for partial schema generation
85
- - URL-based namespace filtering for mounted docs (`/swagger_doc/users`)
86
- - Tag filtering to only include used tags
87
-
88
- #### Rake Tasks
89
- - `grape_oas:generate[API,schema_type,output_path]` for file generation
90
- - `grape_oas:validate[file_path]` for spec validation
91
-
92
- #### Migration Support
93
- - Comprehensive migration guide from grape-swagger
94
- - Feature parity documentation
95
- - Compatibility shim for `add_swagger_documentation`
96
-
97
- #### Extensibility
98
- - Introspector registry - register custom introspectors via `GrapeOAS.introspectors.register()`
99
- - Exporter registry - register custom exporters via `GrapeOAS.exporters.register(ExporterClass, as: :alias)`
100
-
101
- ### Documentation
102
- - README with full usage examples
103
- - `docs/MIGRATING_FROM_GRAPE_SWAGGER.md` - detailed migration guide
104
- - `docs/ARCHITECTURE.md` - system architecture overview
105
- - `docs/INTROSPECTORS.md` - introspector system documentation
106
- - `docs/EXPORTERS.md` - exporter system documentation
107
- - `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
- | [oas_grape](https://github.com/a-chacon/oas_grape) | Another OpenAPI 3.1 generator for Grape |
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::PRIMITIVE_TYPE_MAPPING.fetch(type_str.downcase, Constants::SchemaTypes::STRING)
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(type: resolve_schema_type(primitive))
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
 
@@ -23,7 +23,15 @@ module GrapeOAS
23
23
  .build
24
24
 
25
25
  contract_schema = build_contract_schema
26
- body_schema = contract_schema if contract_schema
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
27
35
 
28
36
  operation.add_parameters(*route_params)
29
37
  append_request_body(body_schema) unless body_schema.empty?
@@ -45,12 +53,10 @@ module GrapeOAS
45
53
 
46
54
  # Set canonical_name if not already set (e.g., DryIntrospector may have set it for polymorphism)
47
55
  if body_schema.respond_to?(:canonical_name) && body_schema.canonical_name.nil?
48
- settings = route.respond_to?(:settings) ? route.settings : {}
49
- contract_class = route.options[:contract] || route.options[:schema] || settings[:contract]
56
+ contract = find_contract
50
57
 
51
- if contract_class.is_a?(Class) && defined?(Menti::Endpoint::Schema) && contract_class < Menti::Endpoint::Schema
52
- body_schema.canonical_name = contract_class.name
53
- elsif contract_class # some other contract (e.g., Dry); keep inline
58
+ if contract
59
+ # Dry contracts are kept inline (no canonical_name)
54
60
  # no-op
55
61
  elsif body_schema.properties.values.any? { |prop| prop.respond_to?(:canonical_name) && prop.canonical_name }
56
62
  # keep entity/property refs intact; don't override
@@ -90,9 +96,64 @@ module GrapeOAS
90
96
  extract_extensions(mt)
91
97
  end
92
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
+
93
155
  def build_contract_schema
94
- settings = route.respond_to?(:settings) ? route.settings : {}
95
- contract = route.options[:contract] || settings[:contract]
156
+ contract = find_contract
96
157
  return unless contract
97
158
 
98
159
  schema_obj = if contract.respond_to?(:schema)
@@ -298,6 +359,62 @@ module GrapeOAS
298
359
  rescue NoMethodError
299
360
  nil
300
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
301
418
  end
302
419
  end
303
420
  end
@@ -132,6 +132,10 @@ module GrapeOAS
132
132
  end
133
133
 
134
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
+
135
139
  ApiModel::Parameter.new(
136
140
  location: location,
137
141
  name: name,
@@ -139,6 +143,8 @@ module GrapeOAS
139
143
  schema: schema,
140
144
  description: spec[:documentation]&.dig(:desc) || spec[:desc],
141
145
  collection_format: extract_collection_format(spec),
146
+ style: style,
147
+ explode: explode,
142
148
  )
143
149
  end
144
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
- items ||= ApiModel::Schema.new(type: sanitize_type(extract_entity_type_from_array(spec, raw_type)))
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(type: sanitize_type(items_type))
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 oneOf schema for Grape's multi-type notation like "[String, Integer]"
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
- schemas = type_names.map do |type_name|
95
- ApiModel::Schema.new(type: resolve_schema_type(type_name))
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: sanitize_type(raw_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(type: items_type),
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