grape-oas 1.4.0 → 1.5.1
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 +63 -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 +22 -29
- data/lib/grape_oas/api_model_builders/request_params.rb +121 -44
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +1 -2
- 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 +8 -27
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +4 -10
- data/lib/grape_oas/api_model_builders/response.rb +33 -5
- data/lib/grape_oas/api_model_builders/response_parsers/base.rb +11 -0
- 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 +82 -17
- data/lib/grape_oas/constants.rb +30 -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 +125 -22
- 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 +73 -23
- 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 +46 -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,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,9 +57,59 @@ 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|
|
|
60
|
+
specs = %i[http_codes failure success].flat_map do |key|
|
|
50
61
|
parse_value(data[key], route)
|
|
51
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
|
+
}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
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) }
|
|
52
113
|
end
|
|
53
114
|
|
|
54
115
|
def parse_value(value, route)
|
|
@@ -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)
|
|
@@ -122,17 +185,19 @@ module GrapeOAS
|
|
|
122
185
|
end
|
|
123
186
|
|
|
124
187
|
def normalize_entry(entry, route)
|
|
125
|
-
case entry
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
188
|
+
spec = case entry
|
|
189
|
+
when Hash
|
|
190
|
+
normalize_hash_entry(entry, route)
|
|
191
|
+
when Array
|
|
192
|
+
normalize_array_entry(entry, route)
|
|
193
|
+
when Class, Module
|
|
194
|
+
# Plain entity class (e.g., success TestEntity)
|
|
195
|
+
normalize_entity_entry(entry, route)
|
|
196
|
+
else
|
|
197
|
+
normalize_plain_entry(entry, route)
|
|
198
|
+
end
|
|
199
|
+
spec[:code] = normalize_status_code(spec[:code])
|
|
200
|
+
spec
|
|
136
201
|
end
|
|
137
202
|
|
|
138
203
|
def normalize_hash_entry(entry, route)
|
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,32 @@ 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/
|
|
73
|
+
end
|
|
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
|
|
61
87
|
end
|
|
62
88
|
|
|
63
89
|
# Default values for OpenAPI spec when not provided by user
|
|
@@ -95,7 +121,7 @@ module GrapeOAS
|
|
|
95
121
|
"float" => { type: SchemaTypes::NUMBER, format: "float" },
|
|
96
122
|
"bigdecimal" => { type: SchemaTypes::NUMBER, format: "double" },
|
|
97
123
|
"string" => { type: SchemaTypes::STRING },
|
|
98
|
-
"integer" => { type: SchemaTypes::INTEGER
|
|
124
|
+
"integer" => { type: SchemaTypes::INTEGER },
|
|
99
125
|
"number" => { type: SchemaTypes::NUMBER, format: "double" },
|
|
100
126
|
"boolean" => { type: SchemaTypes::BOOLEAN },
|
|
101
127
|
"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,24 +18,76 @@ 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
|
-
params =
|
|
29
|
-
|
|
31
|
+
params = representable_parameters.map { |param| build_parameter(param) }
|
|
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
|
|
|
33
42
|
private
|
|
34
43
|
|
|
44
|
+
# Swagger 2.0 restricts `in` to query|header|path|formData|body and
|
|
45
|
+
# non-body parameters to primitive types or arrays of primitives.
|
|
46
|
+
# Drop unsupported parameters rather than emit an invalid document,
|
|
47
|
+
# since ParamLocationResolver resolves per-parameter without knowing
|
|
48
|
+
# the target OAS version.
|
|
49
|
+
def representable_parameters
|
|
50
|
+
Array(@op.parameters).reject do |param|
|
|
51
|
+
if param.location == "cookie"
|
|
52
|
+
GrapeOAS.logger.warn("Dropping cookie parameter '#{param.name}': not representable in OAS 2.0")
|
|
53
|
+
true
|
|
54
|
+
elsif param.location != "body" &&
|
|
55
|
+
unrepresentable?(param.schema, location: param.location)
|
|
56
|
+
GrapeOAS.logger.warn(
|
|
57
|
+
"Dropping parameter '#{param.name}': schema is not representable as an " \
|
|
58
|
+
"OAS 2.0 #{param.location} parameter",
|
|
59
|
+
)
|
|
60
|
+
true
|
|
61
|
+
else
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def first_alternative_schema(schema)
|
|
68
|
+
return schema if schema.nil? || schema.type
|
|
69
|
+
return schema if schema.all_of&.any?
|
|
70
|
+
return first_alternative_schema(schema.one_of.first) if schema.one_of&.any?
|
|
71
|
+
return first_alternative_schema(schema.any_of.first) if schema.any_of&.any?
|
|
72
|
+
|
|
73
|
+
schema
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unrepresentable?(schema, location:, array_item: false)
|
|
77
|
+
schema = first_alternative_schema(schema)
|
|
78
|
+
return true unless schema&.type
|
|
79
|
+
return true if schema.all_of&.any?
|
|
80
|
+
return unrepresentable?(schema.items, location: location, array_item: true) if schema.type == Constants::SchemaTypes::ARRAY
|
|
81
|
+
return true if schema.type == Constants::SchemaTypes::OBJECT
|
|
82
|
+
|
|
83
|
+
schema.type == Constants::SchemaTypes::FILE && (location != "formData" || array_item)
|
|
84
|
+
end
|
|
85
|
+
|
|
35
86
|
def build_parameter(param)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
schema = param.location == "body" ? param.schema : first_alternative_schema(param.schema)
|
|
88
|
+
type = schema&.type
|
|
89
|
+
format = schema&.format
|
|
90
|
+
primitive_types = PRIMITIVE_MAPPINGS.keys + %w[string boolean file json array number]
|
|
39
91
|
is_primitive = type && primitive_types.include?(type)
|
|
40
92
|
|
|
41
93
|
if is_primitive && param.location != "body"
|
|
@@ -48,8 +100,8 @@ module GrapeOAS
|
|
|
48
100
|
"type" => mapping ? mapping[:type] : type,
|
|
49
101
|
"format" => format || (mapping ? mapping[:format] : nil)
|
|
50
102
|
}
|
|
51
|
-
apply_schema_constraints(result,
|
|
52
|
-
apply_collection_format(result, param,
|
|
103
|
+
apply_schema_constraints(result, schema)
|
|
104
|
+
apply_collection_format(result, param, schema)
|
|
53
105
|
result.compact
|
|
54
106
|
else
|
|
55
107
|
{
|
|
@@ -57,7 +109,7 @@ module GrapeOAS
|
|
|
57
109
|
"in" => param.location,
|
|
58
110
|
"required" => param.required,
|
|
59
111
|
"description" => param.description,
|
|
60
|
-
"schema" => build_schema_or_ref(
|
|
112
|
+
"schema" => build_schema_or_ref(schema)
|
|
61
113
|
}.tap do |h|
|
|
62
114
|
h["type"] = type if type
|
|
63
115
|
h["format"] = format if format
|
|
@@ -76,6 +128,7 @@ module GrapeOAS
|
|
|
76
128
|
result["maxLength"] = schema.max_length if schema.respond_to?(:max_length) && !schema.max_length.nil?
|
|
77
129
|
result["minItems"] = schema.min_items if schema.respond_to?(:min_items) && !schema.min_items.nil?
|
|
78
130
|
result["maxItems"] = schema.max_items if schema.respond_to?(:max_items) && !schema.max_items.nil?
|
|
131
|
+
result["uniqueItems"] = true if schema.respond_to?(:unique_items) && schema.unique_items
|
|
79
132
|
result["pattern"] = schema.pattern if schema.respond_to?(:pattern) && schema.pattern
|
|
80
133
|
result["enum"] = normalize_enum(schema.enum, result["type"]) if schema.respond_to?(:enum) && schema.enum
|
|
81
134
|
result["default"] = schema.default if schema.respond_to?(:default) && !schema.default.nil?
|
|
@@ -84,28 +137,77 @@ module GrapeOAS
|
|
|
84
137
|
def normalize_enum(enum_vals, type)
|
|
85
138
|
return nil unless enum_vals.is_a?(Array)
|
|
86
139
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
result = enum_vals.each_with_object([]) do |v, acc|
|
|
141
|
+
next if v.nil?
|
|
142
|
+
|
|
143
|
+
coerced_v = case type
|
|
144
|
+
when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
|
|
145
|
+
when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
|
|
146
|
+
else v
|
|
147
|
+
end
|
|
148
|
+
acc << coerced_v unless coerced_v.nil?
|
|
149
|
+
end
|
|
94
150
|
|
|
95
|
-
result
|
|
151
|
+
result.uniq!
|
|
96
152
|
return nil if result.empty?
|
|
97
153
|
|
|
98
154
|
result
|
|
99
155
|
end
|
|
100
156
|
|
|
101
|
-
def apply_collection_format(result, param,
|
|
102
|
-
return unless type == Constants::SchemaTypes::ARRAY
|
|
157
|
+
def apply_collection_format(result, param, schema)
|
|
158
|
+
return unless schema.type == Constants::SchemaTypes::ARRAY
|
|
159
|
+
|
|
160
|
+
result["items"] = build_schema_or_ref(schema.items) if schema.items
|
|
103
161
|
return unless param.collection_format
|
|
104
162
|
|
|
105
163
|
valid_formats = %w[csv ssv tsv pipes multi brackets]
|
|
106
164
|
result["collectionFormat"] = param.collection_format if valid_formats.include?(param.collection_format)
|
|
107
165
|
end
|
|
108
166
|
|
|
167
|
+
def form_only_request?
|
|
168
|
+
consumes = Array(@op.consumes)
|
|
169
|
+
consumes.any? && consumes.all? { |mime| FORM_MEDIA_TYPES.include?(mime) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_form_parameters(request_body)
|
|
173
|
+
schema = Array(request_body.media_types).first&.schema
|
|
174
|
+
return [] unless schema
|
|
175
|
+
|
|
176
|
+
unless [nil, "object"].include?(schema.type) && !composition?(schema)
|
|
177
|
+
raise ArgumentError, "OAS2 form bodies must have object properties; use OAS3 for this request schema"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
required = Array(schema.required).map(&:to_s)
|
|
181
|
+
schema.properties.map do |name, property_schema|
|
|
182
|
+
unless form_property?(property_schema)
|
|
183
|
+
raise ArgumentError, "OAS2 cannot represent form field #{name.inspect}; use OAS3 for complex form fields"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
build_parameter(
|
|
187
|
+
ApiModel::Parameter.new(
|
|
188
|
+
name: name.to_s,
|
|
189
|
+
location: "formData",
|
|
190
|
+
required: required.include?(name.to_s),
|
|
191
|
+
description: property_schema.description,
|
|
192
|
+
schema: property_schema,
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def form_property?(schema, array_item: false)
|
|
199
|
+
return false unless schema && !composition?(schema)
|
|
200
|
+
return false if array_item && schema.canonical_name
|
|
201
|
+
return form_property?(schema.items, array_item: true) if schema.type == "array"
|
|
202
|
+
|
|
203
|
+
types = array_item ? %w[string integer number boolean] : %w[string integer number boolean file]
|
|
204
|
+
types.include?(schema.type)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def composition?(schema)
|
|
208
|
+
schema.all_of&.any? || schema.one_of&.any? || schema.any_of&.any?
|
|
209
|
+
end
|
|
210
|
+
|
|
109
211
|
def build_body_parameter(request_body)
|
|
110
212
|
schema = build_body_schema(request_body)
|
|
111
213
|
name = derive_body_name(request_body)
|
|
@@ -139,10 +241,11 @@ module GrapeOAS
|
|
|
139
241
|
def build_schema_or_ref(schema)
|
|
140
242
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
141
243
|
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
142
|
-
ref_name = schema.canonical_name
|
|
244
|
+
ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
|
|
143
245
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
144
246
|
else
|
|
145
|
-
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy
|
|
247
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy,
|
|
248
|
+
composition_extensions: @composition_extensions,).build
|
|
146
249
|
end
|
|
147
250
|
end
|
|
148
251
|
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
|