grape-oas 1.5.0 → 1.6.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 +38 -0
- data/UPGRADING.md +38 -0
- data/lib/grape_oas/api_model_builder.rb +5 -4
- data/lib/grape_oas/api_model_builders/request.rb +8 -13
- data/lib/grape_oas/api_model_builders/request_params.rb +38 -41
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +0 -1
- data/lib/grape_oas/api_model_builders/request_params_support/param_location_resolver.rb +33 -27
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +7 -26
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +1 -1
- data/lib/grape_oas/api_model_builders/response.rb +5 -1
- data/lib/grape_oas/api_model_builders/response_parsers/base.rb +17 -0
- data/lib/grape_oas/api_model_builders/response_parsers/default_response_parser.rb +4 -7
- data/lib/grape_oas/api_model_builders/response_parsers/documentation_responses_parser.rb +5 -2
- data/lib/grape_oas/api_model_builders/response_parsers/http_codes_parser.rb +96 -28
- data/lib/grape_oas/constants.rb +14 -0
- data/lib/grape_oas/exporter/concerns/enum_normalizer.rb +35 -0
- data/lib/grape_oas/exporter/concerns/schema_indexer.rb +18 -1
- data/lib/grape_oas/exporter/oas2/parameter.rb +55 -30
- data/lib/grape_oas/exporter/oas2/schema.rb +9 -26
- data/lib/grape_oas/exporter/oas3/schema.rb +26 -39
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +40 -6
- data/lib/grape_oas/type_resolvers/array_resolver.rb +7 -14
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +19 -0
- 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: 2d771c74ede3e601d65419900f26aa0ad36d785a649e09184a9e6755f4749fe7
|
|
4
|
+
data.tar.gz: 55bef62d1b83ccbd6f57cc469ec6ff81aea4b00c38fef892542fa5bc2f0588b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d3f8cc1eeee662d60fdd0574970ed350416dcc9770df2536b7d7d7f017292bcc4338f47cb203cd2d486189d32d614b56d12c74efa83f4c52eabfb69df171b0d
|
|
7
|
+
data.tar.gz: 86d67570e32b7aafa0290963801c2b0f673fce6d1dde815b93cc2fd5ee8794c898541987382b7910b217e62b58807fdd5f9a6e940d427f82371a23b9ecbfaf84
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ 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.6.0] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- [#89](https://github.com/numbata/grape-oas/pull/89): Add `GrapeOAS.entity_exposure_required_default` to make entity exposures without explicit required metadata optional in generated schemas - [@abeljim8am](https://github.com/abeljim8am).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- [#158](https://github.com/numbata/grape-oas/pull/158): Emit bare item references for nullable arrays of entities instead of redundant `allOf` wrappers - [@numbata](https://github.com/numbata).
|
|
17
|
+
- [#72](https://github.com/numbata/grape-oas/pull/72): Infer `201` for POST success responses without an explicit status; honor entity `default_status:` for all methods, omitting content for bodyless statuses - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
18
|
+
- [#154](https://github.com/numbata/grape-oas/pull/154): Keep nullable enum members aligned with the emitted null representation across OAS versions - [@numbata](https://github.com/numbata).
|
|
19
|
+
- [#153](https://github.com/numbata/grape-oas/pull/153): Reject colliding schema reference names instead of silently overwriting definitions - [@numbata](https://github.com/numbata).
|
|
20
|
+
- [#106](https://github.com/numbata/grape-oas/pull/106): Apply `nullable` to the array, not its items, on `is_array: true` entity exposures - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- [#155](https://github.com/numbata/grape-oas/pull/155): Fail generation when explicit `models:` entries cannot be resolved or introspected instead of silently omitting them - [@numbata](https://github.com/numbata).
|
|
25
|
+
|
|
26
|
+
## [1.5.1] - 2026-09-16
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- [#110](https://github.com/numbata/grape-oas/pull/110): Honor Grape `desc` `default` / `default_response` as OAS `responses.default` - [@numbata](https://github.com/numbata).
|
|
31
|
+
- [#136](https://github.com/numbata/grape-oas/pull/136): Preserve top-level Grape `desc:` metadata on request-body properties - [@numbata](https://github.com/numbata).
|
|
32
|
+
- [#140](https://github.com/numbata/grape-oas/pull/140): Preserve nullable entity scalars documented with `types:` - [@numbata](https://github.com/numbata).
|
|
33
|
+
- [#142](https://github.com/numbata/grape-oas/pull/142): Normalize symbolic response status keys to numeric HTTP codes - [@numbata](https://github.com/numbata).
|
|
34
|
+
- [#145](https://github.com/numbata/grape-oas/pull/145): Preserve explicitly body-located GET, HEAD, and DELETE parameters - [@numbata](https://github.com/numbata).
|
|
35
|
+
- [#146](https://github.com/numbata/grape-oas/pull/146): Preserve declared path metadata inside nested body parameter groups and always mark path parameters as required, per OAS, even when their Grape declaration is optional - [@numbata](https://github.com/numbata).
|
|
36
|
+
- [#149](https://github.com/numbata/grape-oas/pull/149): Preserve explicitly query/header/path-located nested parameters on POST, PUT, and PATCH routes; reject unrecognized `param_type:`/`in:` values and `path` locations without a matching route capture instead of emitting invalid output; drop OAS 2.0-invalid `cookie` parameters with a warning instead of emitting them - [@numbata](https://github.com/numbata).
|
|
37
|
+
- [#152](https://github.com/numbata/grape-oas/pull/152): Emit only spec-valid OAS 2.0 non-body parameters and warn when unsupported schemas are dropped - [@numbata](https://github.com/numbata).
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- [#145](https://github.com/numbata/grape-oas/pull/145): Apply route-level body opt-in consistently to flat parameters and `body_name` contracts - [@numbata](https://github.com/numbata).
|
|
42
|
+
|
|
43
|
+
- [#108](https://github.com/numbata/grape-oas/pull/108): Bump actions/checkout from 6 to 7 - [@dependabot[bot]](https://github.com/dependabot[bot]).
|
|
44
|
+
- [#137](https://github.com/numbata/grape-oas/pull/137): Test against Grape 4.0 in CI - [@numbata](https://github.com/numbata).
|
|
45
|
+
|
|
8
46
|
## [1.5.0] - 2026-09-07
|
|
9
47
|
|
|
10
48
|
### Added
|
data/UPGRADING.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Upgrading grape-oas
|
|
2
2
|
|
|
3
|
+
### Upgrading to >= 1.6.0
|
|
4
|
+
|
|
5
|
+
#### Schema reference-name collisions stop generation
|
|
6
|
+
|
|
7
|
+
Distinct schemas that map to the same OAS reference name now raise
|
|
8
|
+
`ArgumentError` instead of silently overwriting one schema. The error identifies
|
|
9
|
+
both canonical names and the shared reference name.
|
|
10
|
+
|
|
11
|
+
Rename one of the schemas or configure a mapping that produces unique names for
|
|
12
|
+
your application. For example, dots distinguish Ruby namespaces from underscores
|
|
13
|
+
while remaining valid in OAS reference names:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
GrapeOAS.schema_ref_name = ->(name) { name.gsub("::", ".") }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
#### Invalid explicit models stop generation
|
|
20
|
+
|
|
21
|
+
Entries passed through `models:` that cannot be resolved or introspected now
|
|
22
|
+
stop generation instead of silently disappearing from the generated document.
|
|
23
|
+
Exceptions from custom introspectors propagate with their original type and
|
|
24
|
+
backtrace.
|
|
25
|
+
|
|
26
|
+
Check model names, remove unsupported entries, and fix errors raised by custom
|
|
27
|
+
introspectors before generating the document again.
|
|
28
|
+
|
|
29
|
+
#### POST success responses use consistent defaults
|
|
30
|
+
|
|
31
|
+
POST `success:` declarations without an explicit response code or
|
|
32
|
+
`default_status:` now infer status `201`. Entity response paths that previously
|
|
33
|
+
hardcoded `200` now use the same precedence. Undocumented POST responses already
|
|
34
|
+
inferred `201`.
|
|
35
|
+
|
|
36
|
+
If a POST endpoint returns `200`, set `code: 200` in the response declaration or
|
|
37
|
+
`default_status: 200` on the route. Explicit response codes take precedence over
|
|
38
|
+
`default_status:`. Entity responses now honor `default_status:` for every HTTP
|
|
39
|
+
method; bodyless statuses such as `204` omit response content.
|
|
40
|
+
|
|
3
41
|
### Upgrading to >= 1.5.0
|
|
4
42
|
|
|
5
43
|
When upgrading from 1.4.0, regenerate your OpenAPI documents and review the diff before regenerating clients.
|
|
@@ -54,11 +54,12 @@ module GrapeOAS
|
|
|
54
54
|
def build_registered_schemas(models)
|
|
55
55
|
return [] unless models
|
|
56
56
|
|
|
57
|
-
Array(models).
|
|
57
|
+
Array(models).map do |model|
|
|
58
58
|
model = model.constantize if model.is_a?(String)
|
|
59
|
-
GrapeOAS.introspectors.build_schema(model, stack: [], registry: {})
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
schema = GrapeOAS.introspectors.build_schema(model, stack: [], registry: {})
|
|
60
|
+
raise ArgumentError, "No introspector can build a schema for #{model.inspect}" unless schema
|
|
61
|
+
|
|
62
|
+
schema
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
end
|
|
@@ -17,9 +17,11 @@ module GrapeOAS
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def build
|
|
20
|
-
body_schema, route_params =
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
body_schema, route_params = RequestParams.new(
|
|
21
|
+
api: api,
|
|
22
|
+
route: route,
|
|
23
|
+
path_param_name_map: path_param_name_map,
|
|
24
|
+
).build
|
|
23
25
|
|
|
24
26
|
contract_schema = build_contract_schema
|
|
25
27
|
|
|
@@ -39,15 +41,6 @@ module GrapeOAS
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
43
|
def append_request_body(body_schema)
|
|
42
|
-
# OAS spec says GET/HEAD/DELETE "MAY ignore" request bodies
|
|
43
|
-
# Skip by default unless explicitly allowed via documentation option
|
|
44
|
-
http_method = operation.http_method.to_s.downcase
|
|
45
|
-
if Constants::HttpMethods::BODYLESS_HTTP_METHODS.include?(http_method)
|
|
46
|
-
allow_body = route.options.dig(:documentation, :request_body) ||
|
|
47
|
-
route.options[:request_body]
|
|
48
|
-
return unless allow_body
|
|
49
|
-
end
|
|
50
|
-
|
|
51
44
|
media_ext = media_type_extensions(Constants::MimeTypes::JSON)
|
|
52
45
|
|
|
53
46
|
# Set canonical_name if not already set (e.g., DryIntrospector may have set it for polymorphism)
|
|
@@ -235,7 +228,9 @@ module GrapeOAS
|
|
|
235
228
|
http_method = operation.http_method.to_s.downcase
|
|
236
229
|
return false unless Constants::HttpMethods::BODYLESS_HTTP_METHODS.include?(http_method)
|
|
237
230
|
|
|
238
|
-
|
|
231
|
+
# Parameter-level body annotations apply to Grape params only; contracts
|
|
232
|
+
# retain route-level body opt-in semantics.
|
|
233
|
+
!RequestParamsSupport::ParamLocationResolver.route_body_opted_in?(route)
|
|
239
234
|
end
|
|
240
235
|
|
|
241
236
|
def build_query_parameter(name, schema, required, doc = {})
|
|
@@ -27,22 +27,36 @@ module GrapeOAS
|
|
|
27
27
|
has_nested = all_params.keys.any? { |k| k.include?("[") }
|
|
28
28
|
|
|
29
29
|
if has_nested
|
|
30
|
-
build_with_nested_params(all_params, route_params)
|
|
30
|
+
body_schema, parameters = build_with_nested_params(all_params, route_params)
|
|
31
31
|
else
|
|
32
|
-
build_flat_params(all_params, route_params)
|
|
32
|
+
body_schema, parameters = build_flat_params(all_params, route_params)
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
[body_schema, parameters]
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
private
|
|
37
39
|
|
|
38
40
|
# Builds params when nested structures are detected.
|
|
39
41
|
def build_with_nested_params(all_params, route_params)
|
|
40
|
-
|
|
42
|
+
body_params = nested_body_params(all_params, route_params)
|
|
43
|
+
body_schema = nested_params_builder.build(body_params, path_params: route_params)
|
|
41
44
|
non_body_params = extract_non_body_params(all_params, route_params)
|
|
42
45
|
|
|
43
46
|
[body_schema, non_body_params]
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
def nested_body_params(all_params, route_params)
|
|
50
|
+
body_roots = all_params.filter_map do |name, spec|
|
|
51
|
+
next if name.include?("[")
|
|
52
|
+
|
|
53
|
+
location = location_resolver.resolve(name: name, spec: spec, route_params: route_params, route: route)
|
|
54
|
+
name if location == "body"
|
|
55
|
+
end.to_set
|
|
56
|
+
|
|
57
|
+
all_params.select { |name, _spec| body_roots.include?(name.split("[", 2).first) }
|
|
58
|
+
end
|
|
59
|
+
|
|
46
60
|
# Builds params for flat (non-nested) structures.
|
|
47
61
|
def build_flat_params(all_params, route_params)
|
|
48
62
|
body_schema = ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
@@ -72,33 +86,38 @@ module GrapeOAS
|
|
|
72
86
|
end
|
|
73
87
|
|
|
74
88
|
# Extracts non-body params (path, query, header) from flat params.
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
89
|
+
# Nested params (bracket notation, e.g. "tax_id[type]") are included as
|
|
90
|
+
# flat non-body parameters, taking their parent's resolved location,
|
|
91
|
+
# regardless of HTTP method — a nested Hash explicitly documented
|
|
92
|
+
# `in: "header"` on a write route (POST/PUT/PATCH) still belongs in
|
|
93
|
+
# the header, not the body.
|
|
78
94
|
def extract_non_body_params(all_params, route_params)
|
|
79
95
|
params = []
|
|
80
|
-
http_method = route.request_method.to_s.downcase
|
|
81
|
-
flatten_nested = should_flatten_nested_to_query?(http_method, all_params)
|
|
82
96
|
|
|
83
97
|
all_params.each do |name, spec|
|
|
84
98
|
# Skip hidden params
|
|
85
99
|
next if location_resolver.hidden_parameter?(spec)
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
if name.include?("[")
|
|
102
|
+
root = name.split("[", 2).first
|
|
103
|
+
root_spec = all_params[root] || {}
|
|
104
|
+
next if location_resolver.hidden_parameter?(root_spec)
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
root_location = location_resolver.resolve(name: root, spec: root_spec, route_params: route_params, route: route)
|
|
107
|
+
next if root_location == "body"
|
|
108
|
+
# A Hash root can itself be a real route capture (e.g. ":filter"),
|
|
109
|
+
# but that doesn't make its bracket children path segments too —
|
|
110
|
+
# only the root itself matches the URL template.
|
|
111
|
+
next if root_location == "path"
|
|
94
112
|
|
|
95
|
-
params << build_parameter(name,
|
|
113
|
+
params << build_parameter(name, root_location, spec[:required] || false, schema_builder.build(spec), spec)
|
|
96
114
|
next
|
|
97
115
|
end
|
|
98
116
|
|
|
99
|
-
# Skip Hash type params
|
|
100
|
-
# or via body schema
|
|
101
|
-
|
|
117
|
+
# Skip Hash type params with nested children of their own (they're
|
|
118
|
+
# handled via the nested bracket branch above or via body schema).
|
|
119
|
+
# A childless Hash falls through to the generic path below.
|
|
120
|
+
next if location_resolver.hash_param?(spec) && all_params.keys.any? { |k| k.start_with?("#{name}[") }
|
|
102
121
|
|
|
103
122
|
location = location_resolver.resolve(
|
|
104
123
|
name: name,
|
|
@@ -115,28 +134,6 @@ module GrapeOAS
|
|
|
115
134
|
params
|
|
116
135
|
end
|
|
117
136
|
|
|
118
|
-
# Determines whether nested params should be flattened to query params.
|
|
119
|
-
# Returns true for GET/HEAD/DELETE unless body is explicitly requested via:
|
|
120
|
-
# - route-level `request_body: true` option
|
|
121
|
-
# - any parameter with `documentation: { in: 'body' }` or `documentation: { param_type: 'body' }`
|
|
122
|
-
def should_flatten_nested_to_query?(http_method, all_params)
|
|
123
|
-
return false unless Constants::HttpMethods::BODYLESS_HTTP_METHODS.include?(http_method)
|
|
124
|
-
|
|
125
|
-
# If request_body is explicitly enabled at route level, use body schema
|
|
126
|
-
return false if route.options.dig(:documentation, :request_body) || route.options[:request_body]
|
|
127
|
-
|
|
128
|
-
# If any parameter is explicitly marked as body, use body schema
|
|
129
|
-
has_explicit_body_param = all_params.any? do |name, spec|
|
|
130
|
-
next false if name.include?("[") # Skip bracket params, check parent Hash params only
|
|
131
|
-
|
|
132
|
-
param_type = spec.dig(:documentation, :param_type)&.to_s&.downcase
|
|
133
|
-
in_location = spec.dig(:documentation, :in)&.to_s&.downcase
|
|
134
|
-
param_type == "body" || in_location == "body"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
!has_explicit_body_param
|
|
138
|
-
end
|
|
139
|
-
|
|
140
137
|
def build_parameter(name, location, required, schema, spec)
|
|
141
138
|
doc = spec[:documentation] || {}
|
|
142
139
|
style = doc.fetch(:style) { doc["style"] }
|
|
@@ -145,7 +142,7 @@ module GrapeOAS
|
|
|
145
142
|
ApiModel::Parameter.new(
|
|
146
143
|
location: location,
|
|
147
144
|
name: name,
|
|
148
|
-
required: required,
|
|
145
|
+
required: location == "path" || required,
|
|
149
146
|
schema: schema,
|
|
150
147
|
description: spec[:documentation]&.dig(:desc) || spec[:desc],
|
|
151
148
|
collection_format: extract_collection_format(spec),
|
|
@@ -5,6 +5,12 @@ module GrapeOAS
|
|
|
5
5
|
module RequestParamsSupport
|
|
6
6
|
# Resolves the location (path, query, body, header) for a parameter.
|
|
7
7
|
class ParamLocationResolver
|
|
8
|
+
# Locations `param_type:`/`in:` may explicitly name. An unrecognized
|
|
9
|
+
# value (a typo, or a grape-swagger location this library doesn't
|
|
10
|
+
# resolve to, like `formData`) is treated as unset rather than
|
|
11
|
+
# emitted verbatim, since nothing downstream validates `Parameter#location`.
|
|
12
|
+
VALID_EXPLICIT_LOCATIONS = %w[body query header path cookie].freeze
|
|
13
|
+
|
|
8
14
|
# Determines the location for a parameter.
|
|
9
15
|
#
|
|
10
16
|
# @param name [String] the parameter name
|
|
@@ -18,29 +24,14 @@ module GrapeOAS
|
|
|
18
24
|
extract_from_spec(spec, route)
|
|
19
25
|
end
|
|
20
26
|
|
|
21
|
-
# Checks if a parameter
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# @param spec [Hash] the parameter specification
|
|
25
|
-
# @return [Boolean] true if it's a body parameter
|
|
26
|
-
def self.body_param?(spec)
|
|
27
|
-
param_type = spec.dig(:documentation, :param_type)&.to_s&.downcase
|
|
28
|
-
in_location = spec.dig(:documentation, :in)&.to_s&.downcase
|
|
29
|
-
|
|
30
|
-
param_type == "body" || in_location == "body" || [Hash, "Hash"].include?(spec[:type])
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Checks if a parameter is explicitly marked as NOT a body param.
|
|
34
|
-
# Supports both `param_type` and `in` for grape-swagger compatibility.
|
|
27
|
+
# Checks if a parameter is a Hash type. Hash parents are represented
|
|
28
|
+
# by their children (nested params) or the request body, never
|
|
29
|
+
# themselves as a parameter.
|
|
35
30
|
#
|
|
36
31
|
# @param spec [Hash] the parameter specification
|
|
37
|
-
# @return [Boolean] true if
|
|
38
|
-
def self.
|
|
39
|
-
|
|
40
|
-
in_location = spec.dig(:documentation, :in)&.to_s&.downcase
|
|
41
|
-
location = param_type || in_location
|
|
42
|
-
|
|
43
|
-
location && %w[query header path].include?(location)
|
|
32
|
+
# @return [Boolean] true if the parameter type is Hash
|
|
33
|
+
def self.hash_param?(spec)
|
|
34
|
+
[Hash, "Hash"].include?(spec[:type])
|
|
44
35
|
end
|
|
45
36
|
|
|
46
37
|
# Checks if a parameter should be hidden from documentation.
|
|
@@ -56,6 +47,10 @@ module GrapeOAS
|
|
|
56
47
|
hidden
|
|
57
48
|
end
|
|
58
49
|
|
|
50
|
+
def self.route_body_opted_in?(route)
|
|
51
|
+
!!(route.options[:body_name] || route.options.dig(:documentation, :request_body) || route.options[:request_body])
|
|
52
|
+
end
|
|
53
|
+
|
|
59
54
|
class << self
|
|
60
55
|
private
|
|
61
56
|
|
|
@@ -70,24 +65,35 @@ module GrapeOAS
|
|
|
70
65
|
# Note: If both `param_type` and `in` are specified, `param_type` takes precedence.
|
|
71
66
|
# For example, `{ param_type: 'query', in: 'body' }` will be treated as query.
|
|
72
67
|
#
|
|
68
|
+
# `resolve` already returns "path" for an actual route capture before
|
|
69
|
+
# calling this method, so an explicit `in: "path"` / `param_type: "path"`
|
|
70
|
+
# reaching here is always a mismatch (the name can never appear in the
|
|
71
|
+
# URL template) and is ignored, same as an unrecognized location.
|
|
72
|
+
#
|
|
73
73
|
# @param spec [Hash] the parameter specification
|
|
74
74
|
# @param route [Object] the Grape route object
|
|
75
75
|
# @return [String] the parameter location
|
|
76
76
|
def extract_from_spec(spec, route)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return "body" if route.options[:body_name] && !param_type && !in_location
|
|
77
|
+
location = explicit_location(spec)
|
|
78
|
+
location = nil if location == "path"
|
|
79
|
+
return "body" if route_body_opted_in?(route) && location.nil?
|
|
81
80
|
|
|
82
81
|
# Support both param_type and in for grape-swagger compatibility
|
|
83
82
|
# param_type takes precedence over in when both are specified
|
|
84
|
-
|
|
85
|
-
return explicit_location if explicit_location
|
|
83
|
+
return location if location
|
|
86
84
|
|
|
87
85
|
# Default: body for write methods (POST/PUT/PATCH), query for read methods (GET/DELETE/HEAD)
|
|
88
86
|
http_method = route.request_method.to_s.downcase
|
|
89
87
|
Constants::HttpMethods::BODYLESS_HTTP_METHODS.include?(http_method) ? "query" : "body"
|
|
90
88
|
end
|
|
89
|
+
|
|
90
|
+
def explicit_location(spec)
|
|
91
|
+
doc = spec[:documentation] || {}
|
|
92
|
+
param_type = doc[:param_type] || doc["param_type"]
|
|
93
|
+
in_location = doc[:in] || doc["in"]
|
|
94
|
+
location = (param_type || in_location)&.to_s&.downcase
|
|
95
|
+
location if VALID_EXPLICIT_LOCATIONS.include?(location)
|
|
96
|
+
end
|
|
91
97
|
end
|
|
92
98
|
end
|
|
93
99
|
end
|
|
@@ -119,19 +119,17 @@ module GrapeOAS
|
|
|
119
119
|
type_names = extract_multi_types(type)
|
|
120
120
|
|
|
121
121
|
# OPTIMIZE: [Type, Nil] becomes nullable Type instead of oneOf
|
|
122
|
-
if
|
|
123
|
-
non_nil_type = type_names.find { |t| !nil_type_name?(t) }
|
|
122
|
+
if (nullable_type = Constants.nullable_type(type_names))
|
|
124
123
|
return ApiModel::Schema.new(
|
|
125
|
-
type: resolve_schema_type(
|
|
126
|
-
format: Constants.format_for_type(
|
|
124
|
+
type: resolve_schema_type(nullable_type),
|
|
125
|
+
format: Constants.format_for_type(nullable_type),
|
|
127
126
|
nullable: true,
|
|
128
127
|
)
|
|
129
128
|
end
|
|
130
129
|
|
|
131
130
|
# General case: build oneOf schema
|
|
132
131
|
# Filter out nil types - OpenAPI 3.0 uses nullable property instead
|
|
133
|
-
|
|
134
|
-
non_nil_types = type_names.reject { |t| nil_type_name?(t) }
|
|
132
|
+
nil_types, non_nil_types = type_names.partition { |type_name| Constants.nil_type?(type_name) }
|
|
135
133
|
|
|
136
134
|
schemas = non_nil_types.map do |type_name|
|
|
137
135
|
ApiModel::Schema.new(
|
|
@@ -139,26 +137,9 @@ module GrapeOAS
|
|
|
139
137
|
format: Constants.format_for_type(type_name),
|
|
140
138
|
)
|
|
141
139
|
end
|
|
142
|
-
ApiModel::Schema.new(one_of: schemas
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# Checks if type_names is a pair of [SomeType, NilType]
|
|
146
|
-
def nullable_type_pair?(type_names)
|
|
147
|
-
return false unless type_names.size == 2
|
|
148
|
-
|
|
149
|
-
type_names.one? { |t| nil_type_name?(t) }
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Checks if the type name represents a nil/null type
|
|
153
|
-
def nil_type_name?(type_name)
|
|
154
|
-
normalized = type_name.to_s
|
|
155
|
-
# Match common nil type patterns:
|
|
156
|
-
# - "NilClass" (Ruby's nil type)
|
|
157
|
-
# - "Nil" (shorthand)
|
|
158
|
-
# - "Foo::Nil", "Types::Nil" (namespaced nil types)
|
|
159
|
-
normalized == "NilClass" ||
|
|
160
|
-
normalized == "Nil" ||
|
|
161
|
-
normalized.end_with?("::Nil")
|
|
140
|
+
schema = ApiModel::Schema.new(one_of: schemas)
|
|
141
|
+
schema.nullable = true if nil_types.any?
|
|
142
|
+
schema
|
|
162
143
|
end
|
|
163
144
|
|
|
164
145
|
def build_primitive_schema(raw_type, doc)
|
|
@@ -13,7 +13,7 @@ module GrapeOAS
|
|
|
13
13
|
def self.apply(schema, spec, doc)
|
|
14
14
|
nullable = extract_nullable(doc)
|
|
15
15
|
|
|
16
|
-
schema.description ||= doc[:desc]
|
|
16
|
+
schema.description ||= doc[:desc] || spec[:desc]
|
|
17
17
|
# Preserve existing nullable: true (e.g., from [Type, Nil] optimization)
|
|
18
18
|
schema.nullable = (schema.nullable || nullable) if schema.respond_to?(:nullable=)
|
|
19
19
|
|
|
@@ -48,7 +48,11 @@ module GrapeOAS
|
|
|
48
48
|
# Parsers are tried in order of priority
|
|
49
49
|
def response_specs
|
|
50
50
|
parser = parsers.find { |p| p.applicable?(route) }
|
|
51
|
-
|
|
51
|
+
return [] unless parser
|
|
52
|
+
|
|
53
|
+
parser.parse(route).each do |spec|
|
|
54
|
+
spec[:code] = ResponseParsers::Base.normalize_status_code(spec[:code])
|
|
55
|
+
end
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
def parsers
|
|
@@ -29,6 +29,17 @@ module GrapeOAS
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
+
def normalize_status_code(status)
|
|
33
|
+
return status unless status.is_a?(Symbol)
|
|
34
|
+
|
|
35
|
+
status_name = status.to_s
|
|
36
|
+
return status_name if status_name == "default" || status_name.match?(/\A[1-5](?:\d{2}|XX)\z/)
|
|
37
|
+
|
|
38
|
+
Rack::Utils.status_code(status)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module_function :normalize_status_code
|
|
42
|
+
|
|
32
43
|
# Extract status code from hash, supporting multiple key names
|
|
33
44
|
def extract_status_code(hash, default_code)
|
|
34
45
|
hash[:code] || hash[:status] || hash[:http_status] || default_code
|
|
@@ -50,6 +61,12 @@ module GrapeOAS
|
|
|
50
61
|
|
|
51
62
|
hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
|
|
52
63
|
end
|
|
64
|
+
|
|
65
|
+
def default_response_code(route, success:)
|
|
66
|
+
return route.options[:default_status] if route.options[:default_status]
|
|
67
|
+
|
|
68
|
+
success && route.request_method.to_s.upcase == "POST" ? 201 : 200
|
|
69
|
+
end
|
|
53
70
|
end
|
|
54
71
|
end
|
|
55
72
|
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module GrapeOAS
|
|
4
4
|
module ApiModelBuilders
|
|
5
5
|
module ResponseParsers
|
|
6
|
-
# Parser that creates a default
|
|
7
|
-
#
|
|
6
|
+
# Parser that creates a default success response when no responses are defined.
|
|
7
|
+
# Status code defaults to 201 for POST routes and 200 for all other methods.
|
|
8
|
+
# This is the fallback parser used when no other parsers are applicable.
|
|
8
9
|
class DefaultResponseParser
|
|
9
10
|
include Base
|
|
10
11
|
|
|
@@ -14,12 +15,8 @@ module GrapeOAS
|
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def parse(route)
|
|
17
|
-
inferred = route.options[:default_status]
|
|
18
|
-
inferred ||= route.request_method.to_s.upcase == "POST" ? 201 : 200
|
|
19
|
-
default_code = inferred.to_s
|
|
20
|
-
|
|
21
18
|
[{
|
|
22
|
-
code:
|
|
19
|
+
code: default_response_code(route, success: true).to_s,
|
|
23
20
|
message: "Success",
|
|
24
21
|
entity: route.options[:entity],
|
|
25
22
|
headers: nil
|
|
@@ -17,10 +17,10 @@ module GrapeOAS
|
|
|
17
17
|
doc_resps = route.options.dig(:documentation, :responses)
|
|
18
18
|
return [] unless doc_resps.is_a?(Hash)
|
|
19
19
|
|
|
20
|
-
doc_resps.map do |code, doc|
|
|
20
|
+
specs = doc_resps.map do |code, doc|
|
|
21
21
|
doc = normalize_hash_keys(doc)
|
|
22
22
|
{
|
|
23
|
-
code: code,
|
|
23
|
+
code: normalize_status_code(code),
|
|
24
24
|
message: extract_description(doc),
|
|
25
25
|
headers: doc[:headers],
|
|
26
26
|
entity: extract_entity(doc, route.options[:entity]),
|
|
@@ -28,6 +28,9 @@ module GrapeOAS
|
|
|
28
28
|
examples: doc[:examples]
|
|
29
29
|
}
|
|
30
30
|
end
|
|
31
|
+
return specs if specs.any? { |spec| spec[:code].to_s == HttpCodesParser::DEFAULT_RESPONSE_CODE }
|
|
32
|
+
|
|
33
|
+
specs + HttpCodesParser.new.default_response_specs(route)
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
end
|