grape-oas 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/UPGRADING.md +38 -0
- data/lib/grape_oas/api_model_builder.rb +5 -4
- data/lib/grape_oas/api_model_builders/request.rb +8 -13
- data/lib/grape_oas/api_model_builders/request_params.rb +38 -41
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +0 -1
- data/lib/grape_oas/api_model_builders/request_params_support/param_location_resolver.rb +33 -27
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +7 -26
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +1 -1
- data/lib/grape_oas/api_model_builders/response.rb +5 -1
- data/lib/grape_oas/api_model_builders/response_parsers/base.rb +17 -0
- data/lib/grape_oas/api_model_builders/response_parsers/default_response_parser.rb +4 -7
- data/lib/grape_oas/api_model_builders/response_parsers/documentation_responses_parser.rb +5 -2
- data/lib/grape_oas/api_model_builders/response_parsers/http_codes_parser.rb +96 -28
- data/lib/grape_oas/constants.rb +14 -0
- data/lib/grape_oas/exporter/concerns/enum_normalizer.rb +35 -0
- data/lib/grape_oas/exporter/concerns/schema_indexer.rb +18 -1
- data/lib/grape_oas/exporter/oas2/parameter.rb +55 -30
- data/lib/grape_oas/exporter/oas2/schema.rb +9 -26
- data/lib/grape_oas/exporter/oas3/schema.rb +26 -39
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +40 -6
- data/lib/grape_oas/type_resolvers/array_resolver.rb +7 -14
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +19 -0
- metadata +3 -2
|
@@ -3,20 +3,31 @@
|
|
|
3
3
|
module GrapeOAS
|
|
4
4
|
module ApiModelBuilders
|
|
5
5
|
module ResponseParsers
|
|
6
|
-
# Parser for responses defined via :http_codes, :failure,
|
|
7
|
-
#
|
|
6
|
+
# Parser for responses defined via :http_codes, :failure, :success, or
|
|
7
|
+
# :default / :default_response (the OAS "default" catch-all).
|
|
8
8
|
class HttpCodesParser
|
|
9
9
|
include Base
|
|
10
10
|
|
|
11
|
+
DEFAULT_RESPONSE_MESSAGE = "Default Response"
|
|
12
|
+
DEFAULT_RESPONSE_CODE = "default"
|
|
13
|
+
|
|
11
14
|
def applicable?(route)
|
|
12
15
|
options_applicable?(route) || desc_block?(route)
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def parse(route)
|
|
16
19
|
specs = parse_from_options(route)
|
|
17
|
-
|
|
20
|
+
specs = parse_from_desc(route) if specs.empty?
|
|
21
|
+
return specs if specs.any? { |spec| spec[:code].to_s == DEFAULT_RESPONSE_CODE }
|
|
22
|
+
|
|
23
|
+
specs + default_response_specs(route)
|
|
24
|
+
end
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
def default_response_specs(route)
|
|
27
|
+
data = route.options
|
|
28
|
+
value = default_response_value(data)
|
|
29
|
+
value ||= default_response_value(desc_data(route))
|
|
30
|
+
value ? parse_default_response(value, route) : []
|
|
20
31
|
end
|
|
21
32
|
|
|
22
33
|
private
|
|
@@ -46,15 +57,65 @@ module GrapeOAS
|
|
|
46
57
|
def parse_values(data, route)
|
|
47
58
|
return [] unless data.is_a?(Hash)
|
|
48
59
|
|
|
49
|
-
%i[http_codes failure success].flat_map do |key|
|
|
50
|
-
parse_value(data[key], route)
|
|
60
|
+
specs = %i[http_codes failure success].flat_map do |key|
|
|
61
|
+
parse_value(data[key], route, success: key == :success)
|
|
62
|
+
end
|
|
63
|
+
default_value = data[:default_response] || data[:default]
|
|
64
|
+
return specs unless default_value
|
|
65
|
+
|
|
66
|
+
specs.reject { |spec| spec[:code].to_s == DEFAULT_RESPONSE_CODE } +
|
|
67
|
+
parse_default_response(default_value, route)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Grape 3.x stores `desc { default ... }` as :default. Grape 4.0
|
|
71
|
+
# (ruby-grape/grape#2861) renamed that key to :default_response and
|
|
72
|
+
# remaps the deprecated `default` alias at write time. Always emit the
|
|
73
|
+
# OAS "default" status — grape-swagger ignores a numeric `code:` here.
|
|
74
|
+
def parse_default_response(value, route)
|
|
75
|
+
entries_for(value).map { |entry| normalize_default_response_entry(entry, route) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_default_response_entry(entry, route)
|
|
79
|
+
if entry.is_a?(Hash)
|
|
80
|
+
entry = normalize_hash_keys(entry)
|
|
81
|
+
{
|
|
82
|
+
code: DEFAULT_RESPONSE_CODE,
|
|
83
|
+
message: extract_description(entry) || DEFAULT_RESPONSE_MESSAGE,
|
|
84
|
+
entity: extract_entity(entry, nil),
|
|
85
|
+
headers: entry[:headers],
|
|
86
|
+
examples: entry[:examples],
|
|
87
|
+
as: entry[:as],
|
|
88
|
+
one_of: normalize_one_of(entry[:one_of]),
|
|
89
|
+
is_array: entry.key?(:is_array) ? entry[:is_array] : route.options[:is_array],
|
|
90
|
+
required: entry[:required]
|
|
91
|
+
}
|
|
92
|
+
else
|
|
93
|
+
{
|
|
94
|
+
code: DEFAULT_RESPONSE_CODE,
|
|
95
|
+
message: DEFAULT_RESPONSE_MESSAGE,
|
|
96
|
+
entity: entry,
|
|
97
|
+
headers: nil,
|
|
98
|
+
is_array: route.options[:is_array]
|
|
99
|
+
}
|
|
51
100
|
end
|
|
52
101
|
end
|
|
53
102
|
|
|
54
|
-
def
|
|
103
|
+
def default_response_value(data)
|
|
104
|
+
return unless data.is_a?(Hash)
|
|
105
|
+
|
|
106
|
+
data[:default_response] || data[:default]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def normalize_one_of(one_of)
|
|
110
|
+
return one_of unless one_of.is_a?(Array)
|
|
111
|
+
|
|
112
|
+
one_of.map { |entry| normalize_hash_keys(entry) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_value(value, route, success:)
|
|
55
116
|
return [] unless value
|
|
56
117
|
|
|
57
|
-
entries_for(value).map { |entry| normalize_entry(entry, route) }
|
|
118
|
+
entries_for(value).map { |entry| normalize_entry(entry, route, success: success) }
|
|
58
119
|
end
|
|
59
120
|
|
|
60
121
|
def entries_for(value)
|
|
@@ -73,12 +134,14 @@ module GrapeOAS
|
|
|
73
134
|
def options_applicable?(route)
|
|
74
135
|
entity_hash = route.options[:entity].is_a?(Hash) ? route.options[:entity] : nil
|
|
75
136
|
route.options[:http_codes] || route.options[:failure] || route.options[:success] ||
|
|
137
|
+
route.options[:default] || route.options[:default_response] ||
|
|
76
138
|
(entity_hash && (entity_hash[:code] || entity_hash[:model] || entity_hash[:entity] || entity_hash[:one_of]))
|
|
77
139
|
end
|
|
78
140
|
|
|
79
141
|
def desc_block?(route)
|
|
80
142
|
data = desc_data(route)
|
|
81
|
-
data && (data[:success] || data[:failure] || data[:http_codes] || data[:entity]
|
|
143
|
+
data && (data[:success] || data[:failure] || data[:http_codes] || data[:entity] ||
|
|
144
|
+
data[:default] || data[:default_response])
|
|
82
145
|
end
|
|
83
146
|
|
|
84
147
|
def desc_block_has_explicit_success?(route)
|
|
@@ -86,6 +149,9 @@ module GrapeOAS
|
|
|
86
149
|
end
|
|
87
150
|
|
|
88
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
|
+
|
|
89
155
|
entity_spec = build_entity_spec(entity_value, route)
|
|
90
156
|
return specs if specs.any? { |spec| spec[:code].to_i == entity_spec[:code].to_i }
|
|
91
157
|
|
|
@@ -96,7 +162,7 @@ module GrapeOAS
|
|
|
96
162
|
if entity_value.is_a?(Hash)
|
|
97
163
|
# Hash format: { code: 201, model: Entity, message: "Created" }
|
|
98
164
|
{
|
|
99
|
-
code: entity_value[:code] ||
|
|
165
|
+
code: entity_value[:code] || default_response_code(route, success: true),
|
|
100
166
|
message: entity_value[:message],
|
|
101
167
|
entity: extract_entity(entity_value, nil),
|
|
102
168
|
headers: entity_value[:headers],
|
|
@@ -109,7 +175,7 @@ module GrapeOAS
|
|
|
109
175
|
else
|
|
110
176
|
# Plain entity class
|
|
111
177
|
{
|
|
112
|
-
code:
|
|
178
|
+
code: default_response_code(route, success: true),
|
|
113
179
|
message: nil,
|
|
114
180
|
entity: entity_value,
|
|
115
181
|
headers: nil,
|
|
@@ -121,24 +187,26 @@ module GrapeOAS
|
|
|
121
187
|
end
|
|
122
188
|
end
|
|
123
189
|
|
|
124
|
-
def normalize_entry(entry, route)
|
|
125
|
-
case entry
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
190
|
+
def normalize_entry(entry, route, success:)
|
|
191
|
+
spec = case entry
|
|
192
|
+
when Hash
|
|
193
|
+
normalize_hash_entry(entry, route, success: success)
|
|
194
|
+
when Array
|
|
195
|
+
normalize_array_entry(entry, route)
|
|
196
|
+
when Class, Module
|
|
197
|
+
# Plain entity class (e.g., success TestEntity)
|
|
198
|
+
normalize_entity_entry(entry, route, success: success)
|
|
199
|
+
else
|
|
200
|
+
normalize_plain_entry(entry, route)
|
|
201
|
+
end
|
|
202
|
+
spec[:code] = normalize_status_code(spec[:code])
|
|
203
|
+
spec
|
|
136
204
|
end
|
|
137
205
|
|
|
138
|
-
def normalize_hash_entry(entry, route)
|
|
139
|
-
default_code = (route
|
|
206
|
+
def normalize_hash_entry(entry, route, success:)
|
|
207
|
+
default_code = default_response_code(route, success: success)
|
|
140
208
|
{
|
|
141
|
-
code: extract_status_code(entry, default_code),
|
|
209
|
+
code: extract_status_code(entry, default_code.to_s),
|
|
142
210
|
message: extract_description(entry),
|
|
143
211
|
entity: extract_entity(entry, route.options[:entity]),
|
|
144
212
|
headers: entry[:headers],
|
|
@@ -163,10 +231,10 @@ module GrapeOAS
|
|
|
163
231
|
}
|
|
164
232
|
end
|
|
165
233
|
|
|
166
|
-
def normalize_entity_entry(entity_class, route)
|
|
234
|
+
def normalize_entity_entry(entity_class, route, success:)
|
|
167
235
|
# Plain entity class (e.g., success TestEntity)
|
|
168
236
|
{
|
|
169
|
-
code: route
|
|
237
|
+
code: default_response_code(route, success: success),
|
|
170
238
|
message: nil,
|
|
171
239
|
entity: entity_class,
|
|
172
240
|
headers: nil,
|
data/lib/grape_oas/constants.rb
CHANGED
|
@@ -72,6 +72,20 @@ module GrapeOAS
|
|
|
72
72
|
VARIANT_COLLECTION = /\A(?<container>Array|Set)\[(?<inner>#{CONST_NAME}(?:,\s*#{CONST_NAME})+)\]\z/
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def self.nil_type?(type)
|
|
76
|
+
return true if type.nil?
|
|
77
|
+
|
|
78
|
+
name = type.to_s
|
|
79
|
+
name == "NilClass" || name == "Nil" || name.end_with?("::Nil")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.nullable_type(types)
|
|
83
|
+
return unless types.is_a?(Array) && types.size == 2
|
|
84
|
+
|
|
85
|
+
nil_types, other_types = types.partition { |type| nil_type?(type) }
|
|
86
|
+
other_types.first if nil_types.size == 1
|
|
87
|
+
end
|
|
88
|
+
|
|
75
89
|
# Default values for OpenAPI spec when not provided by user
|
|
76
90
|
module Defaults
|
|
77
91
|
LICENSE_NAME = "Proprietary"
|
|
@@ -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" },
|
|
@@ -28,7 +30,7 @@ module GrapeOAS
|
|
|
28
30
|
FORM_MEDIA_TYPES = %w[application/x-www-form-urlencoded multipart/form-data].freeze
|
|
29
31
|
|
|
30
32
|
def build
|
|
31
|
-
params =
|
|
33
|
+
params = representable_parameters.map { |param| build_parameter(param) }
|
|
32
34
|
if @op.request_body
|
|
33
35
|
if form_only_request?
|
|
34
36
|
params.concat(build_form_parameters(@op.request_body))
|
|
@@ -41,10 +43,53 @@ module GrapeOAS
|
|
|
41
43
|
|
|
42
44
|
private
|
|
43
45
|
|
|
46
|
+
# Swagger 2.0 restricts `in` to query|header|path|formData|body and
|
|
47
|
+
# non-body parameters to primitive types or arrays of primitives.
|
|
48
|
+
# Drop unsupported parameters rather than emit an invalid document,
|
|
49
|
+
# since ParamLocationResolver resolves per-parameter without knowing
|
|
50
|
+
# the target OAS version.
|
|
51
|
+
def representable_parameters
|
|
52
|
+
Array(@op.parameters).reject do |param|
|
|
53
|
+
if param.location == "cookie"
|
|
54
|
+
GrapeOAS.logger.warn("Dropping cookie parameter '#{param.name}': not representable in OAS 2.0")
|
|
55
|
+
true
|
|
56
|
+
elsif param.location != "body" &&
|
|
57
|
+
unrepresentable?(param.schema, location: param.location)
|
|
58
|
+
GrapeOAS.logger.warn(
|
|
59
|
+
"Dropping parameter '#{param.name}': schema is not representable as an " \
|
|
60
|
+
"OAS 2.0 #{param.location} parameter",
|
|
61
|
+
)
|
|
62
|
+
true
|
|
63
|
+
else
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def first_alternative_schema(schema)
|
|
70
|
+
return schema if schema.nil? || schema.type
|
|
71
|
+
return schema if schema.all_of&.any?
|
|
72
|
+
return first_alternative_schema(schema.one_of.first) if schema.one_of&.any?
|
|
73
|
+
return first_alternative_schema(schema.any_of.first) if schema.any_of&.any?
|
|
74
|
+
|
|
75
|
+
schema
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unrepresentable?(schema, location:, array_item: false)
|
|
79
|
+
schema = first_alternative_schema(schema)
|
|
80
|
+
return true unless schema&.type
|
|
81
|
+
return true if schema.all_of&.any?
|
|
82
|
+
return unrepresentable?(schema.items, location: location, array_item: true) if schema.type == Constants::SchemaTypes::ARRAY
|
|
83
|
+
return true if schema.type == Constants::SchemaTypes::OBJECT
|
|
84
|
+
|
|
85
|
+
schema.type == Constants::SchemaTypes::FILE && (location != "formData" || array_item)
|
|
86
|
+
end
|
|
87
|
+
|
|
44
88
|
def build_parameter(param)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
89
|
+
schema = param.location == "body" ? param.schema : first_alternative_schema(param.schema)
|
|
90
|
+
type = schema&.type
|
|
91
|
+
format = schema&.format
|
|
92
|
+
primitive_types = PRIMITIVE_MAPPINGS.keys + %w[string boolean file json array number]
|
|
48
93
|
is_primitive = type && primitive_types.include?(type)
|
|
49
94
|
|
|
50
95
|
if is_primitive && param.location != "body"
|
|
@@ -57,8 +102,8 @@ module GrapeOAS
|
|
|
57
102
|
"type" => mapping ? mapping[:type] : type,
|
|
58
103
|
"format" => format || (mapping ? mapping[:format] : nil)
|
|
59
104
|
}
|
|
60
|
-
apply_schema_constraints(result,
|
|
61
|
-
apply_collection_format(result, param,
|
|
105
|
+
apply_schema_constraints(result, schema)
|
|
106
|
+
apply_collection_format(result, param, schema)
|
|
62
107
|
result.compact
|
|
63
108
|
else
|
|
64
109
|
{
|
|
@@ -66,7 +111,7 @@ module GrapeOAS
|
|
|
66
111
|
"in" => param.location,
|
|
67
112
|
"required" => param.required,
|
|
68
113
|
"description" => param.description,
|
|
69
|
-
"schema" => build_schema_or_ref(
|
|
114
|
+
"schema" => build_schema_or_ref(schema)
|
|
70
115
|
}.tap do |h|
|
|
71
116
|
h["type"] = type if type
|
|
72
117
|
h["format"] = format if format
|
|
@@ -91,30 +136,10 @@ module GrapeOAS
|
|
|
91
136
|
result["default"] = schema.default if schema.respond_to?(:default) && !schema.default.nil?
|
|
92
137
|
end
|
|
93
138
|
|
|
94
|
-
def
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
result = enum_vals.each_with_object([]) do |v, acc|
|
|
98
|
-
next if v.nil?
|
|
99
|
-
|
|
100
|
-
coerced_v = case type
|
|
101
|
-
when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
|
|
102
|
-
when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
|
|
103
|
-
else v
|
|
104
|
-
end
|
|
105
|
-
acc << coerced_v unless coerced_v.nil?
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
result.uniq!
|
|
109
|
-
return nil if result.empty?
|
|
110
|
-
|
|
111
|
-
result
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def apply_collection_format(result, param, type)
|
|
115
|
-
return unless type == Constants::SchemaTypes::ARRAY
|
|
139
|
+
def apply_collection_format(result, param, schema)
|
|
140
|
+
return unless schema.type == Constants::SchemaTypes::ARRAY
|
|
116
141
|
|
|
117
|
-
result["items"] = build_schema_or_ref(
|
|
142
|
+
result["items"] = build_schema_or_ref(schema.items) if schema.items
|
|
118
143
|
return unless param.collection_format
|
|
119
144
|
|
|
120
145
|
valid_formats = %w[csv ssv tsv pipes multi brackets]
|
|
@@ -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)
|