grape-oas 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +27 -10
- data/UPGRADING.md +104 -0
- data/grape-oas.gemspec +2 -1
- data/lib/grape_oas/api_model/api.rb +2 -1
- data/lib/grape_oas/api_model/schema.rb +1 -1
- data/lib/grape_oas/api_model_builder.rb +3 -2
- data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +9 -1
- data/lib/grape_oas/api_model_builders/concerns/oas_utilities.rb +13 -0
- data/lib/grape_oas/api_model_builders/concerns/route_validations.rb +26 -0
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +4 -2
- data/lib/grape_oas/api_model_builders/operation.rb +3 -4
- data/lib/grape_oas/api_model_builders/path.rb +33 -6
- data/lib/grape_oas/api_model_builders/request.rb +14 -16
- data/lib/grape_oas/api_model_builders/request_params.rb +83 -3
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +1 -1
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +1 -1
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +3 -9
- data/lib/grape_oas/api_model_builders/response.rb +28 -4
- data/lib/grape_oas/constants.rb +16 -4
- data/lib/grape_oas/documentation_extension.rb +7 -9
- data/lib/grape_oas/exporter/concerns/schema_indexer.rb +14 -3
- data/lib/grape_oas/exporter/oas2/operation.rb +4 -2
- data/lib/grape_oas/exporter/oas2/parameter.rb +73 -13
- data/lib/grape_oas/exporter/oas2/paths.rb +1 -0
- data/lib/grape_oas/exporter/oas2/response.rb +6 -4
- data/lib/grape_oas/exporter/oas2/schema.rb +56 -28
- data/lib/grape_oas/exporter/oas2_schema.rb +7 -5
- data/lib/grape_oas/exporter/oas3/operation.rb +5 -3
- data/lib/grape_oas/exporter/oas3/parameter.rb +4 -2
- data/lib/grape_oas/exporter/oas3/paths.rb +1 -0
- data/lib/grape_oas/exporter/oas3/request_body.rb +5 -8
- data/lib/grape_oas/exporter/oas3/response.rb +6 -9
- data/lib/grape_oas/exporter/oas3/schema.rb +232 -84
- data/lib/grape_oas/exporter/oas31/schema.rb +12 -2
- data/lib/grape_oas/exporter/oas31_schema.rb +1 -1
- data/lib/grape_oas/exporter/oas3_schema.rb +3 -2
- data/lib/grape_oas/introspectors/entity_introspector.rb +2 -1
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +55 -20
- data/lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb +2 -8
- data/lib/grape_oas/introspectors/entity_introspector_support/type_schema_resolver.rb +4 -20
- data/lib/grape_oas/range_utils.rb +25 -2
- data/lib/grape_oas/type_resolvers/array_resolver.rb +53 -19
- data/lib/grape_oas/type_resolvers/base.rb +2 -2
- data/lib/grape_oas/type_resolvers/default_resolver.rb +23 -0
- data/lib/grape_oas/type_resolvers/dry_type_resolver.rb +1 -1
- data/lib/grape_oas/type_resolvers/primitive_resolver.rb +29 -46
- data/lib/grape_oas/type_resolvers/registry.rb +31 -15
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +37 -7
- metadata +5 -4
- data/CONTRIBUTING.md +0 -87
- data/RELEASING.md +0 -109
|
@@ -21,6 +21,7 @@ module GrapeOAS
|
|
|
21
21
|
sanitize_enum_against_type(schema_hash)
|
|
22
22
|
apply_extensions_and_extra_properties(schema_hash)
|
|
23
23
|
apply_all_constraints(schema_hash)
|
|
24
|
+
normalize_file_type!(schema_hash)
|
|
24
25
|
schema_hash.compact
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -37,29 +38,19 @@ module GrapeOAS
|
|
|
37
38
|
if !schema_hash["description"] && @schema.items.respond_to?(:description) && @schema.items.description
|
|
38
39
|
schema_hash["description"] = @schema.items.description.to_s
|
|
39
40
|
end
|
|
40
|
-
|
|
41
|
-
@schema.items.respond_to?(:nullable) && @schema.items.nullable
|
|
42
|
-
case @nullable_strategy
|
|
43
|
-
when Constants::NullableStrategy::KEYWORD
|
|
44
|
-
schema_hash["nullable"] = true
|
|
45
|
-
when Constants::NullableStrategy::EXTENSION
|
|
46
|
-
schema_hash["x-nullable"] = true
|
|
47
|
-
when Constants::NullableStrategy::TYPE_ARRAY
|
|
48
|
-
schema_hash["type"] = (Array(schema_hash["type"]) | ["null"])
|
|
49
|
-
end
|
|
50
|
-
end
|
|
41
|
+
|
|
51
42
|
end
|
|
52
43
|
schema_hash["required"] = @schema.required if @schema.required && !@schema.required.empty?
|
|
53
|
-
schema_hash["enum"] = normalize_enum(@schema.enum, schema_hash["type"]) if @schema.enum
|
|
44
|
+
schema_hash["enum"] = normalize_enum(@schema.enum, schema_hash["type"], nullable: nullable?) if @schema.enum
|
|
54
45
|
schema_hash["default"] = @schema.default unless @schema.default.nil?
|
|
55
46
|
schema_hash
|
|
56
47
|
end
|
|
57
48
|
|
|
58
49
|
def apply_examples(schema_hash)
|
|
59
|
-
return
|
|
50
|
+
return if @schema.examples.nil?
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
schema_hash["example"] = examples
|
|
52
|
+
type = schema_hash["type"]
|
|
53
|
+
schema_hash["example"] = coerce_example(@schema.examples, type)
|
|
63
54
|
end
|
|
64
55
|
|
|
65
56
|
def apply_extensions_and_extra_properties(schema_hash)
|
|
@@ -79,8 +70,72 @@ module GrapeOAS
|
|
|
79
70
|
apply_array_constraints(schema_hash, schema)
|
|
80
71
|
end
|
|
81
72
|
|
|
73
|
+
def build_schema_or_ref(schema, include_metadata: true)
|
|
74
|
+
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
75
|
+
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
76
|
+
ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
|
|
77
|
+
ref_hash = { "$ref" => "#/components/schemas/#{ref_name}" }
|
|
78
|
+
unless include_metadata
|
|
79
|
+
return ref_hash unless schema_nullable?(schema)
|
|
80
|
+
|
|
81
|
+
return { "allOf" => [ref_hash] }.tap { |hash| apply_nullable_to_ref(hash, schema) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
result = {}
|
|
85
|
+
result["description"] = schema.description.to_s if schema.description
|
|
86
|
+
result["default"] = schema.default unless schema.default.nil?
|
|
87
|
+
enum_type = schema.type
|
|
88
|
+
if schema_nullable?(schema) && @nullable_strategy == Constants::NullableStrategy::TYPE_ARRAY
|
|
89
|
+
enum_type = Array(enum_type) | ["null"]
|
|
90
|
+
end
|
|
91
|
+
result["enum"] = normalize_enum(schema.enum, enum_type, nullable: schema_nullable?(schema)) if schema.enum
|
|
92
|
+
sanitize_enum_against_type(result, type: schema.type)
|
|
93
|
+
apply_all_constraints(result, schema)
|
|
94
|
+
result.merge!(schema.extensions) if schema.extensions
|
|
95
|
+
if result.empty? && !schema_nullable?(schema)
|
|
96
|
+
ref_hash
|
|
97
|
+
else
|
|
98
|
+
result["allOf"] = [ref_hash]
|
|
99
|
+
apply_nullable_to_ref(result, schema)
|
|
100
|
+
result
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
# self.class preserves the OAS version subclass (e.g. OAS31::Schema)
|
|
104
|
+
# so nested schemas get version-correct normalization.
|
|
105
|
+
built = self.class.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
106
|
+
strip_items_metadata(built) unless include_metadata
|
|
107
|
+
built
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
82
111
|
private
|
|
83
112
|
|
|
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
|
+
# Rewrites `type: file` (or `type: ["file", "null"]`) to the
|
|
120
|
+
# version-appropriate representation. Type detection lives here;
|
|
121
|
+
# version-specific attributes are set by `apply_file_schema_attributes!`.
|
|
122
|
+
def normalize_file_type!(hash)
|
|
123
|
+
type = hash["type"]
|
|
124
|
+
if type == Constants::SchemaTypes::FILE
|
|
125
|
+
hash["type"] = Constants::SchemaTypes::STRING
|
|
126
|
+
apply_file_schema_attributes!(hash)
|
|
127
|
+
elsif type.is_a?(Array) && type.include?(Constants::SchemaTypes::FILE)
|
|
128
|
+
hash["type"] = type.map { |t| t == Constants::SchemaTypes::FILE ? Constants::SchemaTypes::STRING : t }
|
|
129
|
+
apply_file_schema_attributes!(hash)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# OAS 3.0: files are `type: string, format: binary`.
|
|
134
|
+
# OAS 3.1 overrides this with content-* keywords.
|
|
135
|
+
def apply_file_schema_attributes!(hash)
|
|
136
|
+
hash["format"] = "binary"
|
|
137
|
+
end
|
|
138
|
+
|
|
84
139
|
# Build allOf schema for inheritance
|
|
85
140
|
def build_all_of_schema
|
|
86
141
|
items = @schema.all_of.map { |item| build_schema_or_ref(item) }
|
|
@@ -115,10 +170,22 @@ module GrapeOAS
|
|
|
115
170
|
result["format"] = @schema.format if @schema.format
|
|
116
171
|
result["description"] = @schema.description.to_s if @schema.description
|
|
117
172
|
result["default"] = @schema.default unless @schema.default.nil?
|
|
118
|
-
result["enum"] = normalize_enum(@schema.enum, result["type"]) if @schema.enum
|
|
173
|
+
result["enum"] = normalize_enum(@schema.enum, result["type"], nullable: nullable?) if @schema.enum
|
|
119
174
|
sanitize_enum_against_type(result)
|
|
120
175
|
apply_all_constraints(result)
|
|
121
|
-
result
|
|
176
|
+
apply_composition_extensions(result)
|
|
177
|
+
normalize_file_type!(result)
|
|
178
|
+
result
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def apply_composition_extensions(result)
|
|
182
|
+
return unless @schema.extensions
|
|
183
|
+
|
|
184
|
+
extensions = @schema.extensions.reject do |key, _value|
|
|
185
|
+
(key == "x-anyOf" && result.key?("anyOf")) ||
|
|
186
|
+
(key == "x-oneOf" && result.key?("oneOf"))
|
|
187
|
+
end
|
|
188
|
+
result.merge!(extensions)
|
|
122
189
|
end
|
|
123
190
|
|
|
124
191
|
# Build OAS3 discriminator object
|
|
@@ -137,8 +204,12 @@ module GrapeOAS
|
|
|
137
204
|
end
|
|
138
205
|
end
|
|
139
206
|
|
|
207
|
+
def schema_nullable?(schema)
|
|
208
|
+
schema.respond_to?(:nullable) && !!schema.nullable
|
|
209
|
+
end
|
|
210
|
+
|
|
140
211
|
def nullable?
|
|
141
|
-
|
|
212
|
+
schema_nullable?(@schema)
|
|
142
213
|
end
|
|
143
214
|
|
|
144
215
|
def nullable_type
|
|
@@ -151,14 +222,40 @@ module GrapeOAS
|
|
|
151
222
|
def apply_nullable(schema_hash)
|
|
152
223
|
return unless nullable?
|
|
153
224
|
|
|
225
|
+
composition_key = %w[allOf oneOf anyOf].find { |k| schema_hash.key?(k) }
|
|
154
226
|
case @nullable_strategy
|
|
155
227
|
when Constants::NullableStrategy::KEYWORD
|
|
156
|
-
|
|
228
|
+
if composition_key
|
|
229
|
+
apply_keyword_null_union(schema_hash, composition_key)
|
|
230
|
+
elsif schema_hash["type"]
|
|
231
|
+
schema_hash["nullable"] = true
|
|
232
|
+
end
|
|
157
233
|
when Constants::NullableStrategy::EXTENSION
|
|
158
234
|
schema_hash["x-nullable"] = true
|
|
235
|
+
when Constants::NullableStrategy::TYPE_ARRAY
|
|
236
|
+
if composition_key
|
|
237
|
+
apply_null_union(schema_hash, composition_key, { "type" => "null" })
|
|
238
|
+
else
|
|
239
|
+
schema_hash["type"] = (Array(schema_hash["type"]) | ["null"])
|
|
240
|
+
end
|
|
159
241
|
end
|
|
160
242
|
end
|
|
161
243
|
|
|
244
|
+
# Nullable must accompany a type, and every composition constraint must
|
|
245
|
+
# also admit null. The enum keeps the extra branch from allowing objects.
|
|
246
|
+
def apply_keyword_null_union(hash, key)
|
|
247
|
+
hash["nullable"] = true if hash["type"]
|
|
248
|
+
apply_null_union(hash, key, keyword_null_only_branch)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def apply_null_union(hash, key, null_branch)
|
|
252
|
+
raise ArgumentError, "#{key} must be an Array of schemas" unless hash[key].is_a?(Array)
|
|
253
|
+
|
|
254
|
+
branch = { key => hash.delete(key) }
|
|
255
|
+
branch["discriminator"] = hash.delete("discriminator") if hash.key?("discriminator")
|
|
256
|
+
hash["anyOf"] = [branch, null_branch]
|
|
257
|
+
end
|
|
258
|
+
|
|
162
259
|
def build_properties(properties)
|
|
163
260
|
return nil unless properties
|
|
164
261
|
return nil if properties.empty?
|
|
@@ -168,70 +265,80 @@ module GrapeOAS
|
|
|
168
265
|
end
|
|
169
266
|
end
|
|
170
267
|
|
|
171
|
-
def build_schema_or_ref(schema, include_metadata: true)
|
|
172
|
-
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
173
|
-
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
174
|
-
ref_name = schema.canonical_name.gsub("::", "_")
|
|
175
|
-
ref_hash = { "$ref" => "#/components/schemas/#{ref_name}" }
|
|
176
|
-
return ref_hash unless include_metadata
|
|
177
|
-
|
|
178
|
-
result = {}
|
|
179
|
-
result["description"] = schema.description.to_s if schema.description
|
|
180
|
-
result["default"] = schema.default unless schema.default.nil?
|
|
181
|
-
result["enum"] = normalize_enum(schema.enum, schema.type) if schema.enum
|
|
182
|
-
sanitize_enum_against_type(result, type: schema.type)
|
|
183
|
-
apply_all_constraints(result, schema)
|
|
184
|
-
result.merge!(schema.extensions) if schema.extensions
|
|
185
|
-
apply_nullable_to_ref(result, schema)
|
|
186
|
-
if result.empty?
|
|
187
|
-
ref_hash
|
|
188
|
-
else
|
|
189
|
-
result["allOf"] = [ref_hash]
|
|
190
|
-
result
|
|
191
|
-
end
|
|
192
|
-
else
|
|
193
|
-
built = Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
194
|
-
strip_items_metadata(built) unless include_metadata
|
|
195
|
-
built
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
268
|
def strip_items_metadata(hash)
|
|
200
269
|
hash.delete("description")
|
|
201
270
|
end
|
|
202
271
|
|
|
203
272
|
def apply_nullable_to_ref(result, schema)
|
|
204
|
-
return unless
|
|
273
|
+
return unless schema_nullable?(schema)
|
|
205
274
|
|
|
206
275
|
case @nullable_strategy
|
|
207
276
|
when Constants::NullableStrategy::KEYWORD
|
|
208
|
-
result
|
|
277
|
+
apply_ref_null_union(result, keyword_null_only_branch, typed_nullable: true)
|
|
209
278
|
when Constants::NullableStrategy::EXTENSION
|
|
210
279
|
result["x-nullable"] = true
|
|
211
280
|
when Constants::NullableStrategy::TYPE_ARRAY
|
|
212
|
-
|
|
213
|
-
# applied to a $ref schema. For refs we intentionally do nothing.
|
|
214
|
-
nil
|
|
281
|
+
apply_ref_null_union(result, { "type" => "null" }, typed_nullable: false)
|
|
215
282
|
end
|
|
216
283
|
end
|
|
217
284
|
|
|
218
|
-
|
|
285
|
+
# Prefer a bare $ref as the non-null alternative when allOf would only
|
|
286
|
+
# wrap that single ref (OAS 3.0 needs allOf only for $ref siblings).
|
|
287
|
+
def apply_ref_null_union(result, null_branch, typed_nullable:)
|
|
288
|
+
allof = result["allOf"]
|
|
289
|
+
if bare_single_ref_allof?(allof)
|
|
290
|
+
ref = result.delete("allOf").first
|
|
291
|
+
result["nullable"] = true if typed_nullable && result["type"]
|
|
292
|
+
result["anyOf"] = [ref, null_branch]
|
|
293
|
+
elsif typed_nullable
|
|
294
|
+
apply_keyword_null_union(result, "allOf")
|
|
295
|
+
else
|
|
296
|
+
apply_null_union(result, "allOf", null_branch)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def bare_single_ref_allof?(allof)
|
|
301
|
+
allof.is_a?(Array) && allof.size == 1 && allof.first.is_a?(Hash) && allof.first.keys == ["$ref"]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def keyword_null_only_branch
|
|
305
|
+
{ "type" => Constants::SchemaTypes::OBJECT, "nullable" => true, "enum" => [nil] }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def normalize_enum(enum_vals, type, nullable: false)
|
|
219
309
|
return nil unless enum_vals.is_a?(Array)
|
|
220
310
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
|
|
317
|
+
result = enum_vals.each_with_object([]) do |v, acc|
|
|
318
|
+
next if v.nil?
|
|
228
319
|
|
|
229
|
-
|
|
320
|
+
coerced_v = case resolved_type
|
|
321
|
+
when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
|
|
322
|
+
when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
|
|
323
|
+
else v
|
|
324
|
+
end
|
|
325
|
+
acc << coerced_v unless coerced_v.nil?
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
result.uniq!
|
|
329
|
+
result.push(nil) if has_nil
|
|
230
330
|
return nil if result.empty?
|
|
231
331
|
|
|
232
332
|
result
|
|
233
333
|
end
|
|
234
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
|
+
end
|
|
341
|
+
|
|
235
342
|
def apply_numeric_constraints(hash, schema = @schema)
|
|
236
343
|
hash["minimum"] = schema.minimum unless schema.minimum.nil?
|
|
237
344
|
hash["maximum"] = schema.maximum unless schema.maximum.nil?
|
|
@@ -260,55 +367,96 @@ module GrapeOAS
|
|
|
260
367
|
def apply_array_constraints(hash, schema = @schema)
|
|
261
368
|
hash["minItems"] = schema.min_items unless schema.min_items.nil?
|
|
262
369
|
hash["maxItems"] = schema.max_items unless schema.max_items.nil?
|
|
370
|
+
hash["uniqueItems"] = true if schema.unique_items
|
|
263
371
|
end
|
|
264
372
|
|
|
265
|
-
# Ensure enum values match the declared type; drop enum if incompatible to avoid invalid specs
|
|
373
|
+
# Ensure enum values match the declared type; drop enum if incompatible to avoid invalid specs.
|
|
266
374
|
def sanitize_enum_against_type(hash, type: nil)
|
|
375
|
+
# A literal `"enum" => nil` (from a nil-only enum on a non-nullable schema) must not
|
|
376
|
+
# survive: composition/$ref callers never `.compact` their result hash, so an unguarded
|
|
377
|
+
# nil would leak as `enum: null` and, for $ref, would make `result.empty?` false and
|
|
378
|
+
# force an unwanted `allOf` wrapper.
|
|
379
|
+
return hash.delete("enum") if hash.key?("enum") && hash["enum"].nil?
|
|
380
|
+
|
|
267
381
|
enum_vals = hash["enum"]
|
|
268
382
|
type_val = type || hash["type"]
|
|
269
383
|
return unless enum_vals && type_val
|
|
270
384
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
else
|
|
274
|
-
type_val
|
|
275
|
-
end
|
|
385
|
+
base = base_type_for(type_val)
|
|
386
|
+
return hash.delete("enum") if base.nil? || base == Constants::SchemaTypes::ARRAY || base == Constants::SchemaTypes::OBJECT
|
|
276
387
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
hash.delete("enum")
|
|
388
|
+
non_nil_vals = enum_vals.compact
|
|
389
|
+
|
|
390
|
+
case base
|
|
281
391
|
when Constants::SchemaTypes::INTEGER
|
|
282
|
-
hash.delete("enum") unless
|
|
392
|
+
hash.delete("enum") unless non_nil_vals.all?(Integer)
|
|
283
393
|
when Constants::SchemaTypes::NUMBER
|
|
284
|
-
hash.delete("enum") unless
|
|
394
|
+
hash.delete("enum") unless non_nil_vals.all?(Numeric)
|
|
285
395
|
when Constants::SchemaTypes::BOOLEAN
|
|
286
|
-
hash.delete("enum") unless
|
|
396
|
+
hash.delete("enum") unless non_nil_vals.all? { |v| v == true || v == false } # rubocop:disable Style/MultipleComparison
|
|
287
397
|
else # string and fallback
|
|
288
|
-
hash.delete("enum") unless
|
|
398
|
+
hash.delete("enum") unless non_nil_vals.all?(String)
|
|
289
399
|
end
|
|
290
400
|
end
|
|
291
401
|
|
|
292
402
|
def coerce_example(example, type_val)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
403
|
+
return nil if example.nil?
|
|
404
|
+
|
|
405
|
+
base_type = base_type_for(type_val)
|
|
406
|
+
case base_type
|
|
407
|
+
when Constants::SchemaTypes::ARRAY
|
|
408
|
+
return example if example.is_a?(Array)
|
|
409
|
+
|
|
410
|
+
return nil
|
|
411
|
+
when Constants::SchemaTypes::OBJECT
|
|
412
|
+
return example if example.is_a?(Hash)
|
|
413
|
+
|
|
414
|
+
return nil
|
|
415
|
+
when nil
|
|
416
|
+
return example
|
|
417
|
+
else
|
|
418
|
+
return nil if example.is_a?(Hash)
|
|
419
|
+
|
|
420
|
+
if example.is_a?(Array)
|
|
421
|
+
return nil unless example.size == 1
|
|
422
|
+
|
|
423
|
+
example = example.first
|
|
424
|
+
end
|
|
425
|
+
end
|
|
298
426
|
|
|
299
427
|
case base_type
|
|
300
428
|
when Constants::SchemaTypes::INTEGER
|
|
301
|
-
example
|
|
429
|
+
coerce_integer_example(example)
|
|
302
430
|
when Constants::SchemaTypes::NUMBER
|
|
303
|
-
example
|
|
431
|
+
coerce_number_example(example)
|
|
304
432
|
when Constants::SchemaTypes::BOOLEAN
|
|
305
|
-
|
|
306
|
-
|
|
433
|
+
case example
|
|
434
|
+
when true, false
|
|
435
|
+
example
|
|
436
|
+
when String
|
|
437
|
+
return true if example.casecmp("true").zero?
|
|
438
|
+
return false if example.casecmp("false").zero?
|
|
439
|
+
|
|
440
|
+
nil
|
|
441
|
+
end
|
|
442
|
+
when Constants::SchemaTypes::STRING
|
|
307
443
|
example.to_s
|
|
308
444
|
else
|
|
309
445
|
example
|
|
310
446
|
end
|
|
311
447
|
end
|
|
448
|
+
|
|
449
|
+
def coerce_integer_example(example)
|
|
450
|
+
example.is_a?(String) ? Integer(example, 10) : Integer(example)
|
|
451
|
+
rescue ArgumentError, TypeError
|
|
452
|
+
nil
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def coerce_number_example(example)
|
|
456
|
+
Float(example)
|
|
457
|
+
rescue ArgumentError, TypeError
|
|
458
|
+
nil
|
|
459
|
+
end
|
|
312
460
|
end
|
|
313
461
|
end
|
|
314
462
|
end
|
|
@@ -13,10 +13,12 @@ module GrapeOAS
|
|
|
13
13
|
def build
|
|
14
14
|
hash = super
|
|
15
15
|
|
|
16
|
-
# swap example -> examples if present
|
|
16
|
+
# swap example -> examples if present. The OAS3 `example` is a single
|
|
17
|
+
# value, so wrap it as a one-element list (a value that is itself an
|
|
18
|
+
# array is one array-valued example, not many examples).
|
|
17
19
|
if hash.key?("example")
|
|
18
20
|
ex = hash.delete("example")
|
|
19
|
-
hash["examples"]
|
|
21
|
+
hash["examples"] = [ex] unless hash.key?("examples")
|
|
20
22
|
end
|
|
21
23
|
normalize_examples!(hash)
|
|
22
24
|
hash
|
|
@@ -24,6 +26,14 @@ module GrapeOAS
|
|
|
24
26
|
|
|
25
27
|
private
|
|
26
28
|
|
|
29
|
+
# OAS 3.1: files use JSON Schema content-* keywords instead of
|
|
30
|
+
# the OAS 3.0 `format: binary` convention.
|
|
31
|
+
def apply_file_schema_attributes!(hash)
|
|
32
|
+
hash.delete("format")
|
|
33
|
+
hash["contentMediaType"] = "application/octet-stream"
|
|
34
|
+
hash["contentEncoding"] = "binary"
|
|
35
|
+
end
|
|
36
|
+
|
|
27
37
|
# Ensure examples is always an array and recurse into nested schemas
|
|
28
38
|
def normalize_examples!(hash)
|
|
29
39
|
hash["examples"] = [hash["examples"]].compact if hash.key?("examples") && !hash["examples"].is_a?(Array)
|
|
@@ -29,7 +29,7 @@ module GrapeOAS
|
|
|
29
29
|
def nullable_strategy
|
|
30
30
|
# OAS 3.1 always uses JSON Schema null unions ("type": ["string", "null"]).
|
|
31
31
|
# The "nullable" keyword and "x-nullable" extension are not valid in OAS 3.1.
|
|
32
|
-
Constants::NullableStrategy::
|
|
32
|
+
Constants::NullableStrategy::OAS31_DEFAULT
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -20,6 +20,7 @@ module GrapeOAS
|
|
|
20
20
|
"tags" => build_tags,
|
|
21
21
|
"paths" => OAS3::Paths.new(@api, @ref_tracker,
|
|
22
22
|
nullable_strategy: nullable_strategy,
|
|
23
|
+
schema_builder: schema_builder,
|
|
23
24
|
suppress_default_error_response: @api.suppress_default_error_response,).build,
|
|
24
25
|
"components" => build_components,
|
|
25
26
|
"security" => build_security
|
|
@@ -88,7 +89,7 @@ module GrapeOAS
|
|
|
88
89
|
|
|
89
90
|
processed << canonical_name
|
|
90
91
|
|
|
91
|
-
ref_name =
|
|
92
|
+
ref_name = GrapeOAS.schema_ref_name.call(canonical_name)
|
|
92
93
|
schema = find_schema_by_canonical_name(canonical_name)
|
|
93
94
|
if schema
|
|
94
95
|
schemas[ref_name] =
|
|
@@ -120,7 +121,7 @@ module GrapeOAS
|
|
|
120
121
|
end
|
|
121
122
|
|
|
122
123
|
def nullable_strategy
|
|
123
|
-
@api.nullable_strategy || Constants::NullableStrategy::
|
|
124
|
+
@api.nullable_strategy || Constants::NullableStrategy::OAS3_DEFAULT
|
|
124
125
|
end
|
|
125
126
|
end
|
|
126
127
|
end
|
|
@@ -119,7 +119,8 @@ module GrapeOAS
|
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def entity_doc
|
|
122
|
-
@entity_class.respond_to?(:documentation) ? (@entity_class.documentation || {}) : {}
|
|
122
|
+
doc = @entity_class.respond_to?(:documentation) ? (@entity_class.documentation || {}) : {}
|
|
123
|
+
DocKeyNormalizer.normalize(doc)
|
|
123
124
|
rescue NoMethodError
|
|
124
125
|
{}
|
|
125
126
|
end
|
|
@@ -19,8 +19,6 @@ module GrapeOAS
|
|
|
19
19
|
# @param schema [ApiModel::Schema] the schema to populate
|
|
20
20
|
def add_exposures_to_schema(schema)
|
|
21
21
|
exposures.each do |exposure|
|
|
22
|
-
next unless exposed?(exposure)
|
|
23
|
-
|
|
24
22
|
add_exposure_to_schema(schema, exposure)
|
|
25
23
|
end
|
|
26
24
|
end
|
|
@@ -73,8 +71,11 @@ module GrapeOAS
|
|
|
73
71
|
#
|
|
74
72
|
# @param exposure the entity exposure
|
|
75
73
|
# @return [Boolean] true if exposed
|
|
76
|
-
def exposed?(
|
|
77
|
-
|
|
74
|
+
def exposed?(exposure)
|
|
75
|
+
doc = normalize_doc_keys(exposure.documentation || {})
|
|
76
|
+
hidden = doc[:hidden]
|
|
77
|
+
hidden = hidden.call if hidden.respond_to?(:call)
|
|
78
|
+
!hidden
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
# Checks if an exposure is conditional.
|
|
@@ -128,6 +129,8 @@ module GrapeOAS
|
|
|
128
129
|
end
|
|
129
130
|
|
|
130
131
|
def add_exposure_to_schema(schema, exposure)
|
|
132
|
+
return unless exposed?(exposure)
|
|
133
|
+
|
|
131
134
|
doc = normalize_doc_keys(exposure.documentation || {})
|
|
132
135
|
opts = exposure_options(exposure)
|
|
133
136
|
|
|
@@ -155,7 +158,9 @@ module GrapeOAS
|
|
|
155
158
|
is_array = doc[:is_array]
|
|
156
159
|
return prop_schema unless is_array
|
|
157
160
|
|
|
158
|
-
ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: prop_schema)
|
|
161
|
+
array_schema = ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: prop_schema)
|
|
162
|
+
array_schema.examples = doc[:example] if array_valued_example?(doc)
|
|
163
|
+
array_schema
|
|
159
164
|
end
|
|
160
165
|
|
|
161
166
|
# Detects block-based nesting exposures (NestingExposure) that should become
|
|
@@ -178,6 +183,8 @@ module GrapeOAS
|
|
|
178
183
|
nesting_accum = {}
|
|
179
184
|
nesting_required = Hash.new { |h, k| h[k] = [] }
|
|
180
185
|
Array(exposure.nested_exposures).each do |child_exposure|
|
|
186
|
+
next unless exposed?(child_exposure)
|
|
187
|
+
|
|
181
188
|
if nesting_exposure?(child_exposure)
|
|
182
189
|
key = child_exposure.key.to_s
|
|
183
190
|
child_doc = normalize_doc_keys(child_exposure.documentation || {})
|
|
@@ -200,7 +207,16 @@ module GrapeOAS
|
|
|
200
207
|
end
|
|
201
208
|
|
|
202
209
|
def apply_exposure_properties(schema, doc)
|
|
203
|
-
|
|
210
|
+
nullable = PropertyExtractor.extract_nullable(doc)
|
|
211
|
+
if nullable && schema.canonical_name
|
|
212
|
+
# Don't mutate the shared cached entity schema. Create a wrapper with
|
|
213
|
+
# all_of so the exporter emits { nullable: true, allOf: [{ $ref }] }.
|
|
214
|
+
# collect_refs traverses all_of and registers the original schema as a
|
|
215
|
+
# component, keeping its definition free of this property's nullable.
|
|
216
|
+
schema = ApiModel::Schema.new(nullable: true, all_of: [schema])
|
|
217
|
+
else
|
|
218
|
+
schema.nullable = nullable
|
|
219
|
+
end
|
|
204
220
|
raw_values = doc[:values]
|
|
205
221
|
if raw_values
|
|
206
222
|
normalized = ValuesNormalizer.normalize(raw_values, context: "entity exposure values")
|
|
@@ -212,7 +228,7 @@ module GrapeOAS
|
|
|
212
228
|
end
|
|
213
229
|
schema.description = doc[:desc] if doc[:desc]
|
|
214
230
|
schema.format = doc[:format] if doc[:format]
|
|
215
|
-
schema
|
|
231
|
+
schema = apply_example_to_schema(schema, doc[:example]) if doc.key?(:example) && !array_valued_example?(doc)
|
|
216
232
|
schema.additional_properties = doc[:additional_properties] if doc.key?(:additional_properties)
|
|
217
233
|
schema.unevaluated_properties = doc[:unevaluated_properties] if doc.key?(:unevaluated_properties)
|
|
218
234
|
defs = doc[:defs] || doc[:$defs]
|
|
@@ -223,17 +239,24 @@ module GrapeOAS
|
|
|
223
239
|
end
|
|
224
240
|
|
|
225
241
|
# Cached entity schemas (via using:) are shared across all exposures that
|
|
226
|
-
# reference the same entity — dup before
|
|
227
|
-
# the shared schema; emit a warning so users know a
|
|
242
|
+
# reference the same entity — dup before mutating them (enum, example, ...)
|
|
243
|
+
# to avoid corrupting the shared schema; emit a warning so users know a
|
|
244
|
+
# dup occurred.
|
|
228
245
|
#
|
|
246
|
+
# @return [ApiModel::Schema] a dup of schema with canonical_name cleared
|
|
247
|
+
def dup_cached_schema(schema, reason:, value:)
|
|
248
|
+
GrapeOAS.logger.warn(
|
|
249
|
+
"Duplicating cached schema '#{schema.canonical_name}' to apply #{reason} #{value.inspect}",
|
|
250
|
+
)
|
|
251
|
+
dup = schema.dup
|
|
252
|
+
dup.canonical_name = nil
|
|
253
|
+
dup
|
|
254
|
+
end
|
|
255
|
+
|
|
229
256
|
# @return [ApiModel::Schema] the schema (or a dup) with enum applied
|
|
230
257
|
def apply_enum_to_schema(schema, values)
|
|
231
258
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
232
|
-
|
|
233
|
-
"Duplicating cached schema '#{schema.canonical_name}' to apply enum #{values.inspect}",
|
|
234
|
-
)
|
|
235
|
-
schema = schema.dup
|
|
236
|
-
schema.canonical_name = nil
|
|
259
|
+
schema = dup_cached_schema(schema, reason: "enum", value: values)
|
|
237
260
|
schema.enum = values
|
|
238
261
|
return schema
|
|
239
262
|
end
|
|
@@ -241,13 +264,9 @@ module GrapeOAS
|
|
|
241
264
|
if schema.type == Constants::SchemaTypes::ARRAY &&
|
|
242
265
|
schema.respond_to?(:items) && schema.items
|
|
243
266
|
if schema.items.respond_to?(:canonical_name) && schema.items.canonical_name
|
|
244
|
-
|
|
245
|
-
"Duplicating cached schema '#{schema.items.canonical_name}' to apply enum #{values.inspect}",
|
|
246
|
-
)
|
|
247
|
-
schema = schema.dup
|
|
248
|
-
items_dup = schema.items.dup
|
|
249
|
-
items_dup.canonical_name = nil
|
|
267
|
+
items_dup = dup_cached_schema(schema.items, reason: "enum", value: values)
|
|
250
268
|
items_dup.enum = values
|
|
269
|
+
schema = schema.dup
|
|
251
270
|
schema.items = items_dup
|
|
252
271
|
else
|
|
253
272
|
schema.items.enum = values
|
|
@@ -258,6 +277,22 @@ module GrapeOAS
|
|
|
258
277
|
schema
|
|
259
278
|
end
|
|
260
279
|
|
|
280
|
+
def apply_example_to_schema(schema, example)
|
|
281
|
+
return schema unless schema.respond_to?(:examples=)
|
|
282
|
+
|
|
283
|
+
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
284
|
+
schema = dup_cached_schema(schema, reason: "example", value: example)
|
|
285
|
+
end
|
|
286
|
+
schema.examples = example
|
|
287
|
+
schema
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# True when doc[:example] describes the whole array (not a single item),
|
|
291
|
+
# i.e. is_array: true and the example is itself an Array.
|
|
292
|
+
def array_valued_example?(doc)
|
|
293
|
+
doc[:is_array] && doc[:example].is_a?(Array)
|
|
294
|
+
end
|
|
295
|
+
|
|
261
296
|
def normalize_doc_keys(doc)
|
|
262
297
|
DocKeyNormalizer.normalize(doc)
|
|
263
298
|
end
|