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
|
@@ -4,12 +4,15 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS2
|
|
6
6
|
class Schema
|
|
7
|
-
def initialize(schema, ref_tracker = nil, nullable_strategy: nil)
|
|
7
|
+
def initialize(schema, ref_tracker = nil, nullable_strategy: nil, composition_extensions: false)
|
|
8
8
|
@schema = schema
|
|
9
9
|
@ref_tracker = ref_tracker
|
|
10
10
|
@nullable_strategy = nullable_strategy
|
|
11
|
+
@composition_extensions = composition_extensions
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
# OAS 2.0 (Swagger) natively supports `type: file`, so no
|
|
15
|
+
# file-type normalization is needed here (unlike OAS 3.x).
|
|
13
16
|
def build
|
|
14
17
|
return {} unless @schema
|
|
15
18
|
|
|
@@ -23,6 +26,7 @@ module GrapeOAS
|
|
|
23
26
|
|
|
24
27
|
schema_hash = build_base_hash
|
|
25
28
|
apply_constraints(schema_hash)
|
|
29
|
+
apply_compatibility_composition_extension(schema_hash)
|
|
26
30
|
apply_extensions(schema_hash)
|
|
27
31
|
schema_hash.compact
|
|
28
32
|
end
|
|
@@ -32,9 +36,9 @@ module GrapeOAS
|
|
|
32
36
|
"type" => @schema.type,
|
|
33
37
|
"format" => @schema.format,
|
|
34
38
|
"description" => @schema.description&.to_s,
|
|
35
|
-
"properties" => build_properties(@schema.properties)
|
|
36
|
-
"enum" => normalize_enum(@schema.enum, @schema.type)
|
|
39
|
+
"properties" => build_properties(@schema.properties)
|
|
37
40
|
}
|
|
41
|
+
schema_hash["enum"] = normalize_enum(@schema.enum, @schema.type, nullable: nullable?) if @schema.enum
|
|
38
42
|
if @schema.items
|
|
39
43
|
schema_hash["items"] = build_schema_or_ref(@schema.items, include_metadata: false)
|
|
40
44
|
if !schema_hash["description"] && @schema.items.respond_to?(:description) && @schema.items.description
|
|
@@ -66,6 +70,7 @@ module GrapeOAS
|
|
|
66
70
|
schema_hash["pattern"] = schema.pattern if schema.pattern
|
|
67
71
|
schema_hash["minItems"] = schema.min_items unless schema.min_items.nil?
|
|
68
72
|
schema_hash["maxItems"] = schema.max_items unless schema.max_items.nil?
|
|
73
|
+
schema_hash["uniqueItems"] = true if schema.unique_items
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
def apply_extensions(schema_hash)
|
|
@@ -75,24 +80,35 @@ module GrapeOAS
|
|
|
75
80
|
|
|
76
81
|
private
|
|
77
82
|
|
|
83
|
+
def apply_compatibility_composition_extension(result)
|
|
84
|
+
return unless @composition_extensions
|
|
85
|
+
|
|
86
|
+
{ "x-oneOf" => @schema.one_of, "x-anyOf" => @schema.any_of }.each do |key, alternatives|
|
|
87
|
+
next unless alternatives && alternatives.size > 1
|
|
88
|
+
next if @schema.extensions&.key?(key)
|
|
89
|
+
|
|
90
|
+
result[key] = alternatives.map { |item| build_schema_or_ref(item) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def schema_nullable?(schema)
|
|
95
|
+
schema.respond_to?(:nullable) && !!schema.nullable
|
|
96
|
+
end
|
|
97
|
+
|
|
78
98
|
def nullable?
|
|
79
|
-
|
|
99
|
+
schema_nullable?(@schema)
|
|
80
100
|
end
|
|
81
101
|
|
|
82
|
-
#
|
|
83
|
-
# Extensions are merged to allow x-anyOf/x-oneOf for consumers that support them
|
|
84
|
-
#
|
|
85
|
-
# Only description and extensions are applied from the composition node.
|
|
86
|
-
# Type-specific attributes (default, enum, format, constraints) are omitted
|
|
87
|
-
# because they describe the multi-type composition, not the single fallback
|
|
88
|
-
# branch selected here.
|
|
102
|
+
# OAS2 keeps a first-alternative fallback for tools that ignore extensions.
|
|
89
103
|
def build_first_of_schema(composition_type)
|
|
90
|
-
schemas = @schema.
|
|
104
|
+
schemas = composition_type == :any_of ? @schema.any_of : @schema.one_of
|
|
91
105
|
first_schema = schemas.first
|
|
92
106
|
return {} unless first_schema
|
|
93
107
|
|
|
94
108
|
result = build_schema_or_ref(first_schema)
|
|
95
109
|
result["description"] = @schema.description.to_s if @schema.description
|
|
110
|
+
|
|
111
|
+
apply_compatibility_composition_extension(result)
|
|
96
112
|
apply_extensions(result)
|
|
97
113
|
if result.key?("$ref") && result.size > 1
|
|
98
114
|
ref = { "$ref" => result.delete("$ref") }
|
|
@@ -114,7 +130,8 @@ module GrapeOAS
|
|
|
114
130
|
result["format"] = @schema.format if @schema.format
|
|
115
131
|
result["description"] = @schema.description.to_s if @schema.description
|
|
116
132
|
result["default"] = @schema.default unless @schema.default.nil?
|
|
117
|
-
result["enum"] = normalize_enum(@schema.enum, @schema.type) if @schema.enum
|
|
133
|
+
result["enum"] = normalize_enum(@schema.enum, @schema.type, nullable: nullable?) if @schema.enum
|
|
134
|
+
result.delete("enum") if result.key?("enum") && result["enum"].nil?
|
|
118
135
|
apply_constraints(result)
|
|
119
136
|
apply_extensions(result)
|
|
120
137
|
end
|
|
@@ -131,17 +148,16 @@ module GrapeOAS
|
|
|
131
148
|
def build_schema_or_ref(schema, include_metadata: true)
|
|
132
149
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
133
150
|
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
134
|
-
ref_name = schema.canonical_name
|
|
151
|
+
ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
|
|
135
152
|
ref_hash = { "$ref" => "#/definitions/#{ref_name}" }
|
|
136
153
|
return ref_hash unless include_metadata
|
|
137
154
|
|
|
138
155
|
result = {}
|
|
139
|
-
if @nullable_strategy == Constants::NullableStrategy::EXTENSION &&
|
|
140
|
-
result["x-nullable"] = true
|
|
141
|
-
end
|
|
156
|
+
result["x-nullable"] = true if @nullable_strategy == Constants::NullableStrategy::EXTENSION && schema_nullable?(schema)
|
|
142
157
|
result["description"] = schema.description.to_s if schema.description
|
|
143
158
|
result["default"] = schema.default unless schema.default.nil?
|
|
144
|
-
result["enum"] = normalize_enum(schema.enum, schema.type) if schema.enum
|
|
159
|
+
result["enum"] = normalize_enum(schema.enum, schema.type, nullable: schema_nullable?(schema)) if schema.enum
|
|
160
|
+
result.delete("enum") if result.key?("enum") && result["enum"].nil?
|
|
145
161
|
apply_constraints(result, schema)
|
|
146
162
|
result.merge!(schema.extensions) if schema.extensions
|
|
147
163
|
if result.empty?
|
|
@@ -151,24 +167,36 @@ module GrapeOAS
|
|
|
151
167
|
result
|
|
152
168
|
end
|
|
153
169
|
else
|
|
154
|
-
|
|
170
|
+
# self.class preserves any subclass so nested schemas use
|
|
171
|
+
# the version-correct builder.
|
|
172
|
+
built = self.class.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy,
|
|
173
|
+
composition_extensions: @composition_extensions,).build
|
|
155
174
|
built.delete("description") unless include_metadata
|
|
156
175
|
built
|
|
157
176
|
end
|
|
158
177
|
end
|
|
159
178
|
|
|
160
|
-
def normalize_enum(enum_vals, type)
|
|
179
|
+
def normalize_enum(enum_vals, type, nullable: false)
|
|
161
180
|
return nil unless enum_vals.is_a?(Array)
|
|
162
181
|
|
|
163
|
-
|
|
164
|
-
case type
|
|
165
|
-
when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
|
|
166
|
-
when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
|
|
167
|
-
else v
|
|
168
|
-
end
|
|
169
|
-
end.compact
|
|
182
|
+
has_nil = nullable && enum_vals.include?(nil)
|
|
170
183
|
|
|
171
|
-
|
|
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
|
|
172
200
|
end
|
|
173
201
|
end
|
|
174
202
|
end
|
|
@@ -67,18 +67,19 @@ module GrapeOAS
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def nullable_strategy
|
|
70
|
-
@api.nullable_strategy
|
|
70
|
+
@api.nullable_strategy || Constants::NullableStrategy::OAS2_DEFAULT
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def build_paths
|
|
74
74
|
OAS2::Paths.new(@api, @ref_tracker,
|
|
75
75
|
nullable_strategy: nullable_strategy,
|
|
76
|
+
composition_extensions: @api.oas2_composition_extensions,
|
|
76
77
|
suppress_default_error_response: @api.suppress_default_error_response,).build
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def build_schema_or_ref(schema)
|
|
80
81
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
81
|
-
ref_name = schema.canonical_name
|
|
82
|
+
ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
|
|
82
83
|
@ref_tracker << schema.canonical_name
|
|
83
84
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
84
85
|
else
|
|
@@ -87,7 +88,8 @@ module GrapeOAS
|
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
def build_schema(schema)
|
|
90
|
-
OAS2::Schema.new(schema, @ref_tracker, nullable_strategy: nullable_strategy
|
|
91
|
+
OAS2::Schema.new(schema, @ref_tracker, nullable_strategy: nullable_strategy,
|
|
92
|
+
composition_extensions: @api.oas2_composition_extensions,).build
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def build_definitions
|
|
@@ -106,9 +108,9 @@ module GrapeOAS
|
|
|
106
108
|
|
|
107
109
|
processed << canonical_name
|
|
108
110
|
|
|
109
|
-
ref_name =
|
|
111
|
+
ref_name = GrapeOAS.schema_ref_name.call(canonical_name)
|
|
110
112
|
schema = find_schema_by_canonical_name(canonical_name)
|
|
111
|
-
definitions[ref_name] =
|
|
113
|
+
definitions[ref_name] = build_schema(schema) if schema
|
|
112
114
|
collect_refs(schema, pending) if schema
|
|
113
115
|
|
|
114
116
|
@ref_tracker.to_a.each do |cn|
|
|
@@ -12,10 +12,12 @@ module GrapeOAS
|
|
|
12
12
|
def build_version_specific_fields
|
|
13
13
|
strategy = @options[:nullable_strategy] || Constants::NullableStrategy::KEYWORD
|
|
14
14
|
|
|
15
|
+
schema_options = { nullable_strategy: strategy, schema_builder: @options[:schema_builder] || Schema }
|
|
16
|
+
|
|
15
17
|
{
|
|
16
|
-
"parameters" => Parameter.new(@op, @ref_tracker,
|
|
17
|
-
"requestBody" => RequestBody.new(@op.request_body, @ref_tracker,
|
|
18
|
-
"responses" => Response.new(@op.responses, @ref_tracker,
|
|
18
|
+
"parameters" => Parameter.new(@op, @ref_tracker, **schema_options).build,
|
|
19
|
+
"requestBody" => RequestBody.new(@op.request_body, @ref_tracker, **schema_options).build,
|
|
20
|
+
"responses" => Response.new(@op.responses, @ref_tracker, **schema_options).build
|
|
19
21
|
}
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -4,15 +4,17 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS3
|
|
6
6
|
class Parameter
|
|
7
|
-
def initialize(operation, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD
|
|
7
|
+
def initialize(operation, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD,
|
|
8
|
+
schema_builder: Schema)
|
|
8
9
|
@op = operation
|
|
9
10
|
@ref_tracker = ref_tracker
|
|
10
11
|
@nullable_strategy = nullable_strategy
|
|
12
|
+
@schema_builder = schema_builder
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def build
|
|
14
16
|
Array(@op.parameters).map do |param|
|
|
15
|
-
schema_hash =
|
|
17
|
+
schema_hash = @schema_builder.new(param.schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
16
18
|
schema_description = schema_hash.delete("description")
|
|
17
19
|
description = param.description || schema_description
|
|
18
20
|
{
|
|
@@ -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] || Constants::NullableStrategy::KEYWORD,
|
|
15
|
+
schema_builder: @options[:schema_builder] || Schema,
|
|
15
16
|
suppress_default_error_response: @options[:suppress_default_error_response],).build
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -4,10 +4,12 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS3
|
|
6
6
|
class RequestBody
|
|
7
|
-
def initialize(request_body, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD
|
|
7
|
+
def initialize(request_body, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD,
|
|
8
|
+
schema_builder: Schema)
|
|
8
9
|
@request_body = request_body
|
|
9
10
|
@ref_tracker = ref_tracker
|
|
10
11
|
@nullable_strategy = nullable_strategy
|
|
12
|
+
@schema_builder = schema_builder
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def build
|
|
@@ -40,13 +42,8 @@ module GrapeOAS
|
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def build_schema_or_ref(schema)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
ref_name = schema.canonical_name.gsub("::", "_")
|
|
46
|
-
{ "$ref" => "#/components/schemas/#{ref_name}" }
|
|
47
|
-
else
|
|
48
|
-
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
49
|
-
end
|
|
45
|
+
@schema_builder.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy)
|
|
46
|
+
.build_schema_or_ref(schema)
|
|
50
47
|
end
|
|
51
48
|
end
|
|
52
49
|
end
|
|
@@ -4,10 +4,12 @@ module GrapeOAS
|
|
|
4
4
|
module Exporter
|
|
5
5
|
module OAS3
|
|
6
6
|
class Response
|
|
7
|
-
def initialize(responses, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD
|
|
7
|
+
def initialize(responses, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD,
|
|
8
|
+
schema_builder: Schema)
|
|
8
9
|
@responses = responses
|
|
9
10
|
@ref_tracker = ref_tracker
|
|
10
11
|
@nullable_strategy = nullable_strategy
|
|
12
|
+
@schema_builder = schema_builder
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def build
|
|
@@ -42,7 +44,7 @@ module GrapeOAS
|
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def build_content(media_types, response_examples = nil)
|
|
45
|
-
return nil
|
|
47
|
+
return nil if media_types.nil? || media_types.empty?
|
|
46
48
|
|
|
47
49
|
media_types.each_with_object({}) do |mt, h|
|
|
48
50
|
entry = { "schema" => build_schema_or_ref(mt.schema) }
|
|
@@ -71,13 +73,8 @@ module GrapeOAS
|
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
def build_schema_or_ref(schema)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
ref_name = schema.canonical_name.gsub("::", "_")
|
|
77
|
-
{ "$ref" => "#/components/schemas/#{ref_name}" }
|
|
78
|
-
else
|
|
79
|
-
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
80
|
-
end
|
|
76
|
+
@schema_builder.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy)
|
|
77
|
+
.build_schema_or_ref(schema)
|
|
81
78
|
end
|
|
82
79
|
end
|
|
83
80
|
end
|