grape-oas 1.5.1 → 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 +18 -0
- data/UPGRADING.md +38 -0
- data/lib/grape_oas/api_model_builder.rb +5 -4
- data/lib/grape_oas/api_model_builders/response_parsers/base.rb +6 -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/http_codes_parser.rb +16 -13
- 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 +2 -20
- 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 +21 -2
- 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,24 @@ 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
|
+
|
|
8
26
|
## [1.5.1] - 2026-09-16
|
|
9
27
|
|
|
10
28
|
### Fixed
|
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
|
|
@@ -61,6 +61,12 @@ module GrapeOAS
|
|
|
61
61
|
|
|
62
62
|
hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
|
|
63
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
|
|
64
70
|
end
|
|
65
71
|
end
|
|
66
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
|
|
@@ -58,7 +58,7 @@ module GrapeOAS
|
|
|
58
58
|
return [] unless data.is_a?(Hash)
|
|
59
59
|
|
|
60
60
|
specs = %i[http_codes failure success].flat_map do |key|
|
|
61
|
-
parse_value(data[key], route)
|
|
61
|
+
parse_value(data[key], route, success: key == :success)
|
|
62
62
|
end
|
|
63
63
|
default_value = data[:default_response] || data[:default]
|
|
64
64
|
return specs unless default_value
|
|
@@ -112,10 +112,10 @@ module GrapeOAS
|
|
|
112
112
|
one_of.map { |entry| normalize_hash_keys(entry) }
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
def parse_value(value, route)
|
|
115
|
+
def parse_value(value, route, success:)
|
|
116
116
|
return [] unless value
|
|
117
117
|
|
|
118
|
-
entries_for(value).map { |entry| normalize_entry(entry, route) }
|
|
118
|
+
entries_for(value).map { |entry| normalize_entry(entry, route, success: success) }
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def entries_for(value)
|
|
@@ -149,6 +149,9 @@ module GrapeOAS
|
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
def append_entity_spec(specs, entity_value, route)
|
|
152
|
+
explicit_code = (entity_value[:code] if entity_value.is_a?(Hash)) || route.options[:default_status]
|
|
153
|
+
return specs if !explicit_code && specs.any? { |spec| spec[:code] == "2XX" || (200..299).cover?(spec[:code].to_i) }
|
|
154
|
+
|
|
152
155
|
entity_spec = build_entity_spec(entity_value, route)
|
|
153
156
|
return specs if specs.any? { |spec| spec[:code].to_i == entity_spec[:code].to_i }
|
|
154
157
|
|
|
@@ -159,7 +162,7 @@ module GrapeOAS
|
|
|
159
162
|
if entity_value.is_a?(Hash)
|
|
160
163
|
# Hash format: { code: 201, model: Entity, message: "Created" }
|
|
161
164
|
{
|
|
162
|
-
code: entity_value[:code] ||
|
|
165
|
+
code: entity_value[:code] || default_response_code(route, success: true),
|
|
163
166
|
message: entity_value[:message],
|
|
164
167
|
entity: extract_entity(entity_value, nil),
|
|
165
168
|
headers: entity_value[:headers],
|
|
@@ -172,7 +175,7 @@ module GrapeOAS
|
|
|
172
175
|
else
|
|
173
176
|
# Plain entity class
|
|
174
177
|
{
|
|
175
|
-
code:
|
|
178
|
+
code: default_response_code(route, success: true),
|
|
176
179
|
message: nil,
|
|
177
180
|
entity: entity_value,
|
|
178
181
|
headers: nil,
|
|
@@ -184,15 +187,15 @@ module GrapeOAS
|
|
|
184
187
|
end
|
|
185
188
|
end
|
|
186
189
|
|
|
187
|
-
def normalize_entry(entry, route)
|
|
190
|
+
def normalize_entry(entry, route, success:)
|
|
188
191
|
spec = case entry
|
|
189
192
|
when Hash
|
|
190
|
-
normalize_hash_entry(entry, route)
|
|
193
|
+
normalize_hash_entry(entry, route, success: success)
|
|
191
194
|
when Array
|
|
192
195
|
normalize_array_entry(entry, route)
|
|
193
196
|
when Class, Module
|
|
194
197
|
# Plain entity class (e.g., success TestEntity)
|
|
195
|
-
normalize_entity_entry(entry, route)
|
|
198
|
+
normalize_entity_entry(entry, route, success: success)
|
|
196
199
|
else
|
|
197
200
|
normalize_plain_entry(entry, route)
|
|
198
201
|
end
|
|
@@ -200,10 +203,10 @@ module GrapeOAS
|
|
|
200
203
|
spec
|
|
201
204
|
end
|
|
202
205
|
|
|
203
|
-
def normalize_hash_entry(entry, route)
|
|
204
|
-
default_code = (route
|
|
206
|
+
def normalize_hash_entry(entry, route, success:)
|
|
207
|
+
default_code = default_response_code(route, success: success)
|
|
205
208
|
{
|
|
206
|
-
code: extract_status_code(entry, default_code),
|
|
209
|
+
code: extract_status_code(entry, default_code.to_s),
|
|
207
210
|
message: extract_description(entry),
|
|
208
211
|
entity: extract_entity(entry, route.options[:entity]),
|
|
209
212
|
headers: entry[:headers],
|
|
@@ -228,10 +231,10 @@ module GrapeOAS
|
|
|
228
231
|
}
|
|
229
232
|
end
|
|
230
233
|
|
|
231
|
-
def normalize_entity_entry(entity_class, route)
|
|
234
|
+
def normalize_entity_entry(entity_class, route, success:)
|
|
232
235
|
# Plain entity class (e.g., success TestEntity)
|
|
233
236
|
{
|
|
234
|
-
code: route
|
|
237
|
+
code: default_response_code(route, success: success),
|
|
235
238
|
message: nil,
|
|
236
239
|
entity: entity_class,
|
|
237
240
|
headers: nil,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module Concerns
|
|
6
|
+
module EnumNormalizer
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def normalize_enum(enum_values, type, preserve_nil: false)
|
|
10
|
+
return nil unless enum_values.is_a?(Array)
|
|
11
|
+
|
|
12
|
+
base_type = base_type_for(type)
|
|
13
|
+
normalized = enum_values.each_with_object([]) do |value, values|
|
|
14
|
+
next if value.nil?
|
|
15
|
+
|
|
16
|
+
normalized_value = case base_type
|
|
17
|
+
when Constants::SchemaTypes::INTEGER then value.to_i if value.respond_to?(:to_i)
|
|
18
|
+
when Constants::SchemaTypes::NUMBER then value.to_f if value.respond_to?(:to_f)
|
|
19
|
+
else value
|
|
20
|
+
end
|
|
21
|
+
values << normalized_value unless normalized_value.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
normalized.uniq!
|
|
25
|
+
normalized << nil if preserve_nil && enum_values.include?(nil)
|
|
26
|
+
normalized unless normalized.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def base_type_for(type)
|
|
30
|
+
type.is_a?(Array) ? (type - [Constants::SchemaTypes::NULL]).first : type
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -7,13 +7,30 @@ module GrapeOAS
|
|
|
7
7
|
# Handles building schema indexes from operations and collecting nested schema references.
|
|
8
8
|
module SchemaIndexer
|
|
9
9
|
def find_schema_by_canonical_name(canonical_name)
|
|
10
|
-
@ref_schemas[canonical_name] || schema_index[canonical_name]
|
|
10
|
+
schema = @ref_schemas[canonical_name] || schema_index[canonical_name]
|
|
11
|
+
ensure_unique_schema_ref_name!(canonical_name) if schema
|
|
12
|
+
schema
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def schema_index
|
|
14
16
|
@schema_index ||= build_schema_index
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
def ensure_unique_schema_ref_name!(canonical_name)
|
|
20
|
+
ref_name = GrapeOAS.schema_ref_name.call(canonical_name)
|
|
21
|
+
canonical_names_by_ref = @canonical_names_by_ref ||= {}
|
|
22
|
+
existing_name = canonical_names_by_ref[ref_name]
|
|
23
|
+
|
|
24
|
+
if existing_name && existing_name != canonical_name
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"Schema reference name #{ref_name.inspect} is generated for both " \
|
|
27
|
+
"#{existing_name.inspect} and #{canonical_name.inspect}; " \
|
|
28
|
+
"configure GrapeOAS.schema_ref_name to generate unique names"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
canonical_names_by_ref[ref_name] = canonical_name
|
|
32
|
+
end
|
|
33
|
+
|
|
17
34
|
def build_schema_index
|
|
18
35
|
index = {}
|
|
19
36
|
# Index schemas from operations
|
|
@@ -4,6 +4,8 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS2
|
|
6
6
|
class Parameter
|
|
7
|
+
include Concerns::EnumNormalizer
|
|
8
|
+
|
|
7
9
|
PRIMITIVE_MAPPINGS = {
|
|
8
10
|
Constants::SchemaTypes::INTEGER => { type: Constants::SchemaTypes::INTEGER },
|
|
9
11
|
"long" => { type: Constants::SchemaTypes::INTEGER, format: "int64" },
|
|
@@ -134,26 +136,6 @@ module GrapeOAS
|
|
|
134
136
|
result["default"] = schema.default if schema.respond_to?(:default) && !schema.default.nil?
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
def normalize_enum(enum_vals, type)
|
|
138
|
-
return nil unless enum_vals.is_a?(Array)
|
|
139
|
-
|
|
140
|
-
result = enum_vals.each_with_object([]) do |v, acc|
|
|
141
|
-
next if v.nil?
|
|
142
|
-
|
|
143
|
-
coerced_v = case type
|
|
144
|
-
when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
|
|
145
|
-
when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
|
|
146
|
-
else v
|
|
147
|
-
end
|
|
148
|
-
acc << coerced_v unless coerced_v.nil?
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
result.uniq!
|
|
152
|
-
return nil if result.empty?
|
|
153
|
-
|
|
154
|
-
result
|
|
155
|
-
end
|
|
156
|
-
|
|
157
139
|
def apply_collection_format(result, param, schema)
|
|
158
140
|
return unless schema.type == Constants::SchemaTypes::ARRAY
|
|
159
141
|
|
|
@@ -4,6 +4,8 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS2
|
|
6
6
|
class Schema
|
|
7
|
+
include Concerns::EnumNormalizer
|
|
8
|
+
|
|
7
9
|
def initialize(schema, ref_tracker = nil, nullable_strategy: nil, composition_extensions: false)
|
|
8
10
|
@schema = schema
|
|
9
11
|
@ref_tracker = ref_tracker
|
|
@@ -38,7 +40,7 @@ module GrapeOAS
|
|
|
38
40
|
"description" => @schema.description&.to_s,
|
|
39
41
|
"properties" => build_properties(@schema.properties)
|
|
40
42
|
}
|
|
41
|
-
schema_hash["enum"] = normalize_enum(@schema.enum, @schema.type,
|
|
43
|
+
schema_hash["enum"] = normalize_enum(@schema.enum, @schema.type, preserve_nil: enum_allows_null?(@schema)) if @schema.enum
|
|
42
44
|
if @schema.items
|
|
43
45
|
schema_hash["items"] = build_schema_or_ref(@schema.items, include_metadata: false)
|
|
44
46
|
if !schema_hash["description"] && @schema.items.respond_to?(:description) && @schema.items.description
|
|
@@ -99,6 +101,10 @@ module GrapeOAS
|
|
|
99
101
|
schema_nullable?(@schema)
|
|
100
102
|
end
|
|
101
103
|
|
|
104
|
+
def enum_allows_null?(schema)
|
|
105
|
+
@nullable_strategy == Constants::NullableStrategy::EXTENSION && schema_nullable?(schema)
|
|
106
|
+
end
|
|
107
|
+
|
|
102
108
|
# OAS2 keeps a first-alternative fallback for tools that ignore extensions.
|
|
103
109
|
def build_first_of_schema(composition_type)
|
|
104
110
|
schemas = composition_type == :any_of ? @schema.any_of : @schema.one_of
|
|
@@ -130,7 +136,7 @@ module GrapeOAS
|
|
|
130
136
|
result["format"] = @schema.format if @schema.format
|
|
131
137
|
result["description"] = @schema.description.to_s if @schema.description
|
|
132
138
|
result["default"] = @schema.default unless @schema.default.nil?
|
|
133
|
-
result["enum"] = normalize_enum(@schema.enum, @schema.type,
|
|
139
|
+
result["enum"] = normalize_enum(@schema.enum, @schema.type, preserve_nil: enum_allows_null?(@schema)) if @schema.enum
|
|
134
140
|
result.delete("enum") if result.key?("enum") && result["enum"].nil?
|
|
135
141
|
apply_constraints(result)
|
|
136
142
|
apply_extensions(result)
|
|
@@ -156,7 +162,7 @@ module GrapeOAS
|
|
|
156
162
|
result["x-nullable"] = true if @nullable_strategy == Constants::NullableStrategy::EXTENSION && schema_nullable?(schema)
|
|
157
163
|
result["description"] = schema.description.to_s if schema.description
|
|
158
164
|
result["default"] = schema.default unless schema.default.nil?
|
|
159
|
-
result["enum"] = normalize_enum(schema.enum, schema.type,
|
|
165
|
+
result["enum"] = normalize_enum(schema.enum, schema.type, preserve_nil: enum_allows_null?(schema)) if schema.enum
|
|
160
166
|
result.delete("enum") if result.key?("enum") && result["enum"].nil?
|
|
161
167
|
apply_constraints(result, schema)
|
|
162
168
|
result.merge!(schema.extensions) if schema.extensions
|
|
@@ -175,29 +181,6 @@ module GrapeOAS
|
|
|
175
181
|
built
|
|
176
182
|
end
|
|
177
183
|
end
|
|
178
|
-
|
|
179
|
-
def normalize_enum(enum_vals, type, nullable: false)
|
|
180
|
-
return nil unless enum_vals.is_a?(Array)
|
|
181
|
-
|
|
182
|
-
has_nil = nullable && enum_vals.include?(nil)
|
|
183
|
-
|
|
184
|
-
result = enum_vals.each_with_object([]) do |v, acc|
|
|
185
|
-
next if v.nil?
|
|
186
|
-
|
|
187
|
-
coerced_v = case type
|
|
188
|
-
when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
|
|
189
|
-
when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
|
|
190
|
-
else v
|
|
191
|
-
end
|
|
192
|
-
acc << coerced_v unless coerced_v.nil?
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
result.uniq!
|
|
196
|
-
result.push(nil) if has_nil
|
|
197
|
-
return nil if result.empty?
|
|
198
|
-
|
|
199
|
-
result
|
|
200
|
-
end
|
|
201
184
|
end
|
|
202
185
|
end
|
|
203
186
|
end
|
|
@@ -4,6 +4,8 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS3
|
|
6
6
|
class Schema
|
|
7
|
+
include Concerns::EnumNormalizer
|
|
8
|
+
|
|
7
9
|
def initialize(schema, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD)
|
|
8
10
|
@schema = schema
|
|
9
11
|
@ref_tracker = ref_tracker
|
|
@@ -41,7 +43,11 @@ module GrapeOAS
|
|
|
41
43
|
|
|
42
44
|
end
|
|
43
45
|
schema_hash["required"] = @schema.required if @schema.required && !@schema.required.empty?
|
|
44
|
-
|
|
46
|
+
if @schema.enum
|
|
47
|
+
schema_hash["enum"] = normalize_enum(
|
|
48
|
+
@schema.enum, schema_hash["type"], preserve_nil: enum_allows_null?(@schema, schema_hash["type"]),
|
|
49
|
+
)
|
|
50
|
+
end
|
|
45
51
|
schema_hash["default"] = @schema.default unless @schema.default.nil?
|
|
46
52
|
schema_hash
|
|
47
53
|
end
|
|
@@ -88,7 +94,11 @@ module GrapeOAS
|
|
|
88
94
|
if schema_nullable?(schema) && @nullable_strategy == Constants::NullableStrategy::TYPE_ARRAY
|
|
89
95
|
enum_type = Array(enum_type) | ["null"]
|
|
90
96
|
end
|
|
91
|
-
|
|
97
|
+
if schema.enum
|
|
98
|
+
result["enum"] = normalize_enum(
|
|
99
|
+
schema.enum, enum_type, preserve_nil: enum_allows_null?(schema, enum_type, null_union: true),
|
|
100
|
+
)
|
|
101
|
+
end
|
|
92
102
|
sanitize_enum_against_type(result, type: schema.type)
|
|
93
103
|
apply_all_constraints(result, schema)
|
|
94
104
|
result.merge!(schema.extensions) if schema.extensions
|
|
@@ -110,12 +120,6 @@ module GrapeOAS
|
|
|
110
120
|
|
|
111
121
|
private
|
|
112
122
|
|
|
113
|
-
# Returns the primary non-null type from a type value.
|
|
114
|
-
# Assumes at most one non-null type in the array (e.g. ["integer", "null"]).
|
|
115
|
-
def base_type_for(type)
|
|
116
|
-
type.is_a?(Array) ? (type - ["null"]).first : type
|
|
117
|
-
end
|
|
118
|
-
|
|
119
123
|
# Rewrites `type: file` (or `type: ["file", "null"]`) to the
|
|
120
124
|
# version-appropriate representation. Type detection lives here;
|
|
121
125
|
# version-specific attributes are set by `apply_file_schema_attributes!`.
|
|
@@ -170,7 +174,11 @@ module GrapeOAS
|
|
|
170
174
|
result["format"] = @schema.format if @schema.format
|
|
171
175
|
result["description"] = @schema.description.to_s if @schema.description
|
|
172
176
|
result["default"] = @schema.default unless @schema.default.nil?
|
|
173
|
-
|
|
177
|
+
if @schema.enum
|
|
178
|
+
result["enum"] = normalize_enum(
|
|
179
|
+
@schema.enum, result["type"], preserve_nil: enum_allows_null?(@schema, result["type"], null_union: true),
|
|
180
|
+
)
|
|
181
|
+
end
|
|
174
182
|
sanitize_enum_against_type(result)
|
|
175
183
|
apply_all_constraints(result)
|
|
176
184
|
apply_composition_extensions(result)
|
|
@@ -305,38 +313,17 @@ module GrapeOAS
|
|
|
305
313
|
{ "type" => Constants::SchemaTypes::OBJECT, "nullable" => true, "enum" => [nil] }
|
|
306
314
|
end
|
|
307
315
|
|
|
308
|
-
def
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
nullable = (nullable || (type.is_a?(Array) && type.include?(Constants::SchemaTypes::NULL))) &&
|
|
312
|
-
enum_null_supported?(type)
|
|
313
|
-
resolved_type = base_type_for(type)
|
|
314
|
-
|
|
315
|
-
has_nil = nullable && enum_vals.include?(nil)
|
|
316
|
+
def enum_allows_null?(schema, type, null_union: false)
|
|
317
|
+
has_null_type = type.is_a?(Array) && type.include?(Constants::SchemaTypes::NULL)
|
|
316
318
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
end
|
|
325
|
-
acc << coerced_v unless coerced_v.nil?
|
|
319
|
+
case @nullable_strategy
|
|
320
|
+
when Constants::NullableStrategy::KEYWORD
|
|
321
|
+
has_null_type || (schema_nullable?(schema) && (!type.nil? || null_union))
|
|
322
|
+
when Constants::NullableStrategy::TYPE_ARRAY
|
|
323
|
+
has_null_type || (schema_nullable?(schema) && null_union)
|
|
324
|
+
else
|
|
325
|
+
false
|
|
326
326
|
end
|
|
327
|
-
|
|
328
|
-
result.uniq!
|
|
329
|
-
result.push(nil) if has_nil
|
|
330
|
-
return nil if result.empty?
|
|
331
|
-
|
|
332
|
-
result
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def enum_null_supported?(type)
|
|
336
|
-
return true if @nullable_strategy == Constants::NullableStrategy::KEYWORD
|
|
337
|
-
|
|
338
|
-
@nullable_strategy == Constants::NullableStrategy::TYPE_ARRAY &&
|
|
339
|
-
type.is_a?(Array) && type.include?(Constants::SchemaTypes::NULL)
|
|
340
327
|
end
|
|
341
328
|
|
|
342
329
|
def apply_numeric_constraints(hash, schema = @schema)
|
|
@@ -118,7 +118,8 @@ module GrapeOAS
|
|
|
118
118
|
|
|
119
119
|
# Determines whether a property should be marked required.
|
|
120
120
|
# Explicit doc[:required] takes precedence; conditional exposures
|
|
121
|
-
# default to false; unconditional exposures
|
|
121
|
+
# default to false; unconditional exposures fall back to
|
|
122
|
+
# GrapeOAS.entity_exposure_required_default (default `true`).
|
|
122
123
|
#
|
|
123
124
|
# @param doc [Hash] normalized documentation hash
|
|
124
125
|
# @param exposure the entity exposure
|
|
@@ -127,7 +128,7 @@ module GrapeOAS
|
|
|
127
128
|
return doc[:required] unless doc[:required].nil?
|
|
128
129
|
return false if conditional?(exposure)
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
GrapeOAS.entity_exposure_required_default
|
|
131
132
|
end
|
|
132
133
|
|
|
133
134
|
private
|
|
@@ -167,10 +168,28 @@ module GrapeOAS
|
|
|
167
168
|
return prop_schema unless is_array
|
|
168
169
|
|
|
169
170
|
array_schema = ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: prop_schema)
|
|
171
|
+
if PropertyExtractor.extract_nullable(doc)
|
|
172
|
+
array_schema.nullable = true
|
|
173
|
+
if bare_single_ref_allof?(prop_schema)
|
|
174
|
+
array_schema.items = prop_schema.all_of.first
|
|
175
|
+
else
|
|
176
|
+
prop_schema.nullable = false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
170
179
|
array_schema.examples = doc[:example] if array_valued_example?(doc)
|
|
171
180
|
array_schema
|
|
172
181
|
end
|
|
173
182
|
|
|
183
|
+
def bare_single_ref_allof?(schema)
|
|
184
|
+
return false unless schema.all_of&.one? && schema.all_of.first.canonical_name
|
|
185
|
+
|
|
186
|
+
empty_schema = ApiModel::Schema.new
|
|
187
|
+
# Comparing defaults keeps future schema attributes from being discarded.
|
|
188
|
+
(ApiModel::Schema::VALID_ATTRIBUTES - %i[nullable all_of]).all? do |attribute|
|
|
189
|
+
schema.public_send(attribute) == empty_schema.public_send(attribute)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
174
193
|
# Detects block-based nesting exposures (NestingExposure) that should become
|
|
175
194
|
# inline object schemas. Only triggers when no entity class is via `using:`.
|
|
176
195
|
def nesting_exposure?(exposure)
|
data/lib/grape_oas/version.rb
CHANGED
data/lib/grape_oas.rb
CHANGED
|
@@ -91,6 +91,25 @@ module GrapeOAS
|
|
|
91
91
|
|
|
92
92
|
module_function :logger, :logger=
|
|
93
93
|
|
|
94
|
+
# @return [Boolean] whether entity exposures without required metadata
|
|
95
|
+
# default to required
|
|
96
|
+
def entity_exposure_required_default
|
|
97
|
+
return true if @entity_exposure_required_default.nil?
|
|
98
|
+
|
|
99
|
+
@entity_exposure_required_default
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param value [true, false, nil] `nil` resets to the default (`true`)
|
|
103
|
+
def entity_exposure_required_default=(value)
|
|
104
|
+
unless value.nil? || value == true || value == false
|
|
105
|
+
raise ArgumentError, "entity_exposure_required_default must be true, false, or nil (got #{value.class})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@entity_exposure_required_default = value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
module_function :entity_exposure_required_default, :entity_exposure_required_default=
|
|
112
|
+
|
|
94
113
|
# Returns the global introspector registry.
|
|
95
114
|
#
|
|
96
115
|
# The registry manages introspectors that build schemas from various sources
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: grape-oas
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Subbota
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: grape
|
|
@@ -86,6 +86,7 @@ files:
|
|
|
86
86
|
- lib/grape_oas/exporter.rb
|
|
87
87
|
- lib/grape_oas/exporter/base/operation.rb
|
|
88
88
|
- lib/grape_oas/exporter/base/paths.rb
|
|
89
|
+
- lib/grape_oas/exporter/concerns/enum_normalizer.rb
|
|
89
90
|
- lib/grape_oas/exporter/concerns/schema_indexer.rb
|
|
90
91
|
- lib/grape_oas/exporter/concerns/tag_builder.rb
|
|
91
92
|
- lib/grape_oas/exporter/oas2/operation.rb
|