grape-oas 1.3.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 +59 -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 +6 -1
- data/lib/grape_oas/api_model/schema.rb +2 -2
- data/lib/grape_oas/api_model_builder.rb +3 -2
- data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +30 -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 +7 -8
- 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 +29 -22
- 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_location_resolver.rb +7 -2
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +4 -0
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +14 -9
- data/lib/grape_oas/api_model_builders/response.rb +28 -4
- data/lib/grape_oas/constants.rb +22 -1
- 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 +74 -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 +85 -38
- 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 +8 -3
- 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 +273 -117
- 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 +7 -2
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +55 -20
- data/lib/grape_oas/introspectors/entity_introspector_support/inheritance_builder.rb +1 -1
- 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/introspectors/entity_introspector_support.rb +24 -0
- data/lib/grape_oas/range_utils.rb +25 -2
- data/lib/grape_oas/type_resolvers/array_resolver.rb +54 -22
- 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,28 +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
|
|
45
|
+
schema_hash["default"] = @schema.default unless @schema.default.nil?
|
|
54
46
|
schema_hash
|
|
55
47
|
end
|
|
56
48
|
|
|
57
49
|
def apply_examples(schema_hash)
|
|
58
|
-
return
|
|
50
|
+
return if @schema.examples.nil?
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
schema_hash["example"] = examples
|
|
52
|
+
type = schema_hash["type"]
|
|
53
|
+
schema_hash["example"] = coerce_example(@schema.examples, type)
|
|
62
54
|
end
|
|
63
55
|
|
|
64
56
|
def apply_extensions_and_extra_properties(schema_hash)
|
|
@@ -72,34 +64,92 @@ module GrapeOAS
|
|
|
72
64
|
schema_hash["discriminator"] = build_discriminator if @schema.discriminator
|
|
73
65
|
end
|
|
74
66
|
|
|
75
|
-
def apply_all_constraints(schema_hash)
|
|
76
|
-
apply_numeric_constraints(schema_hash)
|
|
77
|
-
apply_string_constraints(schema_hash)
|
|
78
|
-
apply_array_constraints(schema_hash)
|
|
67
|
+
def apply_all_constraints(schema_hash, schema = @schema)
|
|
68
|
+
apply_numeric_constraints(schema_hash, schema)
|
|
69
|
+
apply_string_constraints(schema_hash, schema)
|
|
70
|
+
apply_array_constraints(schema_hash, schema)
|
|
71
|
+
end
|
|
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
|
|
79
109
|
end
|
|
80
110
|
|
|
81
111
|
private
|
|
82
112
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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)
|
|
87
130
|
end
|
|
131
|
+
end
|
|
88
132
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
|
|
139
|
+
# Build allOf schema for inheritance
|
|
140
|
+
def build_all_of_schema
|
|
141
|
+
items = @schema.all_of.map { |item| build_schema_or_ref(item) }
|
|
142
|
+
result = { "allOf" => items }
|
|
143
|
+
apply_composition_attributes(result)
|
|
91
144
|
apply_nullable(result)
|
|
92
145
|
result
|
|
93
146
|
end
|
|
94
147
|
|
|
95
148
|
# Build oneOf schema for polymorphism
|
|
96
149
|
def build_one_of_schema
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
result = { "oneOf" => one_of_items }
|
|
102
|
-
result["description"] = @schema.description.to_s if @schema.description
|
|
150
|
+
items = @schema.one_of.map { |item| build_schema_or_ref(item) }
|
|
151
|
+
result = { "oneOf" => items }
|
|
152
|
+
apply_composition_attributes(result)
|
|
103
153
|
result["discriminator"] = build_discriminator if @schema.discriminator
|
|
104
154
|
apply_nullable(result)
|
|
105
155
|
result
|
|
@@ -107,17 +157,37 @@ module GrapeOAS
|
|
|
107
157
|
|
|
108
158
|
# Build anyOf schema for polymorphism
|
|
109
159
|
def build_any_of_schema
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
result = { "anyOf" => any_of_items }
|
|
115
|
-
result["description"] = @schema.description.to_s if @schema.description
|
|
160
|
+
items = @schema.any_of.map { |item| build_schema_or_ref(item) }
|
|
161
|
+
result = { "anyOf" => items }
|
|
162
|
+
apply_composition_attributes(result)
|
|
116
163
|
result["discriminator"] = build_discriminator if @schema.discriminator
|
|
117
164
|
apply_nullable(result)
|
|
118
165
|
result
|
|
119
166
|
end
|
|
120
167
|
|
|
168
|
+
def apply_composition_attributes(result)
|
|
169
|
+
result["type"] = nullable_type if @schema.type
|
|
170
|
+
result["format"] = @schema.format if @schema.format
|
|
171
|
+
result["description"] = @schema.description.to_s if @schema.description
|
|
172
|
+
result["default"] = @schema.default unless @schema.default.nil?
|
|
173
|
+
result["enum"] = normalize_enum(@schema.enum, result["type"], nullable: nullable?) if @schema.enum
|
|
174
|
+
sanitize_enum_against_type(result)
|
|
175
|
+
apply_all_constraints(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)
|
|
189
|
+
end
|
|
190
|
+
|
|
121
191
|
# Build OAS3 discriminator object
|
|
122
192
|
def build_discriminator
|
|
123
193
|
return nil unless @schema.discriminator
|
|
@@ -134,8 +204,12 @@ module GrapeOAS
|
|
|
134
204
|
end
|
|
135
205
|
end
|
|
136
206
|
|
|
207
|
+
def schema_nullable?(schema)
|
|
208
|
+
schema.respond_to?(:nullable) && !!schema.nullable
|
|
209
|
+
end
|
|
210
|
+
|
|
137
211
|
def nullable?
|
|
138
|
-
|
|
212
|
+
schema_nullable?(@schema)
|
|
139
213
|
end
|
|
140
214
|
|
|
141
215
|
def nullable_type
|
|
@@ -148,14 +222,40 @@ module GrapeOAS
|
|
|
148
222
|
def apply_nullable(schema_hash)
|
|
149
223
|
return unless nullable?
|
|
150
224
|
|
|
225
|
+
composition_key = %w[allOf oneOf anyOf].find { |k| schema_hash.key?(k) }
|
|
151
226
|
case @nullable_strategy
|
|
152
227
|
when Constants::NullableStrategy::KEYWORD
|
|
153
|
-
|
|
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
|
|
154
233
|
when Constants::NullableStrategy::EXTENSION
|
|
155
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
|
|
156
241
|
end
|
|
157
242
|
end
|
|
158
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
|
+
|
|
159
259
|
def build_properties(properties)
|
|
160
260
|
return nil unless properties
|
|
161
261
|
return nil if properties.empty?
|
|
@@ -165,142 +265,198 @@ module GrapeOAS
|
|
|
165
265
|
end
|
|
166
266
|
end
|
|
167
267
|
|
|
168
|
-
def build_schema_or_ref(schema, include_metadata: true)
|
|
169
|
-
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
170
|
-
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
171
|
-
ref_name = schema.canonical_name.gsub("::", "_")
|
|
172
|
-
ref_hash = { "$ref" => "#/components/schemas/#{ref_name}" }
|
|
173
|
-
return ref_hash unless include_metadata
|
|
174
|
-
|
|
175
|
-
result = {}
|
|
176
|
-
result["description"] = schema.description.to_s if schema.description
|
|
177
|
-
apply_nullable_to_ref(result, schema)
|
|
178
|
-
if result.empty?
|
|
179
|
-
ref_hash
|
|
180
|
-
else
|
|
181
|
-
result["allOf"] = [ref_hash]
|
|
182
|
-
result
|
|
183
|
-
end
|
|
184
|
-
else
|
|
185
|
-
built = Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
186
|
-
strip_items_metadata(built) unless include_metadata
|
|
187
|
-
built
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
268
|
def strip_items_metadata(hash)
|
|
192
269
|
hash.delete("description")
|
|
193
270
|
end
|
|
194
271
|
|
|
195
272
|
def apply_nullable_to_ref(result, schema)
|
|
196
|
-
return unless
|
|
273
|
+
return unless schema_nullable?(schema)
|
|
197
274
|
|
|
198
275
|
case @nullable_strategy
|
|
199
276
|
when Constants::NullableStrategy::KEYWORD
|
|
200
|
-
result
|
|
277
|
+
apply_ref_null_union(result, keyword_null_only_branch, typed_nullable: true)
|
|
201
278
|
when Constants::NullableStrategy::EXTENSION
|
|
202
279
|
result["x-nullable"] = true
|
|
203
280
|
when Constants::NullableStrategy::TYPE_ARRAY
|
|
204
|
-
|
|
205
|
-
# applied to a $ref schema. For refs we intentionally do nothing.
|
|
206
|
-
nil
|
|
281
|
+
apply_ref_null_union(result, { "type" => "null" }, typed_nullable: false)
|
|
207
282
|
end
|
|
208
283
|
end
|
|
209
284
|
|
|
210
|
-
|
|
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)
|
|
211
309
|
return nil unless enum_vals.is_a?(Array)
|
|
212
310
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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?
|
|
319
|
+
|
|
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
|
|
220
327
|
|
|
221
|
-
result
|
|
328
|
+
result.uniq!
|
|
329
|
+
result.push(nil) if has_nil
|
|
222
330
|
return nil if result.empty?
|
|
223
331
|
|
|
224
332
|
result
|
|
225
333
|
end
|
|
226
334
|
|
|
227
|
-
def
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
|
|
342
|
+
def apply_numeric_constraints(hash, schema = @schema)
|
|
343
|
+
hash["minimum"] = schema.minimum unless schema.minimum.nil?
|
|
344
|
+
hash["maximum"] = schema.maximum unless schema.maximum.nil?
|
|
230
345
|
|
|
231
346
|
if @nullable_strategy == Constants::NullableStrategy::TYPE_ARRAY
|
|
232
|
-
if
|
|
233
|
-
hash["exclusiveMinimum"] =
|
|
347
|
+
if schema.exclusive_minimum && !schema.minimum.nil?
|
|
348
|
+
hash["exclusiveMinimum"] = schema.minimum
|
|
234
349
|
hash.delete("minimum")
|
|
235
350
|
end
|
|
236
|
-
if
|
|
237
|
-
hash["exclusiveMaximum"] =
|
|
351
|
+
if schema.exclusive_maximum && !schema.maximum.nil?
|
|
352
|
+
hash["exclusiveMaximum"] = schema.maximum
|
|
238
353
|
hash.delete("maximum")
|
|
239
354
|
end
|
|
240
355
|
else
|
|
241
|
-
hash["exclusiveMinimum"] =
|
|
242
|
-
hash["exclusiveMaximum"] =
|
|
356
|
+
hash["exclusiveMinimum"] = schema.exclusive_minimum if schema.exclusive_minimum
|
|
357
|
+
hash["exclusiveMaximum"] = schema.exclusive_maximum if schema.exclusive_maximum
|
|
243
358
|
end
|
|
244
359
|
end
|
|
245
360
|
|
|
246
|
-
def apply_string_constraints(hash)
|
|
247
|
-
hash["minLength"] =
|
|
248
|
-
hash["maxLength"] =
|
|
249
|
-
hash["pattern"] =
|
|
361
|
+
def apply_string_constraints(hash, schema = @schema)
|
|
362
|
+
hash["minLength"] = schema.min_length unless schema.min_length.nil?
|
|
363
|
+
hash["maxLength"] = schema.max_length unless schema.max_length.nil?
|
|
364
|
+
hash["pattern"] = schema.pattern if schema.pattern
|
|
250
365
|
end
|
|
251
366
|
|
|
252
|
-
def apply_array_constraints(hash)
|
|
253
|
-
hash["minItems"] =
|
|
254
|
-
hash["maxItems"] =
|
|
367
|
+
def apply_array_constraints(hash, schema = @schema)
|
|
368
|
+
hash["minItems"] = schema.min_items unless schema.min_items.nil?
|
|
369
|
+
hash["maxItems"] = schema.max_items unless schema.max_items.nil?
|
|
370
|
+
hash["uniqueItems"] = true if schema.unique_items
|
|
255
371
|
end
|
|
256
372
|
|
|
257
|
-
# Ensure enum values match the declared type; drop enum if incompatible to avoid invalid specs
|
|
258
|
-
def sanitize_enum_against_type(hash)
|
|
373
|
+
# Ensure enum values match the declared type; drop enum if incompatible to avoid invalid specs.
|
|
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
|
+
|
|
259
381
|
enum_vals = hash["enum"]
|
|
260
|
-
type_val = hash["type"]
|
|
382
|
+
type_val = type || hash["type"]
|
|
261
383
|
return unless enum_vals && type_val
|
|
262
384
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
else
|
|
266
|
-
type_val
|
|
267
|
-
end
|
|
385
|
+
base = base_type_for(type_val)
|
|
386
|
+
return hash.delete("enum") if base.nil? || base == Constants::SchemaTypes::ARRAY || base == Constants::SchemaTypes::OBJECT
|
|
268
387
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
hash.delete("enum")
|
|
388
|
+
non_nil_vals = enum_vals.compact
|
|
389
|
+
|
|
390
|
+
case base
|
|
273
391
|
when Constants::SchemaTypes::INTEGER
|
|
274
|
-
hash.delete("enum") unless
|
|
392
|
+
hash.delete("enum") unless non_nil_vals.all?(Integer)
|
|
275
393
|
when Constants::SchemaTypes::NUMBER
|
|
276
|
-
hash.delete("enum") unless
|
|
394
|
+
hash.delete("enum") unless non_nil_vals.all?(Numeric)
|
|
277
395
|
when Constants::SchemaTypes::BOOLEAN
|
|
278
|
-
hash.delete("enum") unless
|
|
396
|
+
hash.delete("enum") unless non_nil_vals.all? { |v| v == true || v == false } # rubocop:disable Style/MultipleComparison
|
|
279
397
|
else # string and fallback
|
|
280
|
-
hash.delete("enum") unless
|
|
398
|
+
hash.delete("enum") unless non_nil_vals.all?(String)
|
|
281
399
|
end
|
|
282
400
|
end
|
|
283
401
|
|
|
284
402
|
def coerce_example(example, type_val)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
290
426
|
|
|
291
427
|
case base_type
|
|
292
428
|
when Constants::SchemaTypes::INTEGER
|
|
293
|
-
example
|
|
429
|
+
coerce_integer_example(example)
|
|
294
430
|
when Constants::SchemaTypes::NUMBER
|
|
295
|
-
example
|
|
431
|
+
coerce_number_example(example)
|
|
296
432
|
when Constants::SchemaTypes::BOOLEAN
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
299
443
|
example.to_s
|
|
300
444
|
else
|
|
301
445
|
example
|
|
302
446
|
end
|
|
303
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
|
|
304
460
|
end
|
|
305
461
|
end
|
|
306
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
|
|
@@ -88,12 +88,16 @@ module GrapeOAS
|
|
|
88
88
|
def initialize_or_reuse_schema
|
|
89
89
|
@registry[@entity_class] ||= ApiModel::Schema.new(
|
|
90
90
|
type: Constants::SchemaTypes::OBJECT,
|
|
91
|
-
canonical_name:
|
|
91
|
+
canonical_name: resolve_canonical_name,
|
|
92
92
|
description: nil,
|
|
93
93
|
nullable: nil,
|
|
94
94
|
)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
def resolve_canonical_name
|
|
98
|
+
EntityIntrospectorSupport.resolve_canonical_name(@entity_class)
|
|
99
|
+
end
|
|
100
|
+
|
|
97
101
|
def populate_schema(schema)
|
|
98
102
|
doc = entity_doc
|
|
99
103
|
apply_schema_metadata(schema, doc)
|
|
@@ -115,7 +119,8 @@ module GrapeOAS
|
|
|
115
119
|
end
|
|
116
120
|
|
|
117
121
|
def entity_doc
|
|
118
|
-
@entity_class.respond_to?(:documentation) ? (@entity_class.documentation || {}) : {}
|
|
122
|
+
doc = @entity_class.respond_to?(:documentation) ? (@entity_class.documentation || {}) : {}
|
|
123
|
+
DocKeyNormalizer.normalize(doc)
|
|
119
124
|
rescue NoMethodError
|
|
120
125
|
{}
|
|
121
126
|
end
|