grape-oas 1.1.0 → 1.2.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 +15 -0
- data/lib/grape_oas/api_model/api.rb +2 -1
- data/lib/grape_oas/api_model_builder.rb +1 -0
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +10 -5
- data/lib/grape_oas/api_model_builders/request.rb +3 -158
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +2 -0
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +5 -19
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +7 -7
- data/lib/grape_oas/constants.rb +17 -2
- data/lib/grape_oas/exporter/oas2/operation.rb +4 -2
- data/lib/grape_oas/exporter/oas2/parameter.rb +3 -2
- data/lib/grape_oas/exporter/oas2/paths.rb +1 -0
- data/lib/grape_oas/exporter/oas2/response.rb +3 -2
- data/lib/grape_oas/exporter/oas2/schema.rb +20 -3
- data/lib/grape_oas/exporter/oas2_schema.rb +7 -2
- data/lib/grape_oas/exporter/oas3/operation.rb +4 -4
- data/lib/grape_oas/exporter/oas3/parameter.rb +3 -3
- data/lib/grape_oas/exporter/oas3/paths.rb +2 -3
- data/lib/grape_oas/exporter/oas3/request_body.rb +3 -3
- data/lib/grape_oas/exporter/oas3/response.rb +3 -3
- data/lib/grape_oas/exporter/oas3/schema.rb +52 -21
- data/lib/grape_oas/exporter/oas31_schema.rb +4 -2
- data/lib/grape_oas/exporter/oas3_schema.rb +4 -4
- data/lib/grape_oas/introspectors/dry_introspector_support/type_unwrapper.rb +15 -0
- data/lib/grape_oas/type_resolvers/array_resolver.rb +108 -0
- data/lib/grape_oas/type_resolvers/base.rb +110 -0
- data/lib/grape_oas/type_resolvers/dry_type_resolver.rb +135 -0
- data/lib/grape_oas/type_resolvers/primitive_resolver.rb +105 -0
- data/lib/grape_oas/type_resolvers/registry.rb +137 -0
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +41 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f97a967ef31212399ee7765f184a358e755b4364578cbc43d3fdaf75a919638
|
|
4
|
+
data.tar.gz: c56afb98329feaa1227d6ee252cbf5866ddc0144eac15dc6db8e34f9718d4731
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efed55aaa9f34d9045e495b2c8cbccea7b4da8381b4d2c58628e20785947319f42cc3a1e9952bac87219cb290cdbb81ac31bca7107acc3be3db13e027087c1ac
|
|
7
|
+
data.tar.gz: aa0eebcf7dd0b7122346150adfdf6e5dbfc8ed0287470ecedfe18ff9a95afca3376479899e0dc1e6298be3e65da3711238988b329839b4af68d48dd622bcd1f0
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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.2.0] - 2026-03-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- [#36](https://github.com/numbata/grape-oas/pull/36): Add extensible TypeResolvers for resolving Grape's stringified parameter types to OpenAPI schemas with rich metadata (format, enum, nullable) - [@numbata](https://github.com/numbata).
|
|
13
|
+
- [#37](https://github.com/numbata/grape-oas/pull/37): Replace boolean `nullable_keyword` with configurable `nullable_strategy` - [@numbata](https://github.com/numbata).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- [#41](https://github.com/numbata/grape-oas/pull/41): Fix `Set` enum values being silently dropped and `maybe(Coercible::Integer)` resolving to `string` instead of `integer` - [@numbata](https://github.com/numbata).
|
|
18
|
+
- [#40](https://github.com/numbata/grape-oas/pull/40): Remove dead `spec[:allow_nil]` and `spec[:nullable]` checks from `extract_nullable` — these values were never set by Grape or grape-swagger - [@numbata](https://github.com/numbata).
|
|
19
|
+
- [#39](https://github.com/numbata/grape-oas/pull/39): Support `documentation: { x: { nullable: true } }` on nested Hash params — nullable flag was ignored for object container schemas - [@numbata](https://github.com/numbata).
|
|
20
|
+
- [#38](https://github.com/numbata/grape-oas/pull/38): Wrap `$ref` in `allOf` when `description` or `nullable` is present — fixes sibling properties being ignored per OpenAPI spec - [@numbata](https://github.com/numbata).
|
|
21
|
+
- [#37](https://github.com/numbata/grape-oas/pull/37): Fix OAS 3.0 `nullable` keyword being constructed but not emitted in the generated output - [@numbata](https://github.com/numbata).
|
|
22
|
+
|
|
8
23
|
## [1.1.0] - 2026-01-23
|
|
9
24
|
|
|
10
25
|
### Added
|
|
@@ -11,7 +11,7 @@ module GrapeOAS
|
|
|
11
11
|
class API < Node
|
|
12
12
|
attr_accessor :title, :version, :paths, :servers, :tag_defs, :components,
|
|
13
13
|
:host, :base_path, :schemes, :security_definitions, :security,
|
|
14
|
-
:registered_schemas, :suppress_default_error_response
|
|
14
|
+
:registered_schemas, :suppress_default_error_response, :nullable_strategy
|
|
15
15
|
|
|
16
16
|
def initialize(title:, version:)
|
|
17
17
|
super()
|
|
@@ -28,6 +28,7 @@ module GrapeOAS
|
|
|
28
28
|
@security = []
|
|
29
29
|
@registered_schemas = []
|
|
30
30
|
@suppress_default_error_response = false
|
|
31
|
+
@nullable_strategy = nil
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def add_path(path)
|
|
@@ -19,6 +19,7 @@ module GrapeOAS
|
|
|
19
19
|
@api.servers = build_servers(options)
|
|
20
20
|
@api.registered_schemas = build_registered_schemas(options[:models])
|
|
21
21
|
@api.suppress_default_error_response = options[:suppress_default_error_response] || false
|
|
22
|
+
@api.nullable_strategy = options[:nullable_strategy]
|
|
22
23
|
|
|
23
24
|
@namespace_filter = options[:namespace]
|
|
24
25
|
@apis = []
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
begin
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# BigDecimal is an optional default gem dependency.
|
|
7
|
+
end
|
|
4
8
|
|
|
5
9
|
module GrapeOAS
|
|
6
10
|
module ApiModelBuilders
|
|
@@ -8,11 +12,11 @@ module GrapeOAS
|
|
|
8
12
|
# Centralizes Ruby type to OpenAPI schema type resolution.
|
|
9
13
|
# Used by request builders and introspectors to avoid duplicated type switching logic.
|
|
10
14
|
module TypeResolver
|
|
11
|
-
# Regex to match Grape's typed array notation like "[String]", "[Integer]"
|
|
12
|
-
TYPED_ARRAY_PATTERN =
|
|
15
|
+
# Regex to match Grape's typed array notation like "[String]", "[Integer]", "[MyModule::MyType]"
|
|
16
|
+
TYPED_ARRAY_PATTERN = /\A\[(\w+(?:::\w+)*)\]\z/
|
|
13
17
|
|
|
14
18
|
# Regex to match Grape's multi-type notation like "[String, Integer]", "[String, Float]"
|
|
15
|
-
MULTI_TYPE_PATTERN =
|
|
19
|
+
MULTI_TYPE_PATTERN = /\A\[(\w+(?:::\w+)*(?:,\s*\w+(?:::\w+)*)+)\]\z/
|
|
16
20
|
|
|
17
21
|
# Resolves a Ruby class or type name to its OpenAPI schema type string.
|
|
18
22
|
# Handles both Ruby classes (Integer, Float) and string type names ("integer", "float").
|
|
@@ -27,7 +31,8 @@ module GrapeOAS
|
|
|
27
31
|
# Handle Ruby classes directly
|
|
28
32
|
if type.is_a?(Class)
|
|
29
33
|
# Check static mapping first
|
|
30
|
-
|
|
34
|
+
mapped = Constants::RUBY_TYPE_MAPPING[type]
|
|
35
|
+
return mapped if mapped
|
|
31
36
|
|
|
32
37
|
# Handle Grape::API::Boolean dynamically (may not be loaded at constant definition time)
|
|
33
38
|
return Constants::SchemaTypes::BOOLEAN if grape_boolean_type?(type)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bigdecimal"
|
|
4
|
-
|
|
5
3
|
module GrapeOAS
|
|
6
4
|
module ApiModelBuilders
|
|
7
5
|
class Request
|
|
@@ -202,162 +200,9 @@ module GrapeOAS
|
|
|
202
200
|
|
|
203
201
|
# First try direct lookup (for Ruby class values like String, Integer)
|
|
204
202
|
# Then try class-based lookup (for actual runtime values like "hello", 123)
|
|
205
|
-
Constants::RUBY_TYPE_MAPPING
|
|
206
|
-
Constants::RUBY_TYPE_MAPPING
|
|
207
|
-
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def schema_from_types(types_hash, rule_constraints)
|
|
211
|
-
schema = GrapeOAS::ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
212
|
-
types_hash.each do |name, dry_type|
|
|
213
|
-
prop_schema = schema_for_type(dry_type)
|
|
214
|
-
merge_rule_constraints(prop_schema, rule_constraints[name]) if rule_constraints[name]
|
|
215
|
-
required = true
|
|
216
|
-
required = false if dry_type.respond_to?(:optional?) && dry_type.optional?
|
|
217
|
-
required = false if dry_type.respond_to?(:meta) && dry_type.meta[:omittable]
|
|
218
|
-
schema.add_property(name, prop_schema, required: required)
|
|
219
|
-
end
|
|
220
|
-
schema
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def schema_for_type(dry_type)
|
|
224
|
-
if dry_type.respond_to?(:primitive) && dry_type.primitive == Array && dry_type.respond_to?(:member)
|
|
225
|
-
items_schema = schema_for_type(dry_type.member)
|
|
226
|
-
schema = GrapeOAS::ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
227
|
-
apply_array_meta_constraints(schema, dry_type.respond_to?(:meta) ? dry_type.meta : {})
|
|
228
|
-
return schema
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
primitive, member = derive_primitive_and_member(dry_type)
|
|
232
|
-
if dry_type.respond_to?(:primitive) && dry_type.primitive == Array
|
|
233
|
-
member ||= dry_type.respond_to?(:member) ? dry_type.member : nil
|
|
234
|
-
primitive = Array
|
|
235
|
-
end
|
|
236
|
-
meta = dry_type.respond_to?(:meta) ? dry_type.meta : {}
|
|
237
|
-
nullable = dry_type.respond_to?(:optional?) && dry_type.optional?
|
|
238
|
-
enum_vals = dry_type.respond_to?(:values) ? dry_type.values : nil
|
|
239
|
-
|
|
240
|
-
schema = if primitive == Array
|
|
241
|
-
items_schema = member ? schema_for_type(member) : default_string_schema
|
|
242
|
-
s = GrapeOAS::ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
243
|
-
apply_array_meta_constraints(s, meta)
|
|
244
|
-
s
|
|
245
|
-
else
|
|
246
|
-
build_schema_for_primitive(primitive)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
schema.nullable = nullable
|
|
250
|
-
schema.enum = enum_vals if enum_vals
|
|
251
|
-
apply_string_meta_constraints(schema, meta) if primitive == String
|
|
252
|
-
apply_numeric_meta_constraints(schema, meta) if [Integer, Float, BigDecimal].include?(primitive)
|
|
253
|
-
schema
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def derive_primitive_and_member(dry_type)
|
|
257
|
-
if defined?(Dry::Types::Array::Member) && dry_type.respond_to?(:type) && dry_type.type.is_a?(Dry::Types::Array::Member)
|
|
258
|
-
return [Array, dry_type.type.member]
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
return [Array, dry_type.member] if dry_type.respond_to?(:member)
|
|
262
|
-
|
|
263
|
-
primitive = dry_type.respond_to?(:primitive) ? dry_type.primitive : nil
|
|
264
|
-
[primitive, nil]
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def apply_string_meta_constraints(schema, meta)
|
|
268
|
-
min_length = extract_min_constraint(meta)
|
|
269
|
-
max_length = extract_max_constraint(meta)
|
|
270
|
-
schema.min_length = min_length if min_length
|
|
271
|
-
schema.max_length = max_length if max_length
|
|
272
|
-
schema.pattern = meta[:pattern] if meta[:pattern]
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def apply_array_meta_constraints(schema, meta)
|
|
276
|
-
min_items = extract_min_constraint(meta, :min_items)
|
|
277
|
-
max_items = extract_max_constraint(meta, :max_items)
|
|
278
|
-
schema.min_items = min_items if min_items
|
|
279
|
-
schema.max_items = max_items if max_items
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Extract minimum constraint, supporting multiple key names
|
|
283
|
-
def extract_min_constraint(meta, specific_key = :min_length)
|
|
284
|
-
meta[:min_size] || meta[specific_key]
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Extract maximum constraint, supporting multiple key names
|
|
288
|
-
def extract_max_constraint(meta, specific_key = :max_length)
|
|
289
|
-
meta[:max_size] || meta[specific_key]
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def apply_numeric_meta_constraints(schema, meta)
|
|
293
|
-
if meta[:gt]
|
|
294
|
-
schema.minimum = meta[:gt]
|
|
295
|
-
schema.exclusive_minimum = true
|
|
296
|
-
elsif meta[:gteq]
|
|
297
|
-
schema.minimum = meta[:gteq]
|
|
298
|
-
end
|
|
299
|
-
if meta[:lt]
|
|
300
|
-
schema.maximum = meta[:lt]
|
|
301
|
-
schema.exclusive_maximum = true
|
|
302
|
-
elsif meta[:lteq]
|
|
303
|
-
schema.maximum = meta[:lteq]
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def merge_rule_constraints(schema, rule_constraints)
|
|
308
|
-
return unless rule_constraints
|
|
309
|
-
|
|
310
|
-
schema.enum ||= rule_constraints[:enum]
|
|
311
|
-
schema.nullable ||= rule_constraints[:nullable]
|
|
312
|
-
schema.min_length ||= rule_constraints[:min] if rule_constraints[:min]
|
|
313
|
-
schema.max_length ||= rule_constraints[:max] if rule_constraints[:max]
|
|
314
|
-
schema.minimum ||= rule_constraints[:minimum] if rule_constraints[:minimum]
|
|
315
|
-
schema.maximum ||= rule_constraints[:maximum] if rule_constraints[:maximum]
|
|
316
|
-
schema.exclusive_minimum ||= rule_constraints[:exclusive_minimum]
|
|
317
|
-
schema.exclusive_maximum ||= rule_constraints[:exclusive_maximum]
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
# Very small parser for FakeType rule_ast used in tests
|
|
321
|
-
def extract_rule_constraints(schema_obj)
|
|
322
|
-
return {} unless schema_obj.respond_to?(:rules)
|
|
323
|
-
|
|
324
|
-
# Only supports FakeSchema/FakeType used in tests
|
|
325
|
-
constraints = Hash.new { |h, k| h[k] = {} }
|
|
326
|
-
if schema_obj.respond_to?(:types)
|
|
327
|
-
schema_obj.types.each do |name, dry_type|
|
|
328
|
-
next unless dry_type.respond_to?(:rule_ast)
|
|
329
|
-
|
|
330
|
-
rules = dry_type.rule_ast
|
|
331
|
-
Array(rules).each do |rule|
|
|
332
|
-
next unless rule.is_a?(Array)
|
|
333
|
-
|
|
334
|
-
_, pred = rule
|
|
335
|
-
next unless pred.is_a?(Array)
|
|
336
|
-
|
|
337
|
-
pname, pargs = pred
|
|
338
|
-
case pname
|
|
339
|
-
when :size?
|
|
340
|
-
min, max = Array(pargs).first
|
|
341
|
-
constraints[name][:min] = min
|
|
342
|
-
constraints[name][:max] = max
|
|
343
|
-
when :maybe
|
|
344
|
-
constraints[name][:nullable] = true
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
constraints
|
|
350
|
-
rescue NoMethodError, TypeError
|
|
351
|
-
{}
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def extract_enum_from_core_values(core)
|
|
355
|
-
return unless core.respond_to?(:values)
|
|
356
|
-
|
|
357
|
-
vals = core.values
|
|
358
|
-
vals if vals.is_a?(Array)
|
|
359
|
-
rescue NoMethodError
|
|
360
|
-
nil
|
|
203
|
+
Constants::RUBY_TYPE_MAPPING[value] ||
|
|
204
|
+
Constants::RUBY_TYPE_MAPPING[value.class] ||
|
|
205
|
+
Constants::SchemaTypes::STRING
|
|
361
206
|
end
|
|
362
207
|
|
|
363
208
|
def convert_contract_schema_to_params(schema)
|
|
@@ -148,6 +148,8 @@ module GrapeOAS
|
|
|
148
148
|
schema.unevaluated_properties = doc[:unevaluated_properties] if doc.key?(:unevaluated_properties)
|
|
149
149
|
schema.format = doc[:format] if doc[:format]
|
|
150
150
|
schema.examples = doc[:example] if doc[:example]
|
|
151
|
+
nullable = SchemaEnhancer.extract_nullable(doc)
|
|
152
|
+
schema.nullable = (schema.nullable || nullable) if schema.respond_to?(:nullable=)
|
|
151
153
|
end
|
|
152
154
|
end
|
|
153
155
|
end
|
|
@@ -37,9 +37,13 @@ module GrapeOAS
|
|
|
37
37
|
return build_entity_schema(raw_type) if grape_entity?(raw_type)
|
|
38
38
|
return build_elements_array_schema(spec) if array_with_elements?(raw_type, spec)
|
|
39
39
|
return build_multi_type_schema(raw_type) if multi_type?(raw_type)
|
|
40
|
-
return build_typed_array_schema(raw_type) if typed_array?(raw_type)
|
|
41
40
|
return build_simple_array_schema if simple_array?(raw_type)
|
|
42
41
|
|
|
42
|
+
# Use TypeResolvers registry for arrays, Dry::Types, and primitives
|
|
43
|
+
# This resolves stringified types back to actual classes and extracts rich metadata
|
|
44
|
+
resolved_schema = GrapeOAS.type_resolvers.build_schema(raw_type)
|
|
45
|
+
return resolved_schema if resolved_schema
|
|
46
|
+
|
|
43
47
|
build_primitive_schema(raw_type, doc)
|
|
44
48
|
end
|
|
45
49
|
|
|
@@ -170,29 +174,11 @@ module GrapeOAS
|
|
|
170
174
|
!!resolve_entity_class(type)
|
|
171
175
|
end
|
|
172
176
|
|
|
173
|
-
# Checks if type is a Grape typed array notation like "[String]"
|
|
174
|
-
def typed_array?(type)
|
|
175
|
-
type.is_a?(String) && type.match?(TYPED_ARRAY_PATTERN)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
177
|
# Checks if type is a simple Array (class or string)
|
|
179
178
|
def simple_array?(type)
|
|
180
179
|
type == Array || type.to_s == "Array"
|
|
181
180
|
end
|
|
182
181
|
|
|
183
|
-
# Builds schema for Grape's typed array notation like "[String]", "[Integer]"
|
|
184
|
-
def build_typed_array_schema(type)
|
|
185
|
-
member_type = extract_typed_array_member(type)
|
|
186
|
-
items_type = resolve_schema_type(member_type)
|
|
187
|
-
ApiModel::Schema.new(
|
|
188
|
-
type: Constants::SchemaTypes::ARRAY,
|
|
189
|
-
items: ApiModel::Schema.new(
|
|
190
|
-
type: items_type,
|
|
191
|
-
format: Constants.format_for_type(member_type),
|
|
192
|
-
),
|
|
193
|
-
)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
182
|
def resolve_entity_class(type)
|
|
197
183
|
return nil unless defined?(Grape::Entity)
|
|
198
184
|
return type if type.is_a?(Class) && type <= Grape::Entity
|
|
@@ -11,7 +11,7 @@ module GrapeOAS
|
|
|
11
11
|
# @param spec [Hash] the parameter specification
|
|
12
12
|
# @param doc [Hash] the documentation hash
|
|
13
13
|
def self.apply(schema, spec, doc)
|
|
14
|
-
nullable = extract_nullable(
|
|
14
|
+
nullable = extract_nullable(doc)
|
|
15
15
|
|
|
16
16
|
schema.description ||= doc[:desc]
|
|
17
17
|
# Preserve existing nullable: true (e.g., from [Type, Nil] optimization)
|
|
@@ -23,13 +23,12 @@ module GrapeOAS
|
|
|
23
23
|
apply_values(schema, spec)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Extracts nullable flag from
|
|
26
|
+
# Extracts nullable flag from a documentation hash.
|
|
27
27
|
#
|
|
28
|
-
# @param spec [Hash] the parameter specification
|
|
29
28
|
# @param doc [Hash] the documentation hash
|
|
30
29
|
# @return [Boolean] true if nullable
|
|
31
|
-
def self.extract_nullable(
|
|
32
|
-
|
|
30
|
+
def self.extract_nullable(doc)
|
|
31
|
+
doc[:nullable] || (doc[:x].is_a?(Hash) && doc[:x][:nullable]) || false
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
class << self
|
|
@@ -82,8 +81,9 @@ module GrapeOAS
|
|
|
82
81
|
|
|
83
82
|
if values.is_a?(Range)
|
|
84
83
|
apply_range_values(schema, values)
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
else
|
|
85
|
+
enum_values = defined?(Set) && values.is_a?(Set) ? values.to_a : values
|
|
86
|
+
apply_enum_values(schema, enum_values) if enum_values.is_a?(Array) && enum_values.any?
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
data/lib/grape_oas/constants.rb
CHANGED
|
@@ -35,6 +35,18 @@ module GrapeOAS
|
|
|
35
35
|
ALL = [JSON, XML, FORM_URLENCODED, MULTIPART_FORM].freeze
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Nullable representation strategies for different OpenAPI versions.
|
|
39
|
+
# Passed via `nullable_strategy:` option to control how nullable fields
|
|
40
|
+
# are represented in the generated schema.
|
|
41
|
+
module NullableStrategy
|
|
42
|
+
# OAS 3.0: emits `"nullable": true` alongside the type
|
|
43
|
+
KEYWORD = :keyword
|
|
44
|
+
# OAS 3.1: emits `"type": ["string", "null"]` (JSON Schema style)
|
|
45
|
+
TYPE_ARRAY = :type_array
|
|
46
|
+
# OAS 2.0: emits `"x-nullable": true` extension
|
|
47
|
+
EXTENSION = :extension
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
# Default values for OpenAPI spec when not provided by user
|
|
39
51
|
module Defaults
|
|
40
52
|
LICENSE_NAME = "Proprietary"
|
|
@@ -49,13 +61,16 @@ module GrapeOAS
|
|
|
49
61
|
RUBY_TYPE_MAPPING = {
|
|
50
62
|
Integer => SchemaTypes::INTEGER,
|
|
51
63
|
Float => SchemaTypes::NUMBER,
|
|
52
|
-
BigDecimal => SchemaTypes::NUMBER,
|
|
53
64
|
TrueClass => SchemaTypes::BOOLEAN,
|
|
54
65
|
FalseClass => SchemaTypes::BOOLEAN,
|
|
55
66
|
Array => SchemaTypes::ARRAY,
|
|
56
67
|
Hash => SchemaTypes::OBJECT,
|
|
57
68
|
File => SchemaTypes::FILE
|
|
58
|
-
}.
|
|
69
|
+
}.tap do |mapping|
|
|
70
|
+
mapping.default_proc = lambda do |_hash, key|
|
|
71
|
+
key.is_a?(Class) && key.to_s == "BigDecimal" ? SchemaTypes::NUMBER : nil
|
|
72
|
+
end
|
|
73
|
+
end.freeze
|
|
59
74
|
|
|
60
75
|
# String type name to schema type and format mapping (lowercase).
|
|
61
76
|
# Supports lookup with any case via primitive_type helper.
|
|
@@ -10,11 +10,13 @@ module GrapeOAS
|
|
|
10
10
|
|
|
11
11
|
# OAS2-specific fields: consumes, produces, parameters (including body)
|
|
12
12
|
def build_version_specific_fields
|
|
13
|
+
strategy = @options[:nullable_strategy]
|
|
14
|
+
|
|
13
15
|
{
|
|
14
16
|
"consumes" => consumes,
|
|
15
17
|
"produces" => produces,
|
|
16
|
-
"parameters" => Parameter.new(@op, @ref_tracker).build,
|
|
17
|
-
"responses" => Response.new(@op.responses, @ref_tracker).build
|
|
18
|
+
"parameters" => Parameter.new(@op, @ref_tracker, nullable_strategy: strategy).build,
|
|
19
|
+
"responses" => Response.new(@op.responses, @ref_tracker, nullable_strategy: strategy).build
|
|
18
20
|
}
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -18,9 +18,10 @@ module GrapeOAS
|
|
|
18
18
|
"uuid" => { type: Constants::SchemaTypes::STRING, format: "uuid" }
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
-
def initialize(operation, ref_tracker = nil)
|
|
21
|
+
def initialize(operation, ref_tracker = nil, nullable_strategy: nil)
|
|
22
22
|
@op = operation
|
|
23
23
|
@ref_tracker = ref_tracker
|
|
24
|
+
@nullable_strategy = nullable_strategy
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def build
|
|
@@ -140,7 +141,7 @@ module GrapeOAS
|
|
|
140
141
|
ref_name = schema.canonical_name.gsub("::", "_")
|
|
141
142
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
142
143
|
else
|
|
143
|
-
Schema.new(schema, @ref_tracker).build
|
|
144
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
144
145
|
end
|
|
145
146
|
end
|
|
146
147
|
end
|
|
@@ -11,6 +11,7 @@ module GrapeOAS
|
|
|
11
11
|
# Build OAS2-specific operation
|
|
12
12
|
def build_operation(operation)
|
|
13
13
|
Operation.new(operation, @ref_tracker,
|
|
14
|
+
nullable_strategy: @options[:nullable_strategy],
|
|
14
15
|
suppress_default_error_response: @options[:suppress_default_error_response],).build
|
|
15
16
|
end
|
|
16
17
|
end
|
|
@@ -4,9 +4,10 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS2
|
|
6
6
|
class Response
|
|
7
|
-
def initialize(responses, ref_tracker = nil)
|
|
7
|
+
def initialize(responses, ref_tracker = nil, nullable_strategy: nil)
|
|
8
8
|
@responses = responses
|
|
9
9
|
@ref_tracker = ref_tracker
|
|
10
|
+
@nullable_strategy = nullable_strategy
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def build
|
|
@@ -36,7 +37,7 @@ module GrapeOAS
|
|
|
36
37
|
ref_name = schema.canonical_name.gsub("::", "_")
|
|
37
38
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
38
39
|
else
|
|
39
|
-
Schema.new(schema, @ref_tracker).build
|
|
40
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -4,9 +4,10 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS2
|
|
6
6
|
class Schema
|
|
7
|
-
def initialize(schema, ref_tracker = nil)
|
|
7
|
+
def initialize(schema, ref_tracker = nil, nullable_strategy: nil)
|
|
8
8
|
@schema = schema
|
|
9
9
|
@ref_tracker = ref_tracker
|
|
10
|
+
@nullable_strategy = nullable_strategy
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def build
|
|
@@ -57,11 +58,16 @@ module GrapeOAS
|
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def apply_extensions(schema_hash)
|
|
61
|
+
schema_hash["x-nullable"] = true if @nullable_strategy == Constants::NullableStrategy::EXTENSION && nullable?
|
|
60
62
|
schema_hash.merge!(@schema.extensions) if @schema.extensions
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
private
|
|
64
66
|
|
|
67
|
+
def nullable?
|
|
68
|
+
@schema.respond_to?(:nullable) && @schema.nullable
|
|
69
|
+
end
|
|
70
|
+
|
|
65
71
|
# Build schema from oneOf/anyOf by using first type (OAS2 doesn't support these)
|
|
66
72
|
# Extensions are merged to allow x-anyOf/x-oneOf for consumers that support them
|
|
67
73
|
def build_first_of_schema(composition_type)
|
|
@@ -100,9 +106,20 @@ module GrapeOAS
|
|
|
100
106
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
101
107
|
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
102
108
|
ref_name = schema.canonical_name.gsub("::", "_")
|
|
103
|
-
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
109
|
+
ref_hash = { "$ref" => "#/definitions/#{ref_name}" }
|
|
110
|
+
result = {}
|
|
111
|
+
if @nullable_strategy == Constants::NullableStrategy::EXTENSION && schema.respond_to?(:nullable) && schema.nullable
|
|
112
|
+
result["x-nullable"] = true
|
|
113
|
+
end
|
|
114
|
+
result["description"] = schema.description.to_s if schema.description
|
|
115
|
+
if result.empty?
|
|
116
|
+
ref_hash
|
|
117
|
+
else
|
|
118
|
+
result["allOf"] = [ref_hash]
|
|
119
|
+
result
|
|
120
|
+
end
|
|
104
121
|
else
|
|
105
|
-
Schema.new(schema, @ref_tracker).build
|
|
122
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
106
123
|
end
|
|
107
124
|
end
|
|
108
125
|
|
|
@@ -66,8 +66,13 @@ module GrapeOAS
|
|
|
66
66
|
media_types.empty? ? [Constants::MimeTypes::JSON] : media_types
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
def nullable_strategy
|
|
70
|
+
@api.nullable_strategy
|
|
71
|
+
end
|
|
72
|
+
|
|
69
73
|
def build_paths
|
|
70
74
|
OAS2::Paths.new(@api, @ref_tracker,
|
|
75
|
+
nullable_strategy: nullable_strategy,
|
|
71
76
|
suppress_default_error_response: @api.suppress_default_error_response,).build
|
|
72
77
|
end
|
|
73
78
|
|
|
@@ -82,7 +87,7 @@ module GrapeOAS
|
|
|
82
87
|
end
|
|
83
88
|
|
|
84
89
|
def build_schema(schema)
|
|
85
|
-
OAS2::Schema.new(schema, @ref_tracker).build
|
|
90
|
+
OAS2::Schema.new(schema, @ref_tracker, nullable_strategy: nullable_strategy).build
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
def build_definitions
|
|
@@ -103,7 +108,7 @@ module GrapeOAS
|
|
|
103
108
|
|
|
104
109
|
ref_name = canonical_name.gsub("::", "_")
|
|
105
110
|
schema = find_schema_by_canonical_name(canonical_name)
|
|
106
|
-
definitions[ref_name] = OAS2::Schema.new(schema, @ref_tracker).build if schema
|
|
111
|
+
definitions[ref_name] = OAS2::Schema.new(schema, @ref_tracker, nullable_strategy: nullable_strategy).build if schema
|
|
107
112
|
collect_refs(schema, pending) if schema
|
|
108
113
|
|
|
109
114
|
@ref_tracker.to_a.each do |cn|
|
|
@@ -10,12 +10,12 @@ module GrapeOAS
|
|
|
10
10
|
|
|
11
11
|
# OAS3-specific fields: parameters (no body), requestBody, responses
|
|
12
12
|
def build_version_specific_fields
|
|
13
|
-
|
|
13
|
+
strategy = @options[:nullable_strategy] || Constants::NullableStrategy::KEYWORD
|
|
14
14
|
|
|
15
15
|
{
|
|
16
|
-
"parameters" => Parameter.new(@op, @ref_tracker,
|
|
17
|
-
"requestBody" => RequestBody.new(@op.request_body, @ref_tracker,
|
|
18
|
-
"responses" => Response.new(@op.responses, @ref_tracker,
|
|
16
|
+
"parameters" => Parameter.new(@op, @ref_tracker, nullable_strategy: strategy).build,
|
|
17
|
+
"requestBody" => RequestBody.new(@op.request_body, @ref_tracker, nullable_strategy: strategy).build,
|
|
18
|
+
"responses" => Response.new(@op.responses, @ref_tracker, nullable_strategy: strategy).build
|
|
19
19
|
}
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -4,10 +4,10 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS3
|
|
6
6
|
class Parameter
|
|
7
|
-
def initialize(operation, ref_tracker = nil,
|
|
7
|
+
def initialize(operation, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD)
|
|
8
8
|
@op = operation
|
|
9
9
|
@ref_tracker = ref_tracker
|
|
10
|
-
@
|
|
10
|
+
@nullable_strategy = nullable_strategy
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def build
|
|
@@ -19,7 +19,7 @@ module GrapeOAS
|
|
|
19
19
|
"description" => param.description,
|
|
20
20
|
"style" => param.style,
|
|
21
21
|
"explode" => param.explode,
|
|
22
|
-
"schema" => Schema.new(param.schema, @ref_tracker,
|
|
22
|
+
"schema" => Schema.new(param.schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
23
23
|
}.compact
|
|
24
24
|
end.presence
|
|
25
25
|
end
|
|
@@ -8,11 +8,10 @@ module GrapeOAS
|
|
|
8
8
|
class Paths < Base::Paths
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
-
# Build OAS3-specific operation with
|
|
11
|
+
# Build OAS3-specific operation with nullable_strategy option
|
|
12
12
|
def build_operation(operation)
|
|
13
|
-
nullable_keyword = @options.key?(:nullable_keyword) ? @options[:nullable_keyword] : true
|
|
14
13
|
Operation.new(operation, @ref_tracker,
|
|
15
|
-
|
|
14
|
+
nullable_strategy: @options[:nullable_strategy] || Constants::NullableStrategy::KEYWORD,
|
|
16
15
|
suppress_default_error_response: @options[:suppress_default_error_response],).build
|
|
17
16
|
end
|
|
18
17
|
end
|
|
@@ -4,10 +4,10 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS3
|
|
6
6
|
class RequestBody
|
|
7
|
-
def initialize(request_body, ref_tracker = nil,
|
|
7
|
+
def initialize(request_body, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD)
|
|
8
8
|
@request_body = request_body
|
|
9
9
|
@ref_tracker = ref_tracker
|
|
10
|
-
@
|
|
10
|
+
@nullable_strategy = nullable_strategy
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def build
|
|
@@ -45,7 +45,7 @@ module GrapeOAS
|
|
|
45
45
|
ref_name = schema.canonical_name.gsub("::", "_")
|
|
46
46
|
{ "$ref" => "#/components/schemas/#{ref_name}" }
|
|
47
47
|
else
|
|
48
|
-
Schema.new(schema, @ref_tracker,
|
|
48
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -4,10 +4,10 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS3
|
|
6
6
|
class Response
|
|
7
|
-
def initialize(responses, ref_tracker = nil,
|
|
7
|
+
def initialize(responses, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD)
|
|
8
8
|
@responses = responses
|
|
9
9
|
@ref_tracker = ref_tracker
|
|
10
|
-
@
|
|
10
|
+
@nullable_strategy = nullable_strategy
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def build
|
|
@@ -76,7 +76,7 @@ module GrapeOAS
|
|
|
76
76
|
ref_name = schema.canonical_name.gsub("::", "_")
|
|
77
77
|
{ "$ref" => "#/components/schemas/#{ref_name}" }
|
|
78
78
|
else
|
|
79
|
-
Schema.new(schema, @ref_tracker,
|
|
79
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
end
|