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
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
module GrapeOAS
|
|
4
4
|
module ApiModelBuilders
|
|
5
5
|
class RequestParams
|
|
6
|
-
|
|
6
|
+
include Concerns::RouteValidations
|
|
7
|
+
|
|
8
|
+
ROUTE_PARAM_REGEX = /(?<=[:*])\w+/
|
|
9
|
+
|
|
10
|
+
def self.path_param_names(path)
|
|
11
|
+
path.sub(/(\(\.[^)]+\))+$/, "").scan(ROUTE_PARAM_REGEX)
|
|
12
|
+
end
|
|
7
13
|
|
|
8
14
|
attr_reader :api, :route, :path_param_name_map
|
|
9
15
|
|
|
@@ -14,8 +20,8 @@ module GrapeOAS
|
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
def build
|
|
17
|
-
route_params = route.path
|
|
18
|
-
all_params =
|
|
23
|
+
route_params = self.class.path_param_names(route.path)
|
|
24
|
+
all_params = declared_params
|
|
19
25
|
|
|
20
26
|
# Check if we have nested params (bracket notation)
|
|
21
27
|
has_nested = all_params.keys.any? { |k| k.include?("[") }
|
|
@@ -152,6 +158,80 @@ module GrapeOAS
|
|
|
152
158
|
spec.dig(:documentation, :collectionFormat) || spec.dig(:documentation, :collection_format)
|
|
153
159
|
end
|
|
154
160
|
|
|
161
|
+
# Grape 3.x stores documented params in `route.options[:params]`.
|
|
162
|
+
# Grape 4.0 moved them to `Route#params` (grape#2785) and no longer
|
|
163
|
+
# copies the hash into options. Path captures that are not Hash specs
|
|
164
|
+
# (empty-string defaults from the pattern) are dropped.
|
|
165
|
+
def declared_params
|
|
166
|
+
specs = params_from_options || params_from_route
|
|
167
|
+
return {} unless specs.is_a?(Hash)
|
|
168
|
+
|
|
169
|
+
flat = specs.select { |_name, spec| spec.is_a?(Hash) }
|
|
170
|
+
conditional = conditional_param_names
|
|
171
|
+
return flat if conditional.empty?
|
|
172
|
+
|
|
173
|
+
flat.each_with_object({}) do |(name, spec), params|
|
|
174
|
+
params[name] = conditional.include?(name.to_s) ? spec.merge(required: false) : spec
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns names of params that Grape will only validate conditionally
|
|
179
|
+
# (declared inside a `given` block). Their validators have a
|
|
180
|
+
# `params_scope` with `@dependent_on` set.
|
|
181
|
+
def conditional_param_names
|
|
182
|
+
return Set.new unless route.respond_to?(:app) && route.app.respond_to?(:inheritable_setting)
|
|
183
|
+
|
|
184
|
+
validations = grape_route_validations(route.app.inheritable_setting)
|
|
185
|
+
return Set.new unless validations.is_a?(Array)
|
|
186
|
+
|
|
187
|
+
conditional = Set.new
|
|
188
|
+
unconditional = Set.new
|
|
189
|
+
validations.each do |validator|
|
|
190
|
+
scope, attrs = validator_details(validator)
|
|
191
|
+
next unless scope.respond_to?(:full_name)
|
|
192
|
+
|
|
193
|
+
target = conditional_scope?(scope) ? conditional : unconditional
|
|
194
|
+
Array(attrs).each { |attr| target << scope.full_name(attr) }
|
|
195
|
+
end
|
|
196
|
+
conditional - unconditional
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Grape < 3.2 stores validators as hashes; >= 3.2 stores instances.
|
|
200
|
+
def validator_details(validator)
|
|
201
|
+
presence = Grape::Validations::Validators::PresenceValidator
|
|
202
|
+
if validator.is_a?(Hash)
|
|
203
|
+
return unless validator[:validator_class].is_a?(Class) && validator[:validator_class] <= presence
|
|
204
|
+
|
|
205
|
+
[validator[:params_scope], validator[:attributes]]
|
|
206
|
+
elsif validator.is_a?(presence)
|
|
207
|
+
[validator.instance_variable_get(:@scope), validator.attrs]
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def conditional_scope?(scope)
|
|
212
|
+
while scope
|
|
213
|
+
return true if scope.instance_variable_get(:@dependent_on)&.any?
|
|
214
|
+
|
|
215
|
+
scope = scope.respond_to?(:parent) ? scope.parent : nil
|
|
216
|
+
end
|
|
217
|
+
false
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def params_from_options
|
|
221
|
+
params = route.options[:params]
|
|
222
|
+
params if params.is_a?(Hash) && params.any?
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def params_from_route
|
|
226
|
+
return unless route.respond_to?(:params)
|
|
227
|
+
# Grape 3.x Route#params(input = nil) merges path captures; skip it.
|
|
228
|
+
# Grape 4.0 Route#params takes no arguments and is the documentation hash.
|
|
229
|
+
return unless route.method(:params).arity.zero?
|
|
230
|
+
|
|
231
|
+
params = route.params
|
|
232
|
+
params if params.is_a?(Hash) && params.any?
|
|
233
|
+
end
|
|
234
|
+
|
|
155
235
|
def location_resolver
|
|
156
236
|
RequestParamsSupport::ParamLocationResolver
|
|
157
237
|
end
|
|
@@ -147,7 +147,7 @@ module GrapeOAS
|
|
|
147
147
|
schema.additional_properties = doc[:additional_properties] if doc.key?(:additional_properties)
|
|
148
148
|
schema.unevaluated_properties = doc[:unevaluated_properties] if doc.key?(:unevaluated_properties)
|
|
149
149
|
schema.format = doc[:format] if doc[:format]
|
|
150
|
-
schema.examples = doc[:example] if doc
|
|
150
|
+
schema.examples = doc[:example] if doc.key?(:example)
|
|
151
151
|
nullable = SchemaEnhancer.extract_nullable(doc)
|
|
152
152
|
schema.nullable = (schema.nullable || nullable) if schema.respond_to?(:nullable=)
|
|
153
153
|
end
|
|
@@ -36,7 +36,7 @@ module GrapeOAS
|
|
|
36
36
|
|
|
37
37
|
# is_array: true on a typed array like "[String]" is redundant and would
|
|
38
38
|
# double-wrap it as Array<Array<...>> via build_primitive_array_schema.
|
|
39
|
-
return GrapeOAS.type_resolvers.build_schema(raw_type) if doc[:is_array] &&
|
|
39
|
+
return GrapeOAS.type_resolvers.build_schema(raw_type) if doc[:is_array] && TypeResolvers::ArrayResolver.handles?(raw_type)
|
|
40
40
|
return build_primitive_array_schema(doc_type, raw_type) if doc[:is_array]
|
|
41
41
|
return build_entity_schema(doc_type) if grape_entity?(doc_type)
|
|
42
42
|
return build_entity_schema(raw_type) if grape_entity?(raw_type)
|
|
@@ -24,15 +24,9 @@ module GrapeOAS
|
|
|
24
24
|
apply_default(schema, spec, doc)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Extracts nullable flag from a documentation hash.
|
|
28
|
-
#
|
|
29
|
-
# @param doc [Hash] the documentation hash
|
|
30
|
-
# @return [Boolean] true if nullable
|
|
31
|
-
def self.extract_nullable(doc)
|
|
32
|
-
doc[:nullable] || (doc[:x].is_a?(Hash) && doc[:x][:nullable]) || false
|
|
33
|
-
end
|
|
34
|
-
|
|
35
27
|
class << self
|
|
28
|
+
include GrapeOAS::ApiModelBuilders::Concerns::OasUtilities
|
|
29
|
+
|
|
36
30
|
private
|
|
37
31
|
|
|
38
32
|
def apply_additional_properties(schema, doc)
|
|
@@ -58,7 +52,7 @@ module GrapeOAS
|
|
|
58
52
|
|
|
59
53
|
def apply_format_and_example(schema, doc)
|
|
60
54
|
schema.format = doc[:format] if doc[:format] && schema.respond_to?(:format=)
|
|
61
|
-
schema.examples = doc[:example] if doc
|
|
55
|
+
schema.examples = doc[:example] if doc.key?(:example) && schema.respond_to?(:examples=)
|
|
62
56
|
end
|
|
63
57
|
|
|
64
58
|
def apply_values(schema, spec)
|
|
@@ -65,6 +65,9 @@ module GrapeOAS
|
|
|
65
65
|
# Else if any spec has `one_of:`, build a oneOf response from one_of entries and
|
|
66
66
|
# any regular specs in the group (this branch only runs when no `as:` entries exist)
|
|
67
67
|
def build_response_from_group(group_specs)
|
|
68
|
+
first_spec = group_specs.first
|
|
69
|
+
return build_response_without_content(first_spec) if bodyless_status?(first_spec[:code])
|
|
70
|
+
|
|
68
71
|
has_one_of = group_specs.any? { |s| s[:one_of] && !s[:one_of].empty? }
|
|
69
72
|
has_as = group_specs.any? { |s| !s[:as].nil? }
|
|
70
73
|
|
|
@@ -169,14 +172,16 @@ module GrapeOAS
|
|
|
169
172
|
|
|
170
173
|
# Merges examples from multiple specs
|
|
171
174
|
def merge_examples(specs)
|
|
172
|
-
examples = specs.
|
|
175
|
+
examples = specs.filter_map { |s| s[:examples] }
|
|
173
176
|
return nil if examples.empty?
|
|
174
177
|
|
|
175
178
|
examples.reduce({}, :merge)
|
|
176
179
|
end
|
|
177
180
|
|
|
178
181
|
def build_response_from_spec(spec)
|
|
182
|
+
code_str = spec[:code].to_s
|
|
179
183
|
entity_schema = build_schema(spec[:entity])
|
|
184
|
+
entity_schema = array_schema(entity_schema) if spec[:is_array]
|
|
180
185
|
schema = wrap_with_root(entity_schema, spec[:entity], is_array: spec[:is_array])
|
|
181
186
|
media_types = Array(response_content_types).map do |mime|
|
|
182
187
|
build_media_type(
|
|
@@ -189,7 +194,7 @@ module GrapeOAS
|
|
|
189
194
|
description = message.is_a?(String) ? message : message&.to_s
|
|
190
195
|
|
|
191
196
|
GrapeOAS::ApiModel::Response.new(
|
|
192
|
-
http_status:
|
|
197
|
+
http_status: code_str,
|
|
193
198
|
description: description || "Success",
|
|
194
199
|
media_types: media_types,
|
|
195
200
|
headers: normalize_headers(spec[:headers]) || headers_from_route,
|
|
@@ -198,6 +203,25 @@ module GrapeOAS
|
|
|
198
203
|
)
|
|
199
204
|
end
|
|
200
205
|
|
|
206
|
+
def build_response_without_content(spec)
|
|
207
|
+
message = spec[:message]
|
|
208
|
+
description = message.is_a?(String) ? message : message&.to_s
|
|
209
|
+
|
|
210
|
+
GrapeOAS::ApiModel::Response.new(
|
|
211
|
+
http_status: spec[:code].to_s,
|
|
212
|
+
description: description || "Success",
|
|
213
|
+
media_types: [],
|
|
214
|
+
headers: normalize_headers(spec[:headers]) || headers_from_route,
|
|
215
|
+
extensions: spec[:extensions] || extensions_from_route,
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# RFC 9110 §6.4.1 and §15.3.6: these responses never carry a message body.
|
|
220
|
+
def bodyless_status?(code)
|
|
221
|
+
status = code.to_s.to_i
|
|
222
|
+
status.between?(100, 199) || status == 204 || status == 205 || status == 304
|
|
223
|
+
end
|
|
224
|
+
|
|
201
225
|
def array_schema(schema)
|
|
202
226
|
GrapeOAS::ApiModel::Schema.new(
|
|
203
227
|
type: Constants::SchemaTypes::ARRAY,
|
|
@@ -238,7 +262,7 @@ module GrapeOAS
|
|
|
238
262
|
# Build schema for response body
|
|
239
263
|
# Delegates to EntityIntrospector when entity is present
|
|
240
264
|
def build_schema(entity_class)
|
|
241
|
-
return GrapeOAS::ApiModel::Schema.new
|
|
265
|
+
return GrapeOAS::ApiModel::Schema.new unless entity_class
|
|
242
266
|
|
|
243
267
|
GrapeOAS.introspectors.build_schema(entity_class, stack: [], registry: {})
|
|
244
268
|
end
|
|
@@ -291,7 +315,7 @@ module GrapeOAS
|
|
|
291
315
|
end
|
|
292
316
|
|
|
293
317
|
def response_content_types
|
|
294
|
-
resolve_content_types
|
|
318
|
+
explicit_media_types(:produces) || resolve_content_types
|
|
295
319
|
end
|
|
296
320
|
end
|
|
297
321
|
end
|
data/lib/grape_oas/constants.rb
CHANGED
|
@@ -12,6 +12,7 @@ module GrapeOAS
|
|
|
12
12
|
OBJECT = "object"
|
|
13
13
|
ARRAY = "array"
|
|
14
14
|
FILE = "file"
|
|
15
|
+
NULL = "null"
|
|
15
16
|
|
|
16
17
|
ALL = [STRING, INTEGER, NUMBER, BOOLEAN, OBJECT, ARRAY, FILE].freeze
|
|
17
18
|
end
|
|
@@ -45,6 +46,11 @@ module GrapeOAS
|
|
|
45
46
|
TYPE_ARRAY = :type_array
|
|
46
47
|
# OAS 2.0: emits `"x-nullable": true` extension
|
|
47
48
|
EXTENSION = :extension
|
|
49
|
+
|
|
50
|
+
# Version-specific defaults used by exporters
|
|
51
|
+
OAS2_DEFAULT = EXTENSION
|
|
52
|
+
OAS3_DEFAULT = KEYWORD
|
|
53
|
+
OAS31_DEFAULT = TYPE_ARRAY
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
# Maximum number of elements to expand from a non-numeric Range into an enum array.
|
|
@@ -52,12 +58,18 @@ module GrapeOAS
|
|
|
52
58
|
MAX_ENUM_RANGE_SIZE = 100
|
|
53
59
|
|
|
54
60
|
# Regex patterns for Grape's stringified type notations.
|
|
55
|
-
# Grape
|
|
56
|
-
# `type: [
|
|
61
|
+
# Grape's ParamsDocumentation TypeCache stores `coerce_type.to_s`:
|
|
62
|
+
# - `type: [SomeClass]` / `type: Array[SomeClass]` → "[SomeClass]" (Array#to_s)
|
|
63
|
+
# - `types: [String, Integer]` → "[String, Integer]" (Array#to_s of the types list)
|
|
64
|
+
# - Grape 3.3+ `type: Array[Integer, String]` → "Array[Integer, String]"
|
|
65
|
+
# (VariantCollectionCoercer#to_s, grape#2758). Distinct from the `types:`
|
|
66
|
+
# form so a collection of variant members is not confused with a scalar
|
|
67
|
+
# that accepts multiple types. `Set[...]` is the same for Set containers.
|
|
57
68
|
module TypePatterns
|
|
58
69
|
CONST_NAME = /(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*/
|
|
59
|
-
TYPED_ARRAY = /\A
|
|
70
|
+
TYPED_ARRAY = /\A(?:(?<container>Array|Set))?\[(?<inner>#{CONST_NAME})\]\z/
|
|
60
71
|
MULTI_TYPE = /\A\[(#{CONST_NAME}(?:,\s*#{CONST_NAME})+)\]\z/
|
|
72
|
+
VARIANT_COLLECTION = /\A(?<container>Array|Set)\[(?<inner>#{CONST_NAME}(?:,\s*#{CONST_NAME})+)\]\z/
|
|
61
73
|
end
|
|
62
74
|
|
|
63
75
|
# Default values for OpenAPI spec when not provided by user
|
|
@@ -95,7 +107,7 @@ module GrapeOAS
|
|
|
95
107
|
"float" => { type: SchemaTypes::NUMBER, format: "float" },
|
|
96
108
|
"bigdecimal" => { type: SchemaTypes::NUMBER, format: "double" },
|
|
97
109
|
"string" => { type: SchemaTypes::STRING },
|
|
98
|
-
"integer" => { type: SchemaTypes::INTEGER
|
|
110
|
+
"integer" => { type: SchemaTypes::INTEGER },
|
|
99
111
|
"number" => { type: SchemaTypes::NUMBER, format: "double" },
|
|
100
112
|
"boolean" => { type: SchemaTypes::BOOLEAN },
|
|
101
113
|
"grape::api::boolean" => { type: SchemaTypes::BOOLEAN },
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
module GrapeOAS
|
|
4
4
|
module DocumentationExtension
|
|
5
5
|
# Primary entry for grape-oas documentation
|
|
6
|
-
def add_oas_documentation(**options)
|
|
7
|
-
return if options.delete(:hide_documentation_path)
|
|
8
|
-
|
|
6
|
+
def add_oas_documentation(hide_documentation_path: true, **options)
|
|
9
7
|
# Prefer grape-oas namespaced options to avoid clashing with grape-swagger
|
|
10
8
|
default_mount_path = options.delete(:oas_mount_path) ||
|
|
11
9
|
options.delete(:mount_path) ||
|
|
@@ -25,13 +23,13 @@ module GrapeOAS
|
|
|
25
23
|
api = self
|
|
26
24
|
|
|
27
25
|
mount_paths.each do |key, path|
|
|
28
|
-
add_documentation_routes(path, key, default_format, cache_control, etag_value, options, api)
|
|
26
|
+
add_documentation_routes(path, key, default_format, cache_control, etag_value, options, api, hidden: hide_documentation_path)
|
|
29
27
|
end
|
|
30
28
|
end
|
|
31
29
|
|
|
32
|
-
def add_documentation_routes(path, key, default_format, cache_control, etag_value, options, api)
|
|
30
|
+
def add_documentation_routes(path, key, default_format, cache_control, etag_value, options, api, hidden: false)
|
|
33
31
|
# Main route without namespace filter
|
|
34
|
-
add_route(path) do
|
|
32
|
+
add_route(path, hidden: hidden) do
|
|
35
33
|
GrapeOAS::DocumentationExtension.generate_documentation(
|
|
36
34
|
self, api, key, default_format, cache_control, etag_value, options, nil,
|
|
37
35
|
)
|
|
@@ -39,7 +37,7 @@ module GrapeOAS
|
|
|
39
37
|
|
|
40
38
|
# Route with namespace filter: /swagger_doc/:namespace
|
|
41
39
|
# Supports nested namespaces via *namespace (catches slashes)
|
|
42
|
-
add_route("#{path}/*namespace") do
|
|
40
|
+
add_route("#{path}/*namespace", hidden: hidden) do
|
|
43
41
|
namespace_filter = params[:namespace]&.sub(/\.json$/, "")
|
|
44
42
|
GrapeOAS::DocumentationExtension.generate_documentation(
|
|
45
43
|
self, api, key, default_format, cache_control, etag_value, options, namespace_filter,
|
|
@@ -89,10 +87,10 @@ module GrapeOAS
|
|
|
89
87
|
private
|
|
90
88
|
|
|
91
89
|
# Minimal route mounting helper
|
|
92
|
-
def add_route(path, &block)
|
|
90
|
+
def add_route(path, hidden: false, &block)
|
|
93
91
|
api_class = self
|
|
94
92
|
namespace do
|
|
95
|
-
get(path) { instance_exec(api_class, &block) }
|
|
93
|
+
get(path, hidden: hidden) { instance_exec(api_class, &block) }
|
|
96
94
|
end
|
|
97
95
|
end
|
|
98
96
|
|
|
@@ -47,10 +47,21 @@ module GrapeOAS
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def index_schema(schema, index)
|
|
51
|
-
return unless schema
|
|
50
|
+
def index_schema(schema, index, seen = Set.new)
|
|
51
|
+
return unless schema
|
|
52
|
+
|
|
53
|
+
schema_id = schema.object_id
|
|
54
|
+
return if seen.include?(schema_id)
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
seen << schema_id
|
|
57
|
+
index[schema.canonical_name] ||= schema if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
58
|
+
index_schema(schema.items, index, seen) if schema.respond_to?(:items) && schema.items
|
|
59
|
+
schema.properties.each_value { |child| index_schema(child, index, seen) } if schema.respond_to?(:properties) && schema.properties
|
|
60
|
+
%i[all_of one_of any_of].each do |composition|
|
|
61
|
+
next unless schema.respond_to?(composition)
|
|
62
|
+
|
|
63
|
+
Array(schema.public_send(composition)).each { |child| index_schema(child, index, seen) }
|
|
64
|
+
end
|
|
54
65
|
end
|
|
55
66
|
|
|
56
67
|
def collect_refs(schema, pending, seen = Set.new)
|
|
@@ -15,8 +15,10 @@ module GrapeOAS
|
|
|
15
15
|
{
|
|
16
16
|
"consumes" => consumes,
|
|
17
17
|
"produces" => produces,
|
|
18
|
-
"parameters" => Parameter.new(@op, @ref_tracker, nullable_strategy: strategy
|
|
19
|
-
|
|
18
|
+
"parameters" => Parameter.new(@op, @ref_tracker, nullable_strategy: strategy,
|
|
19
|
+
composition_extensions: @options[:composition_extensions],).build,
|
|
20
|
+
"responses" => Response.new(@op.responses, @ref_tracker, nullable_strategy: strategy,
|
|
21
|
+
composition_extensions: @options[:composition_extensions],).build
|
|
20
22
|
}
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -5,7 +5,7 @@ module GrapeOAS
|
|
|
5
5
|
module OAS2
|
|
6
6
|
class Parameter
|
|
7
7
|
PRIMITIVE_MAPPINGS = {
|
|
8
|
-
Constants::SchemaTypes::INTEGER => { type: Constants::SchemaTypes::INTEGER
|
|
8
|
+
Constants::SchemaTypes::INTEGER => { type: Constants::SchemaTypes::INTEGER },
|
|
9
9
|
"long" => { type: Constants::SchemaTypes::INTEGER, format: "int64" },
|
|
10
10
|
"float" => { type: Constants::SchemaTypes::NUMBER, format: "float" },
|
|
11
11
|
"double" => { type: Constants::SchemaTypes::NUMBER, format: "double" },
|
|
@@ -18,15 +18,24 @@ module GrapeOAS
|
|
|
18
18
|
"uuid" => { type: Constants::SchemaTypes::STRING, format: "uuid" }
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
-
def initialize(operation, ref_tracker = nil, nullable_strategy: nil)
|
|
21
|
+
def initialize(operation, ref_tracker = nil, nullable_strategy: nil, composition_extensions: false)
|
|
22
22
|
@op = operation
|
|
23
23
|
@ref_tracker = ref_tracker
|
|
24
24
|
@nullable_strategy = nullable_strategy
|
|
25
|
+
@composition_extensions = composition_extensions
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
FORM_MEDIA_TYPES = %w[application/x-www-form-urlencoded multipart/form-data].freeze
|
|
29
|
+
|
|
27
30
|
def build
|
|
28
31
|
params = Array(@op.parameters).map { |param| build_parameter(param) }
|
|
29
|
-
|
|
32
|
+
if @op.request_body
|
|
33
|
+
if form_only_request?
|
|
34
|
+
params.concat(build_form_parameters(@op.request_body))
|
|
35
|
+
else
|
|
36
|
+
params << build_body_parameter(@op.request_body)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
30
39
|
params
|
|
31
40
|
end
|
|
32
41
|
|
|
@@ -76,6 +85,7 @@ module GrapeOAS
|
|
|
76
85
|
result["maxLength"] = schema.max_length if schema.respond_to?(:max_length) && !schema.max_length.nil?
|
|
77
86
|
result["minItems"] = schema.min_items if schema.respond_to?(:min_items) && !schema.min_items.nil?
|
|
78
87
|
result["maxItems"] = schema.max_items if schema.respond_to?(:max_items) && !schema.max_items.nil?
|
|
88
|
+
result["uniqueItems"] = true if schema.respond_to?(:unique_items) && schema.unique_items
|
|
79
89
|
result["pattern"] = schema.pattern if schema.respond_to?(:pattern) && schema.pattern
|
|
80
90
|
result["enum"] = normalize_enum(schema.enum, result["type"]) if schema.respond_to?(:enum) && schema.enum
|
|
81
91
|
result["default"] = schema.default if schema.respond_to?(:default) && !schema.default.nil?
|
|
@@ -84,15 +94,18 @@ module GrapeOAS
|
|
|
84
94
|
def normalize_enum(enum_vals, type)
|
|
85
95
|
return nil unless enum_vals.is_a?(Array)
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
94
107
|
|
|
95
|
-
result
|
|
108
|
+
result.uniq!
|
|
96
109
|
return nil if result.empty?
|
|
97
110
|
|
|
98
111
|
result
|
|
@@ -100,12 +113,58 @@ module GrapeOAS
|
|
|
100
113
|
|
|
101
114
|
def apply_collection_format(result, param, type)
|
|
102
115
|
return unless type == Constants::SchemaTypes::ARRAY
|
|
116
|
+
|
|
117
|
+
result["items"] = build_schema_or_ref(param.schema.items) if param.schema.items
|
|
103
118
|
return unless param.collection_format
|
|
104
119
|
|
|
105
120
|
valid_formats = %w[csv ssv tsv pipes multi brackets]
|
|
106
121
|
result["collectionFormat"] = param.collection_format if valid_formats.include?(param.collection_format)
|
|
107
122
|
end
|
|
108
123
|
|
|
124
|
+
def form_only_request?
|
|
125
|
+
consumes = Array(@op.consumes)
|
|
126
|
+
consumes.any? && consumes.all? { |mime| FORM_MEDIA_TYPES.include?(mime) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_form_parameters(request_body)
|
|
130
|
+
schema = Array(request_body.media_types).first&.schema
|
|
131
|
+
return [] unless schema
|
|
132
|
+
|
|
133
|
+
unless [nil, "object"].include?(schema.type) && !composition?(schema)
|
|
134
|
+
raise ArgumentError, "OAS2 form bodies must have object properties; use OAS3 for this request schema"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
required = Array(schema.required).map(&:to_s)
|
|
138
|
+
schema.properties.map do |name, property_schema|
|
|
139
|
+
unless form_property?(property_schema)
|
|
140
|
+
raise ArgumentError, "OAS2 cannot represent form field #{name.inspect}; use OAS3 for complex form fields"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
build_parameter(
|
|
144
|
+
ApiModel::Parameter.new(
|
|
145
|
+
name: name.to_s,
|
|
146
|
+
location: "formData",
|
|
147
|
+
required: required.include?(name.to_s),
|
|
148
|
+
description: property_schema.description,
|
|
149
|
+
schema: property_schema,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def form_property?(schema, array_item: false)
|
|
156
|
+
return false unless schema && !composition?(schema)
|
|
157
|
+
return false if array_item && schema.canonical_name
|
|
158
|
+
return form_property?(schema.items, array_item: true) if schema.type == "array"
|
|
159
|
+
|
|
160
|
+
types = array_item ? %w[string integer number boolean] : %w[string integer number boolean file]
|
|
161
|
+
types.include?(schema.type)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def composition?(schema)
|
|
165
|
+
schema.all_of&.any? || schema.one_of&.any? || schema.any_of&.any?
|
|
166
|
+
end
|
|
167
|
+
|
|
109
168
|
def build_body_parameter(request_body)
|
|
110
169
|
schema = build_body_schema(request_body)
|
|
111
170
|
name = derive_body_name(request_body)
|
|
@@ -139,10 +198,11 @@ module GrapeOAS
|
|
|
139
198
|
def build_schema_or_ref(schema)
|
|
140
199
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
141
200
|
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
142
|
-
ref_name = schema.canonical_name
|
|
201
|
+
ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
|
|
143
202
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
144
203
|
else
|
|
145
|
-
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy
|
|
204
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy,
|
|
205
|
+
composition_extensions: @composition_extensions,).build
|
|
146
206
|
end
|
|
147
207
|
end
|
|
148
208
|
end
|
|
@@ -12,6 +12,7 @@ module GrapeOAS
|
|
|
12
12
|
def build_operation(operation)
|
|
13
13
|
Operation.new(operation, @ref_tracker,
|
|
14
14
|
nullable_strategy: @options[:nullable_strategy],
|
|
15
|
+
composition_extensions: @options[:composition_extensions],
|
|
15
16
|
suppress_default_error_response: @options[:suppress_default_error_response],).build
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -4,10 +4,11 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS2
|
|
6
6
|
class Response
|
|
7
|
-
def initialize(responses, ref_tracker = nil, nullable_strategy: nil)
|
|
7
|
+
def initialize(responses, ref_tracker = nil, nullable_strategy: nil, composition_extensions: false)
|
|
8
8
|
@responses = responses
|
|
9
9
|
@ref_tracker = ref_tracker
|
|
10
10
|
@nullable_strategy = nullable_strategy
|
|
11
|
+
@composition_extensions = composition_extensions
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def build
|
|
@@ -34,10 +35,11 @@ module GrapeOAS
|
|
|
34
35
|
def build_schema_or_ref(schema)
|
|
35
36
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
36
37
|
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
37
|
-
ref_name = schema.canonical_name
|
|
38
|
+
ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
|
|
38
39
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
39
40
|
else
|
|
40
|
-
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy
|
|
41
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy,
|
|
42
|
+
composition_extensions: @composition_extensions,).build
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
|
|
@@ -60,7 +62,7 @@ module GrapeOAS
|
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def build_examples(media_types, response_examples = nil)
|
|
63
|
-
return nil
|
|
65
|
+
return nil if media_types.nil? || media_types.empty?
|
|
64
66
|
|
|
65
67
|
mt = Array(media_types).first
|
|
66
68
|
# Media type examples take precedence over response-level examples
|