openapi_parser_firetail 1.0.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 +7 -0
- data/.github/workflows/ci.yaml +25 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_ignore.yml +6 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +132 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +112 -0
- data/Rakefile +10 -0
- data/Steepfile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/openapi_parser/concern.rb +5 -0
- data/lib/openapi_parser/concerns/expandable.rb +87 -0
- data/lib/openapi_parser/concerns/findable.rb +54 -0
- data/lib/openapi_parser/concerns/media_type_selectable.rb +29 -0
- data/lib/openapi_parser/concerns/parameter_validatable.rb +62 -0
- data/lib/openapi_parser/concerns/parser/core.rb +21 -0
- data/lib/openapi_parser/concerns/parser/hash.rb +10 -0
- data/lib/openapi_parser/concerns/parser/hash_body.rb +12 -0
- data/lib/openapi_parser/concerns/parser/list.rb +10 -0
- data/lib/openapi_parser/concerns/parser/object.rb +14 -0
- data/lib/openapi_parser/concerns/parser/value.rb +14 -0
- data/lib/openapi_parser/concerns/parser.rb +45 -0
- data/lib/openapi_parser/concerns/schema_loader/base.rb +28 -0
- data/lib/openapi_parser/concerns/schema_loader/creator.rb +48 -0
- data/lib/openapi_parser/concerns/schema_loader/hash_body_loader.rb +37 -0
- data/lib/openapi_parser/concerns/schema_loader/hash_objects_loader.rb +29 -0
- data/lib/openapi_parser/concerns/schema_loader/list_loader.rb +28 -0
- data/lib/openapi_parser/concerns/schema_loader/objects_loader.rb +21 -0
- data/lib/openapi_parser/concerns/schema_loader/values_loader.rb +10 -0
- data/lib/openapi_parser/concerns/schema_loader.rb +58 -0
- data/lib/openapi_parser/config.rb +55 -0
- data/lib/openapi_parser/errors.rb +281 -0
- data/lib/openapi_parser/parameter_validator.rb +33 -0
- data/lib/openapi_parser/path_item_finder.rb +161 -0
- data/lib/openapi_parser/reference_expander.rb +9 -0
- data/lib/openapi_parser/request_operation.rb +90 -0
- data/lib/openapi_parser/schema_validator/all_of_validator.rb +40 -0
- data/lib/openapi_parser/schema_validator/any_of_validator.rb +18 -0
- data/lib/openapi_parser/schema_validator/array_validator.rb +32 -0
- data/lib/openapi_parser/schema_validator/base.rb +39 -0
- data/lib/openapi_parser/schema_validator/boolean_validator.rb +29 -0
- data/lib/openapi_parser/schema_validator/enumable.rb +13 -0
- data/lib/openapi_parser/schema_validator/float_validator.rb +34 -0
- data/lib/openapi_parser/schema_validator/integer_validator.rb +32 -0
- data/lib/openapi_parser/schema_validator/minimum_maximum.rb +38 -0
- data/lib/openapi_parser/schema_validator/nil_validator.rb +11 -0
- data/lib/openapi_parser/schema_validator/object_validator.rb +56 -0
- data/lib/openapi_parser/schema_validator/one_of_validator.rb +22 -0
- data/lib/openapi_parser/schema_validator/options.rb +29 -0
- data/lib/openapi_parser/schema_validator/string_validator.rb +108 -0
- data/lib/openapi_parser/schema_validator/unspecified_type_validator.rb +8 -0
- data/lib/openapi_parser/schema_validator.rb +164 -0
- data/lib/openapi_parser/schemas/base.rb +28 -0
- data/lib/openapi_parser/schemas/classes.rb +22 -0
- data/lib/openapi_parser/schemas/components.rb +32 -0
- data/lib/openapi_parser/schemas/discriminator.rb +11 -0
- data/lib/openapi_parser/schemas/header.rb +18 -0
- data/lib/openapi_parser/schemas/info.rb +6 -0
- data/lib/openapi_parser/schemas/media_type.rb +18 -0
- data/lib/openapi_parser/schemas/openapi.rb +63 -0
- data/lib/openapi_parser/schemas/operation.rb +50 -0
- data/lib/openapi_parser/schemas/parameter.rb +20 -0
- data/lib/openapi_parser/schemas/path_item.rb +22 -0
- data/lib/openapi_parser/schemas/paths.rb +7 -0
- data/lib/openapi_parser/schemas/reference.rb +7 -0
- data/lib/openapi_parser/schemas/request_body.rb +34 -0
- data/lib/openapi_parser/schemas/response.rb +54 -0
- data/lib/openapi_parser/schemas/responses.rb +56 -0
- data/lib/openapi_parser/schemas/schema.rb +117 -0
- data/lib/openapi_parser/schemas/security.rb +7 -0
- data/lib/openapi_parser/schemas/security_schemes.rb +20 -0
- data/lib/openapi_parser/schemas.rb +20 -0
- data/lib/openapi_parser/version.rb +3 -0
- data/lib/openapi_parser.rb +108 -0
- data/openapi_parser.gemspec +43 -0
- data/sig/openapi_parser/config.rbs +19 -0
- data/sig/openapi_parser/errors.rbs +22 -0
- data/sig/openapi_parser/reference_expander.rbs +3 -0
- data/sig/openapi_parser/schema_validator.rbs +46 -0
- data/sig/openapi_parser/schema_validators/base.rbs +18 -0
- data/sig/openapi_parser/schema_validators/options.rbs +17 -0
- data/sig/openapi_parser/schemas/base.rbs +17 -0
- data/sig/openapi_parser/version.rbs +3 -0
- data/sig/openapi_parser.rbs +19 -0
- data/sig/types.rbs +13 -0
- data/sig/wip_types.rbs +64 -0
- metadata +288 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class OpenAPIError < StandardError
|
|
3
|
+
def initialize(reference)
|
|
4
|
+
@reference = reference
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class MissingReferenceError < OpenAPIError
|
|
9
|
+
def message
|
|
10
|
+
"'#{@reference}' was referenced but could not be found"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class ValidateError < OpenAPIError
|
|
15
|
+
def initialize(data, type, reference)
|
|
16
|
+
super(reference)
|
|
17
|
+
@data = data
|
|
18
|
+
@type = type
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def message
|
|
22
|
+
"#{@reference} expected #{@type}, but received #{@data.class}: #{@data.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# create ValidateError for SchemaValidator return data
|
|
27
|
+
# @param [Object] value
|
|
28
|
+
# @param [OpenAPIParser::Schemas::Base] schema
|
|
29
|
+
def build_error_result(value, schema)
|
|
30
|
+
[nil, OpenAPIParser::ValidateError.new(value, schema.type, schema.object_reference)]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class NotNullError < OpenAPIError
|
|
36
|
+
def message
|
|
37
|
+
"#{@reference} does not allow null values"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class NotExistRequiredKey < OpenAPIError
|
|
42
|
+
def initialize(keys, reference)
|
|
43
|
+
super(reference)
|
|
44
|
+
@keys = keys
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def message
|
|
48
|
+
"#{@reference} missing required parameters: #{@keys.join(", ")}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class NotExistPropertyDefinition < OpenAPIError
|
|
53
|
+
def initialize(keys, reference)
|
|
54
|
+
super(reference)
|
|
55
|
+
@keys = keys
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def message
|
|
59
|
+
"#{@reference} does not define properties: #{@keys.join(", ")}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class NotExistDiscriminatorMappedSchema < OpenAPIError
|
|
64
|
+
def initialize(mapped_schema_reference, reference)
|
|
65
|
+
super(reference)
|
|
66
|
+
@mapped_schema_reference = mapped_schema_reference
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def message
|
|
70
|
+
"discriminator mapped schema #{@mapped_schema_reference} does not exist in #{@reference}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class NotExistDiscriminatorPropertyName < OpenAPIError
|
|
75
|
+
def initialize(key, value, reference)
|
|
76
|
+
super(reference)
|
|
77
|
+
@key = key
|
|
78
|
+
@value = value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def message
|
|
82
|
+
"discriminator propertyName #{@key} does not exist in value #{@value.inspect} in #{@reference}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class NotOneOf < OpenAPIError
|
|
87
|
+
def initialize(value, reference)
|
|
88
|
+
super(reference)
|
|
89
|
+
@value = value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def message
|
|
93
|
+
"#{@value.inspect} isn't one of in #{@reference}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class NotAnyOf < OpenAPIError
|
|
98
|
+
def initialize(value, reference)
|
|
99
|
+
super(reference)
|
|
100
|
+
@value = value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def message
|
|
104
|
+
"#{@value.inspect} isn't any of in #{@reference}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class NotEnumInclude < OpenAPIError
|
|
109
|
+
def initialize(value, reference)
|
|
110
|
+
super(reference)
|
|
111
|
+
@value = value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def message
|
|
115
|
+
"#{@value.inspect} isn't part of the enum in #{@reference}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class LessThanMinimum < OpenAPIError
|
|
120
|
+
def initialize(value, reference)
|
|
121
|
+
super(reference)
|
|
122
|
+
@value = value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def message
|
|
126
|
+
"#{@reference} #{@value.inspect} is less than minimum value"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class LessThanExclusiveMinimum < OpenAPIError
|
|
131
|
+
def initialize(value, reference)
|
|
132
|
+
super(reference)
|
|
133
|
+
@value = value
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def message
|
|
137
|
+
"#{@reference} #{@value.inspect} cannot be less than or equal to exclusive minimum value"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class MoreThanMaximum < OpenAPIError
|
|
142
|
+
def initialize(value, reference)
|
|
143
|
+
super(reference)
|
|
144
|
+
@value = value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def message
|
|
148
|
+
"#{@reference} #{@value.inspect} is more than maximum value"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class MoreThanExclusiveMaximum < OpenAPIError
|
|
153
|
+
def initialize(value, reference)
|
|
154
|
+
super(reference)
|
|
155
|
+
@value = value
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def message
|
|
159
|
+
"#{@reference} #{@value.inspect} cannot be more than or equal to exclusive maximum value"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class InvalidPattern < OpenAPIError
|
|
164
|
+
def initialize(value, pattern, reference, example)
|
|
165
|
+
super(reference)
|
|
166
|
+
@value = value
|
|
167
|
+
@pattern = pattern
|
|
168
|
+
@example = example
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def message
|
|
172
|
+
"#{@reference} pattern #{@pattern} does not match value: #{@value.inspect}#{@example ? ", example: #{@example}" : ""}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class InvalidEmailFormat < OpenAPIError
|
|
177
|
+
def initialize(value, reference)
|
|
178
|
+
super(reference)
|
|
179
|
+
@value = value
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def message
|
|
183
|
+
"#{@reference} email address format does not match value: #{@value.inspect}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class InvalidUUIDFormat < OpenAPIError
|
|
188
|
+
def initialize(value, reference)
|
|
189
|
+
super(reference)
|
|
190
|
+
@value = value
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def message
|
|
194
|
+
"#{@reference} Value: #{@value.inspect} is not conformant with UUID format"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
class InvalidDateFormat < OpenAPIError
|
|
199
|
+
def initialize(value, reference)
|
|
200
|
+
super(reference)
|
|
201
|
+
@value = value
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def message
|
|
205
|
+
"#{@reference} Value: #{@value.inspect} is not conformant with date format"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class InvalidDateTimeFormat < OpenAPIError
|
|
210
|
+
def initialize(value, reference)
|
|
211
|
+
super(reference)
|
|
212
|
+
@value = value
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def message
|
|
216
|
+
"#{@reference} Value: #{@value.inspect} is not conformant with date-time format"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class NotExistStatusCodeDefinition < OpenAPIError
|
|
221
|
+
def message
|
|
222
|
+
"#{@reference} status code definition does not exist"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
class NotExistContentTypeDefinition < OpenAPIError
|
|
227
|
+
def message
|
|
228
|
+
"#{@reference} response definition does not exist"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
class MoreThanMaxLength < OpenAPIError
|
|
233
|
+
def initialize(value, reference)
|
|
234
|
+
super(reference)
|
|
235
|
+
@value = value
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def message
|
|
239
|
+
"#{@reference} #{@value.inspect} is longer than max length"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
class LessThanMinLength < OpenAPIError
|
|
244
|
+
def initialize(value, reference)
|
|
245
|
+
super(reference)
|
|
246
|
+
@value = value
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def message
|
|
250
|
+
"#{@reference} #{@value.inspect} is shorter than min length"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class MoreThanMaxItems < OpenAPIError
|
|
255
|
+
def initialize(value, reference)
|
|
256
|
+
super(reference)
|
|
257
|
+
@value = value
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def message
|
|
261
|
+
"#{@reference} #{@value.inspect} contains more than max items"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
class LessThanMinItems < OpenAPIError
|
|
266
|
+
def initialize(value, reference)
|
|
267
|
+
super(reference)
|
|
268
|
+
@value = value
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def message
|
|
272
|
+
"#{@reference} #{@value.inspect} contains fewer than min items"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
class ValidateSecurityError < OpenAPIError
|
|
277
|
+
def message
|
|
278
|
+
"access denied"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class OpenAPIParser::ParameterValidator
|
|
2
|
+
class << self
|
|
3
|
+
# @param [Hash{String => Parameter}] parameters_hash
|
|
4
|
+
# @param [Hash] params
|
|
5
|
+
# @param [String] object_reference
|
|
6
|
+
# @param [OpenAPIParser::SchemaValidator::Options] options
|
|
7
|
+
# @param [Boolean] is_header is header or not (ignore params key case)
|
|
8
|
+
def validate_parameter(parameters_hash, params, object_reference, options, is_header = false)
|
|
9
|
+
no_exist_required_key = []
|
|
10
|
+
|
|
11
|
+
params_key_converted = params.keys.map { |k| [convert_key(k, is_header), k] }.to_h
|
|
12
|
+
parameters_hash.each do |k, v|
|
|
13
|
+
key = params_key_converted[convert_key(k, is_header)]
|
|
14
|
+
if params.include?(key)
|
|
15
|
+
coerced = v.validate_params(params[key], options)
|
|
16
|
+
params[key] = coerced if options.coerce_value
|
|
17
|
+
elsif v.required
|
|
18
|
+
no_exist_required_key << k
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
raise OpenAPIParser::NotExistRequiredKey.new(no_exist_required_key, object_reference) unless no_exist_required_key.empty?
|
|
23
|
+
|
|
24
|
+
params
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def convert_key(k, is_header)
|
|
30
|
+
is_header ? k&.downcase : k
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
class OpenAPIParser::PathItemFinder
|
|
2
|
+
# @param [OpenAPIParser::Schemas::Paths] paths
|
|
3
|
+
def initialize(paths)
|
|
4
|
+
@paths = paths
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# find operation object and if not exist return nil
|
|
8
|
+
# @param [String, Symbol] http_method like (get, post .... allow symbol)
|
|
9
|
+
# @param [String] request_path
|
|
10
|
+
# @return [Result, nil]
|
|
11
|
+
def operation_object(http_method, request_path)
|
|
12
|
+
if (path_item_object = @paths.path[request_path])
|
|
13
|
+
if (op = path_item_object.operation(http_method))
|
|
14
|
+
return Result.new(path_item_object, op, request_path, {}) # find no path_params path
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# check with path_params
|
|
19
|
+
parse_request_path(http_method, request_path)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Result
|
|
23
|
+
attr_reader :path_item_object, :operation_object, :original_path, :path_params
|
|
24
|
+
# @!attribute [r] path_item_object
|
|
25
|
+
# @return [OpenAPIParser::Schemas::PathItem]
|
|
26
|
+
# @!attribute [r] operation_object
|
|
27
|
+
# @return [OpenAPIParser::Schemas::Operation]
|
|
28
|
+
# @!attribute [r] path_params
|
|
29
|
+
# @return [Hash{String => String}]
|
|
30
|
+
# @!attribute [r] original_path
|
|
31
|
+
# @return [String]
|
|
32
|
+
|
|
33
|
+
def initialize(path_item_object, operation_object, original_path, path_params)
|
|
34
|
+
@path_item_object = path_item_object
|
|
35
|
+
@operation_object = operation_object
|
|
36
|
+
@original_path = original_path
|
|
37
|
+
@path_params = path_params
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_path_parameters(schema_path, request_path)
|
|
42
|
+
parameters = path_parameters(schema_path)
|
|
43
|
+
return nil if parameters.empty?
|
|
44
|
+
|
|
45
|
+
# If there are regex special characters in the path, the regex will
|
|
46
|
+
# be too permissive, so escape the non-parameter parts.
|
|
47
|
+
components = []
|
|
48
|
+
unprocessed = schema_path.dup
|
|
49
|
+
parameters.each do |parameter|
|
|
50
|
+
parts = unprocessed.partition(parameter)
|
|
51
|
+
components << Regexp.escape(parts[0]) unless parts[0] == ''
|
|
52
|
+
components << "(?<#{param_name(parameter)}>.+)"
|
|
53
|
+
unprocessed = parts[2]
|
|
54
|
+
end
|
|
55
|
+
components << Regexp.escape(unprocessed) unless unprocessed == ''
|
|
56
|
+
|
|
57
|
+
regex = components.join('')
|
|
58
|
+
matches = request_path.match(regex)
|
|
59
|
+
return nil unless matches
|
|
60
|
+
|
|
61
|
+
# Match up the captured names with the captured values as a hash
|
|
62
|
+
matches.names.zip(matches.captures).to_h
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
def path_parameters(schema_path)
|
|
67
|
+
# OAS3 follows a RFC6570 subset for URL templates
|
|
68
|
+
# https://swagger.io/docs/specification/serialization/#uri-templates
|
|
69
|
+
# A URL template param can be preceded optionally by a "." or ";", and can be succeeded optionally by a "*";
|
|
70
|
+
# this regex returns a match of the full parameter name with all of these modifiers. Ex: {;id*}
|
|
71
|
+
parameters = schema_path.scan(/(\{[\.;]*[^\{\*\}]+\**\})/)
|
|
72
|
+
# The `String#scan` method returns an array of arrays; we want an array of strings
|
|
73
|
+
parameters.collect { |param| param[0] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# check if there is a identical path in the schema (without any param)
|
|
77
|
+
def matches_directly?(request_path, http_method)
|
|
78
|
+
@paths.path[request_path]&.operation(http_method)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# used to filter paths with different depth or without given http method
|
|
82
|
+
def different_depth_or_method?(splitted_schema_path, splitted_request_path, path_item, http_method)
|
|
83
|
+
splitted_schema_path.size != splitted_request_path.size || !path_item.operation(http_method)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# check if the path item is a template
|
|
87
|
+
# EXAMPLE: path_template?('{id}') => true
|
|
88
|
+
def path_template?(schema_path_item)
|
|
89
|
+
schema_path_item.start_with?('{') && schema_path_item.end_with?('}')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# get the parameter name from the schema path item
|
|
93
|
+
# EXAMPLE: param_name('{id}') => 'id'
|
|
94
|
+
def param_name(schema_path_item)
|
|
95
|
+
schema_path_item[1..(schema_path_item.length - 2)]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# extract params by comparing the request path and the path from schema
|
|
99
|
+
# EXAMPLE:
|
|
100
|
+
# extract_params(['org', '1', 'user', '2', 'edit'], ['org', '{org_id}', 'user', '{user_id}'])
|
|
101
|
+
# => { 'org_id' => 1, 'user_id' => 2 }
|
|
102
|
+
# return nil if the schema does not match
|
|
103
|
+
def extract_params(splitted_request_path, splitted_schema_path)
|
|
104
|
+
splitted_request_path.zip(splitted_schema_path).reduce({}) do |result, zip_item|
|
|
105
|
+
request_path_item, schema_path_item = zip_item
|
|
106
|
+
|
|
107
|
+
params = parse_path_parameters(schema_path_item, request_path_item)
|
|
108
|
+
if params
|
|
109
|
+
result.merge!(params)
|
|
110
|
+
else
|
|
111
|
+
return if schema_path_item != request_path_item
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
result
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# find all matching paths with parameters extracted
|
|
119
|
+
# EXAMPLE:
|
|
120
|
+
# [
|
|
121
|
+
# ['/user/{id}/edit', { 'id' => 1 }],
|
|
122
|
+
# ['/user/{id}/{action}', { 'id' => 1, 'action' => 'edit' }],
|
|
123
|
+
# ]
|
|
124
|
+
def matching_paths_with_params(request_path, http_method)
|
|
125
|
+
splitted_request_path = request_path.split('/')
|
|
126
|
+
|
|
127
|
+
@paths.path.reduce([]) do |result, item|
|
|
128
|
+
path, path_item = item
|
|
129
|
+
splitted_schema_path = path.split('/')
|
|
130
|
+
|
|
131
|
+
next result if different_depth_or_method?(splitted_schema_path, splitted_request_path, path_item, http_method)
|
|
132
|
+
|
|
133
|
+
extracted_params = extract_params(splitted_request_path, splitted_schema_path)
|
|
134
|
+
result << [path, extracted_params] if extracted_params
|
|
135
|
+
result
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# find matching path and extract params
|
|
140
|
+
# EXAMPLE: find_path_and_params('get', '/user/1') => ['/user/{id}', { 'id' => 1 }]
|
|
141
|
+
def find_path_and_params(http_method, request_path)
|
|
142
|
+
return [request_path, {}] if matches_directly?(request_path, http_method)
|
|
143
|
+
|
|
144
|
+
matching = matching_paths_with_params(request_path, http_method)
|
|
145
|
+
|
|
146
|
+
# if there are many matching paths, return the one with the smallest number of params
|
|
147
|
+
# (prefer /user/{id}/action over /user/{param_1}/{param_2} )
|
|
148
|
+
matching.min_by { |match| match[1].size }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def parse_request_path(http_method, request_path)
|
|
152
|
+
original_path, path_params = find_path_and_params(http_method, request_path)
|
|
153
|
+
return nil unless original_path # # can't find
|
|
154
|
+
|
|
155
|
+
path_item_object = @paths.path[original_path]
|
|
156
|
+
obj = path_item_object.operation(http_method.to_s)
|
|
157
|
+
return nil unless obj
|
|
158
|
+
|
|
159
|
+
Result.new(path_item_object, obj, original_path, path_params)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# binding request data and operation object
|
|
2
|
+
|
|
3
|
+
class OpenAPIParser::RequestOperation
|
|
4
|
+
class << self
|
|
5
|
+
# @param [OpenAPIParser::Config] config
|
|
6
|
+
# @param [OpenAPIParser::PathItemFinder] path_item_finder
|
|
7
|
+
# @return [OpenAPIParser::RequestOperation, nil]
|
|
8
|
+
def create(http_method, request_path, path_item_finder, config, security_schemes = {})
|
|
9
|
+
result = path_item_finder.operation_object(http_method, request_path)
|
|
10
|
+
return nil unless result
|
|
11
|
+
|
|
12
|
+
self.new(http_method, result, config, security_schemes)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @!attribute [r] operation_object
|
|
17
|
+
# @return [OpenAPIParser::Schemas::Operation]
|
|
18
|
+
# @!attribute [r] path_params
|
|
19
|
+
# @return [Hash{String => String}]
|
|
20
|
+
# @!attribute [r] config
|
|
21
|
+
# @return [OpenAPIParser::Config]
|
|
22
|
+
# @!attribute [r] http_method
|
|
23
|
+
# @return [String]
|
|
24
|
+
# @!attribute [r] original_path
|
|
25
|
+
# @return [String]
|
|
26
|
+
# @!attribute [r] path_item
|
|
27
|
+
# @return [OpenAPIParser::Schemas::PathItem]
|
|
28
|
+
attr_reader :operation_object, :path_params, :config, :http_method, :original_path, :path_item, :security_schemes
|
|
29
|
+
|
|
30
|
+
# @param [String] http_method
|
|
31
|
+
# @param [OpenAPIParser::PathItemFinder::Result] result
|
|
32
|
+
# @param [OpenAPIParser::Config] config
|
|
33
|
+
def initialize(http_method, result, config, security_schemes)
|
|
34
|
+
@http_method = http_method.to_s
|
|
35
|
+
@original_path = result.original_path
|
|
36
|
+
@operation_object = result.operation_object
|
|
37
|
+
@path_params = result.path_params || {}
|
|
38
|
+
@path_item = result.path_item_object
|
|
39
|
+
@config = config
|
|
40
|
+
@security_schemes = security_schemes
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate_security(security_schemes, headers)
|
|
44
|
+
operation_object.validate_security_schemes(security_schemes, headers)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_path_params(options = nil)
|
|
48
|
+
options ||= config.path_params_options
|
|
49
|
+
operation_object&.validate_path_params(path_params, options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param [String] content_type
|
|
53
|
+
# @param [Hash] params
|
|
54
|
+
# @param [OpenAPIParser::SchemaValidator::Options] options
|
|
55
|
+
def validate_request_body(content_type, params, options = nil)
|
|
56
|
+
options ||= config.request_body_options
|
|
57
|
+
operation_object&.validate_request_body(content_type, params, options)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param [OpenAPIParser::RequestOperation::ValidatableResponseBody] response_body
|
|
61
|
+
# @param [OpenAPIParser::SchemaValidator::ResponseValidateOptions] response_validate_options
|
|
62
|
+
def validate_response_body(response_body, response_validate_options = nil)
|
|
63
|
+
response_validate_options ||= config.response_validate_options
|
|
64
|
+
|
|
65
|
+
operation_object&.validate_response(response_body, response_validate_options)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param [Hash] params parameter hash
|
|
69
|
+
# @param [Hash] headers headers hash
|
|
70
|
+
# @param [OpenAPIParser::SchemaValidator::Options] options request validator options
|
|
71
|
+
def validate_request_parameter(params, headers, options = nil)
|
|
72
|
+
options ||= config.request_validator_options
|
|
73
|
+
validate_security(security_schemes, headers)
|
|
74
|
+
operation_object&.validate_request_parameter(params, headers, options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class ValidatableResponseBody
|
|
78
|
+
attr_reader :status_code, :response_data, :headers
|
|
79
|
+
|
|
80
|
+
def initialize(status_code, response_data, headers)
|
|
81
|
+
@status_code = status_code
|
|
82
|
+
@response_data = response_data
|
|
83
|
+
@headers = headers
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def content_type
|
|
87
|
+
headers['Content-Type'].to_s.split(';').first.to_s
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# validate AllOf schema
|
|
2
|
+
class OpenAPIParser::SchemaValidator
|
|
3
|
+
class AllOfValidator < Base
|
|
4
|
+
# coerce and validate value
|
|
5
|
+
# @param [Object] value
|
|
6
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
7
|
+
def coerce_and_validate(value, schema, **keyword_args)
|
|
8
|
+
# if any schema return error, it's not valida all of value
|
|
9
|
+
remaining_keys = value.kind_of?(Hash) ? value.keys : []
|
|
10
|
+
nested_additional_properties = false
|
|
11
|
+
schema.all_of.each do |s|
|
|
12
|
+
# We need to store the reference to all of, so we can perform strict check on allowed properties
|
|
13
|
+
_coerced, err = validatable.validate_schema(
|
|
14
|
+
value,
|
|
15
|
+
s,
|
|
16
|
+
:parent_all_of => true,
|
|
17
|
+
parent_discriminator_schemas: keyword_args[:parent_discriminator_schemas]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if s.type == "object"
|
|
21
|
+
remaining_keys -= (s.properties || {}).keys
|
|
22
|
+
nested_additional_properties = true if s.additional_properties
|
|
23
|
+
else
|
|
24
|
+
# If this is not allOf having array of objects inside, but e.g. having another anyOf/oneOf nested
|
|
25
|
+
remaining_keys.clear
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return [nil, err] if err
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# If there are nested additionalProperites, we allow not defined extra properties and lean on the specific
|
|
32
|
+
# additionalProperties validation
|
|
33
|
+
if !nested_additional_properties && !remaining_keys.empty?
|
|
34
|
+
return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
[value, nil]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class OpenAPIParser::SchemaValidator
|
|
2
|
+
class AnyOfValidator < Base
|
|
3
|
+
# @param [Object] value
|
|
4
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
5
|
+
def coerce_and_validate(value, schema, **_keyword_args)
|
|
6
|
+
if schema.discriminator
|
|
7
|
+
return validate_discriminator_schema(schema.discriminator, value)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# in all schema return error (=true) not any of data
|
|
11
|
+
schema.any_of.each do |s|
|
|
12
|
+
coerced, err = validatable.validate_schema(value, s)
|
|
13
|
+
return [coerced, nil] if err.nil?
|
|
14
|
+
end
|
|
15
|
+
[nil, OpenAPIParser::NotAnyOf.new(value, schema.object_reference)]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class OpenAPIParser::SchemaValidator
|
|
2
|
+
class ArrayValidator < Base
|
|
3
|
+
# @param [Array] value
|
|
4
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
5
|
+
def coerce_and_validate(value, schema, **_keyword_args)
|
|
6
|
+
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Array)
|
|
7
|
+
|
|
8
|
+
value, err = validate_max_min_items(value, schema)
|
|
9
|
+
return [nil, err] if err
|
|
10
|
+
|
|
11
|
+
# array type have an schema in items property
|
|
12
|
+
items_schema = schema.items
|
|
13
|
+
coerced_values = value.map do |v|
|
|
14
|
+
coerced, err = validatable.validate_schema(v, items_schema)
|
|
15
|
+
return [nil, err] if err
|
|
16
|
+
|
|
17
|
+
coerced
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
value.each_index { |idx| value[idx] = coerced_values[idx] } if @coerce_value
|
|
21
|
+
|
|
22
|
+
[value, nil]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate_max_min_items(value, schema)
|
|
26
|
+
return [nil, OpenAPIParser::MoreThanMaxItems.new(value, schema.object_reference)] if schema.maxItems && value.length > schema.maxItems
|
|
27
|
+
return [nil, OpenAPIParser::LessThanMinItems.new(value, schema.object_reference)] if schema.minItems && value.length < schema.minItems
|
|
28
|
+
|
|
29
|
+
[value, nil]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|