rspec-rails-api 0.4.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -5
- data/.gitlab-ci.yml +1 -1
- data/.rubocop.yml +5 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +119 -27
- data/Gemfile.lock +98 -0
- data/README.md +210 -7
- data/lib/rspec/rails/api/dsl/example.rb +36 -19
- data/lib/rspec/rails/api/dsl/example_group.rb +12 -2
- data/lib/rspec/rails/api/entity_config.rb +7 -10
- data/lib/rspec/rails/api/field_config.rb +6 -3
- data/lib/rspec/rails/api/matchers.rb +22 -27
- data/lib/rspec/rails/api/metadata.rb +71 -35
- data/lib/rspec/rails/api/open_api_renderer.rb +161 -45
- data/lib/rspec/rails/api/utils.rb +28 -129
- data/lib/rspec/rails/api/validator.rb +211 -0
- data/lib/rspec/rails/api/version.rb +1 -1
- data/lib/rspec_rails_api.rb +2 -5
- data/rspec-rails-api.gemspec +4 -1
- metadata +49 -5
@@ -6,7 +6,7 @@ require 'active_support'
|
|
6
6
|
module RSpec
|
7
7
|
module Rails
|
8
8
|
module Api
|
9
|
-
# Class to render
|
9
|
+
# Class to render metadata.
|
10
10
|
# Example:
|
11
11
|
# ```rb
|
12
12
|
# renderer = RSpec::Rails::Api::OpenApiRenderer.new
|
@@ -15,16 +15,36 @@ module RSpec
|
|
15
15
|
# ```
|
16
16
|
class OpenApiRenderer # rubocop:disable Metrics/ClassLength
|
17
17
|
attr_writer :api_servers, :api_title, :api_version, :api_description, :api_tos
|
18
|
+
attr_reader :redactables
|
18
19
|
|
19
20
|
def initialize
|
20
|
-
@metadata
|
21
|
-
@api_infos
|
22
|
-
@api_servers
|
23
|
-
@api_paths
|
21
|
+
@metadata = { resources: {}, entities: {} }
|
22
|
+
@api_infos = {}
|
23
|
+
@api_servers = []
|
24
|
+
@api_paths = {}
|
24
25
|
@api_components = {}
|
25
26
|
@api_tags = []
|
26
27
|
@api_contact = {}
|
27
28
|
@api_license = {}
|
29
|
+
@api_security = {}
|
30
|
+
@redactables = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def redact_responses(pairs)
|
34
|
+
@redactables = pairs
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Adds a security scheme definition to the API documentation
|
39
|
+
#
|
40
|
+
# @param reference [Symbol] Reference to use in the tests
|
41
|
+
# @param name [String] Human friendly name
|
42
|
+
# @param definition [Hash] Security scheme definition as per https://swagger.io/specification/#security-scheme-object
|
43
|
+
def add_security_scheme(reference, name, definition)
|
44
|
+
raise "Security scheme #{reference} is already defined" if @api_security.key? reference
|
45
|
+
|
46
|
+
definition[:name] = name
|
47
|
+
@api_security[reference] = definition
|
28
48
|
end
|
29
49
|
|
30
50
|
##
|
@@ -35,11 +55,10 @@ module RSpec
|
|
35
55
|
#
|
36
56
|
# @return [void
|
37
57
|
def merge_context(context, dump_metadata: false)
|
38
|
-
@metadata[:resources].deep_merge! context[:resources]
|
39
|
-
@metadata[:entities].deep_merge! context[:entities]
|
58
|
+
@metadata[:resources].deep_merge! context.respond_to?(:resources) ? context.resources : context[:resources]
|
40
59
|
|
41
60
|
# Save context for debug and fixtures
|
42
|
-
File.write ::Rails.root.join('tmp', 'rra_metadata.yaml'),
|
61
|
+
File.write ::Rails.root.join('tmp', 'rra_metadata.yaml'), @metadata.to_yaml if dump_metadata
|
43
62
|
end
|
44
63
|
|
45
64
|
##
|
@@ -50,14 +69,20 @@ module RSpec
|
|
50
69
|
#
|
51
70
|
# @return [void]
|
52
71
|
def write_files(path = nil, only: %i[yaml json])
|
72
|
+
return unless write_file? RSpec.world.filtered_examples
|
73
|
+
|
53
74
|
path ||= ::Rails.root.join('tmp', 'rspec_api_rails')
|
54
75
|
|
55
|
-
|
76
|
+
metadata = prepare_metadata
|
56
77
|
|
78
|
+
file_types = %i[yaml json]
|
57
79
|
only.each do |type|
|
58
80
|
next unless file_types.include? type
|
59
81
|
|
60
|
-
|
82
|
+
data = metadata.to_yaml if type == :yaml
|
83
|
+
data = JSON.pretty_generate(metadata) if type == :json
|
84
|
+
|
85
|
+
File.write "#{path}.#{type}", data
|
61
86
|
end
|
62
87
|
end
|
63
88
|
|
@@ -66,7 +91,7 @@ module RSpec
|
|
66
91
|
#
|
67
92
|
# @return [Hash] The OpenAPI structure
|
68
93
|
def prepare_metadata
|
69
|
-
|
94
|
+
extract_metadata
|
70
95
|
# Example: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml
|
71
96
|
hash = {
|
72
97
|
openapi: '3.0.0',
|
@@ -76,7 +101,7 @@ module RSpec
|
|
76
101
|
components: @api_components,
|
77
102
|
tags: @api_tags,
|
78
103
|
}
|
79
|
-
JSON.parse(
|
104
|
+
JSON.parse(hash.to_json)
|
80
105
|
end
|
81
106
|
|
82
107
|
##
|
@@ -107,29 +132,50 @@ module RSpec
|
|
107
132
|
|
108
133
|
private
|
109
134
|
|
135
|
+
def write_file?(examples)
|
136
|
+
acceptance_examples = examples.values.flatten.filter do |e|
|
137
|
+
e.metadata[:type] == :acceptance
|
138
|
+
end
|
139
|
+
unless acceptance_examples.none?(&:exception)
|
140
|
+
puts "\n\e[00;31mSome acceptance tests failed. OpenApi specification file was not updated.\n\e[00m"
|
141
|
+
return false
|
142
|
+
end
|
143
|
+
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
110
147
|
##
|
111
148
|
# Extracts metadata for rendering
|
112
149
|
#
|
113
150
|
# @return [void]
|
114
|
-
def
|
115
|
-
|
151
|
+
def extract_metadata
|
152
|
+
extract_security
|
116
153
|
api_infos
|
117
154
|
api_servers
|
155
|
+
global_entities
|
156
|
+
extract_from_resources
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Extracts metadata from security schemes for rendering
|
161
|
+
#
|
162
|
+
# @return [void]
|
163
|
+
def extract_security
|
164
|
+
return unless @api_security.keys.count.positive?
|
165
|
+
|
166
|
+
@api_components['securitySchemes'] = @api_security
|
118
167
|
end
|
119
168
|
|
120
169
|
##
|
121
170
|
# Extracts metadata from resources for rendering
|
122
171
|
#
|
123
172
|
# @return [void]
|
124
|
-
def extract_from_resources
|
173
|
+
def extract_from_resources
|
125
174
|
@api_components[:schemas] ||= {}
|
126
175
|
@metadata[:resources].each do |resource_key, resource|
|
127
|
-
resource[:entities].each do |name, entity|
|
128
|
-
@api_components[:schemas][name] = process_entity(entity)
|
129
|
-
end
|
130
176
|
@api_tags.push(
|
131
177
|
name: resource_key.to_s,
|
132
|
-
description: resource[:description]
|
178
|
+
description: resource[:description].presence&.strip || ''
|
133
179
|
)
|
134
180
|
process_resource resource: resource_key, resource_config: resource
|
135
181
|
end
|
@@ -177,24 +223,29 @@ module RSpec
|
|
177
223
|
parameters
|
178
224
|
end
|
179
225
|
|
180
|
-
def process_entity(entity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
226
|
+
def process_entity(entity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
181
227
|
schema = {
|
182
228
|
properties: {},
|
183
229
|
}
|
184
230
|
required = []
|
185
231
|
entity.fields.each do |name, field|
|
186
232
|
property = {
|
187
|
-
description: field.description,
|
233
|
+
description: field.description.presence&.strip || '',
|
188
234
|
type: PARAM_TYPES[field.type][:type],
|
189
235
|
}
|
190
|
-
property[:format]
|
191
|
-
|
192
|
-
# Primitives support
|
236
|
+
property[:format] = PARAM_TYPES[field.type][:format] if PARAM_TYPES[field.type][:format]
|
237
|
+
|
193
238
|
if PRIMITIVES.include? field.attributes
|
194
|
-
property[:items] =
|
195
|
-
|
239
|
+
property[:items] = { type: field.attributes }
|
240
|
+
elsif field.type == :object && field.attributes.is_a?(Symbol)
|
241
|
+
property = { '$ref' => "#/components/schemas/#{field.attributes}" }
|
242
|
+
elsif field.type == :array && field.attributes.is_a?(Symbol)
|
243
|
+
property = { type: :array, items: { '$ref' => "#/components/schemas/#{field.attributes}" } }
|
196
244
|
end
|
245
|
+
|
197
246
|
required.push name unless field.required == false
|
247
|
+
|
248
|
+
schema[:properties][name] = property
|
198
249
|
end
|
199
250
|
|
200
251
|
schema[:required] = required unless required.size.zero?
|
@@ -210,10 +261,10 @@ module RSpec
|
|
210
261
|
#
|
211
262
|
#
|
212
263
|
# @return [void]
|
213
|
-
def process_path_param(name, param) # rubocop:disable Metrics/MethodLength
|
264
|
+
def process_path_param(name, param) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
214
265
|
parameter = {
|
215
266
|
name: name.to_s,
|
216
|
-
description: param[:description],
|
267
|
+
description: param[:description].presence&.strip || '',
|
217
268
|
required: param[:required] || true,
|
218
269
|
in: param[:scope].to_s,
|
219
270
|
schema: {
|
@@ -239,7 +290,7 @@ module RSpec
|
|
239
290
|
#
|
240
291
|
# FIXME: Rename "action_config" to "action"
|
241
292
|
# FIXME: Rename "parameters" to "path_parameters"
|
242
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
293
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
243
294
|
def process_action(resource: nil, path: nil, path_config: nil, action_config: nil, parameters: nil)
|
244
295
|
responses = {}
|
245
296
|
request_body = nil
|
@@ -257,21 +308,31 @@ module RSpec
|
|
257
308
|
responses[status_key] = process_response status: status_key, status_config: status, content: content
|
258
309
|
end
|
259
310
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
operationId: "#{resource} #{summary}".downcase.gsub(/[^\w]/, '_'),
|
311
|
+
action = {
|
312
|
+
summary: path_config[:actions][action_config][:summary]&.strip || '',
|
313
|
+
description: path_config[:actions][action_config][:description].presence&.strip || '',
|
314
|
+
operationId: "#{resource} #{action_config} #{path}".downcase.gsub(/[^\w]/, '_'),
|
265
315
|
parameters: parameters,
|
266
316
|
responses: responses,
|
267
317
|
tags: [resource.to_s],
|
268
318
|
}
|
269
319
|
|
320
|
+
if path_config[:actions][action_config].key? :security
|
321
|
+
references = path_config[:actions][action_config][:security]
|
322
|
+
|
323
|
+
action[:security] = []
|
324
|
+
references.each do |reference|
|
325
|
+
raise "No security scheme defined with reference #{reference}" unless @api_security.key? reference
|
326
|
+
|
327
|
+
action[:security].push({ reference => [] })
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
270
331
|
action[:requestBody] = request_body if request_body
|
271
332
|
|
272
333
|
action
|
273
334
|
end
|
274
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
335
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
275
336
|
|
276
337
|
##
|
277
338
|
# Processes a request body from metadata
|
@@ -282,12 +343,13 @@ module RSpec
|
|
282
343
|
#
|
283
344
|
# @return [void]
|
284
345
|
def process_request_body(schema: nil, ref: nil, examples: {})
|
285
|
-
Utils.deep_set @api_components,
|
346
|
+
Utils.deep_set @api_components, ['schemas', ref], schema
|
347
|
+
|
286
348
|
{
|
287
349
|
# description: '',
|
288
350
|
required: true,
|
289
351
|
content: {
|
290
|
-
|
352
|
+
content_type_from_schema(schema) => {
|
291
353
|
schema: { '$ref' => "#/components/schemas/#{ref}" },
|
292
354
|
examples: examples,
|
293
355
|
},
|
@@ -295,6 +357,23 @@ module RSpec
|
|
295
357
|
}
|
296
358
|
end
|
297
359
|
|
360
|
+
def content_type_from_schema(schema)
|
361
|
+
schema_includes_file?(schema) ? 'multipart/form-data' : 'application/json'
|
362
|
+
end
|
363
|
+
|
364
|
+
def schema_includes_file?(schema)
|
365
|
+
return true if schema[:type] == 'string' && schema[:format] == 'binary'
|
366
|
+
return false unless schema[:properties].is_a?(Hash) && schema[:required].is_a?(Array)
|
367
|
+
|
368
|
+
schema[:properties].each_value do |definition|
|
369
|
+
next unless schema_includes_file?(definition)
|
370
|
+
|
371
|
+
return true
|
372
|
+
end
|
373
|
+
|
374
|
+
false
|
375
|
+
end
|
376
|
+
|
298
377
|
##
|
299
378
|
# Process a response from metadata
|
300
379
|
#
|
@@ -303,31 +382,58 @@ module RSpec
|
|
303
382
|
# @param content [String] Response content
|
304
383
|
#
|
305
384
|
# @return [void]
|
306
|
-
def process_response(status: nil, status_config: nil, content: nil)
|
307
|
-
response = { description: status_config[:description] }
|
385
|
+
def process_response(status: nil, status_config: nil, content: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
386
|
+
response = { description: status_config[:description].presence&.strip || '' }
|
308
387
|
|
309
388
|
return response if status.to_s == '204' && content # No content
|
310
389
|
|
390
|
+
data = begin
|
391
|
+
JSON.parse(content)
|
392
|
+
rescue JSON::ParserError, TypeError
|
393
|
+
content
|
394
|
+
end
|
395
|
+
|
396
|
+
entity = status_config[:expectations][:one] || status_config[:expectations][:many]
|
397
|
+
|
398
|
+
# TODO: handle sub-entities
|
399
|
+
if @redactables.key?(entity) && data.is_a?(Hash)
|
400
|
+
if status_config[:expectations][:one]
|
401
|
+
@redactables[entity].each_pair do |attribute, replacement|
|
402
|
+
data[attribute.to_s] = replacement
|
403
|
+
end
|
404
|
+
else
|
405
|
+
data.each_index do |index|
|
406
|
+
@redactables[entity].each_pair do |attribute, replacement|
|
407
|
+
data[index][attribute.to_s] = replacement
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
311
413
|
response[:content] = {
|
312
414
|
'application/json': {
|
313
415
|
schema: response_schema(status_config[:expectations]),
|
314
|
-
examples: { default: { value:
|
416
|
+
examples: { default: { value: data } },
|
315
417
|
},
|
316
418
|
}
|
317
419
|
|
318
420
|
response
|
319
421
|
end
|
320
422
|
|
321
|
-
def response_schema(expectations)
|
423
|
+
def response_schema(expectations) # rubocop:disable Metrics/MethodLength
|
322
424
|
if expectations[:many]
|
323
425
|
items = if PRIMITIVES.include?(expectations[:many])
|
324
|
-
{ type: expectations[:many]
|
426
|
+
{ type: expectations[:many] }
|
325
427
|
else
|
326
428
|
{ '$ref' => "#/components/schemas/#{expectations[:many]}" }
|
327
429
|
end
|
328
430
|
{ type: 'array', items: items }
|
329
431
|
elsif expectations[:one]
|
330
|
-
|
432
|
+
if PRIMITIVES.include?(expectations[:one])
|
433
|
+
{ type: expectations[:one] }
|
434
|
+
else
|
435
|
+
{ '$ref' => "#/components/schemas/#{expectations[:one]}" }
|
436
|
+
end
|
331
437
|
end
|
332
438
|
end
|
333
439
|
|
@@ -350,6 +456,16 @@ module RSpec
|
|
350
456
|
request_examples
|
351
457
|
end
|
352
458
|
|
459
|
+
def global_entities
|
460
|
+
return if RSpec::Rails::Api::Metadata.entities.keys.count.zero?
|
461
|
+
|
462
|
+
@api_components[:schemas] = {}
|
463
|
+
|
464
|
+
RSpec::Rails::Api::Metadata.entities.each_pair do |name, entity|
|
465
|
+
@api_components[:schemas][name] = process_entity(entity)
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
353
469
|
##
|
354
470
|
# Converts path with params like ":id" to their OpenAPI representation
|
355
471
|
#
|
@@ -376,12 +492,12 @@ module RSpec
|
|
376
492
|
# Fills the API general information sections
|
377
493
|
#
|
378
494
|
# @return [void]
|
379
|
-
def api_infos
|
495
|
+
def api_infos # rubocop:disable Metrics/CyclomaticComplexity
|
380
496
|
@api_infos = {
|
381
497
|
title: @api_title || 'Some sample app',
|
382
498
|
version: @api_version || '1.0',
|
383
499
|
}
|
384
|
-
@api_infos[:description] = @api_description if @api_description
|
500
|
+
@api_infos[:description] = @api_description.strip || '' if @api_description.present?
|
385
501
|
@api_infos[:termsOfService] = @api_tos if @api_tos
|
386
502
|
@api_infos[:contact] = @api_contact if @api_contact[:name]
|
387
503
|
@api_infos[:license] = @api_license if @api_license[:name]
|
@@ -7,139 +7,38 @@ module RSpec
|
|
7
7
|
module Api
|
8
8
|
# Helper methods
|
9
9
|
class Utils
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
10
|
+
class << self
|
11
|
+
##
|
12
|
+
# Sets a value at given dotted path in a hash
|
13
|
+
#
|
14
|
+
# @param hash [Hash] The target hash
|
15
|
+
# @param path [Array] List of keys to access value
|
16
|
+
# @param value [*] Value to set
|
17
|
+
#
|
18
|
+
# @return [Hash] The modified hash
|
19
|
+
def deep_set(hash, path, value)
|
20
|
+
raise 'path should be an array' unless path.is_a? Array
|
21
|
+
|
22
|
+
return value if path.count.zero?
|
23
|
+
|
24
|
+
current_key = path.shift.to_s.to_sym
|
25
|
+
hash[current_key] = {} unless hash[current_key].is_a?(Hash)
|
26
|
+
hash[current_key] = deep_set(hash[current_key], path, value)
|
27
|
+
|
28
|
+
hash
|
22
29
|
end
|
23
|
-
end
|
24
|
-
|
25
|
-
##
|
26
|
-
# Sets a value at given dotted path in a hash
|
27
|
-
#
|
28
|
-
# @param hash [Hash] The target hash
|
29
|
-
# @param path [String] Dotted path
|
30
|
-
# @param value [*] Value to set
|
31
|
-
#
|
32
|
-
# @return [Hash] The modified hash
|
33
|
-
def self.deep_set(hash, path, value)
|
34
|
-
path = path.split('.') unless path.is_a? Array
|
35
|
-
|
36
|
-
return value if path.count.zero?
|
37
|
-
|
38
|
-
current_key = path.shift.to_sym
|
39
|
-
hash[current_key] = {} unless hash[current_key].is_a?(Hash)
|
40
|
-
hash[current_key] = deep_set(hash[current_key], path, value)
|
41
|
-
|
42
|
-
hash
|
43
|
-
end
|
44
|
-
|
45
|
-
##
|
46
|
-
# Checks if a value is of the given parameter type
|
47
|
-
#
|
48
|
-
# @param type [Symbol] Type to compare to
|
49
|
-
# @param value [*] Value to test
|
50
|
-
#
|
51
|
-
# @return [Boolean] True when the value corresponds to the given type
|
52
|
-
def self.check_value_type(type, value)
|
53
|
-
return true if type == :boolean && (value.is_a?(TrueClass) || value.is_a?(FalseClass))
|
54
|
-
return true if type == :array && value.is_a?(Array)
|
55
|
-
|
56
|
-
raise "Unknown type #{type}" unless PARAM_TYPES.key? type
|
57
|
-
|
58
|
-
value.is_a? PARAM_TYPES[type][:class]
|
59
|
-
end
|
60
|
-
|
61
|
-
##
|
62
|
-
# Validates an object keys and values types
|
63
|
-
#
|
64
|
-
# @param actual [Hash] Hash to compare
|
65
|
-
# @param expected [Hash] Structure to compare
|
66
|
-
#
|
67
|
-
# @return [Boolean] True when the object matches the structure
|
68
|
-
def self.validate_object_structure(actual, expected)
|
69
|
-
# Check keys
|
70
|
-
return false unless same_keys? actual, expected
|
71
|
-
|
72
|
-
expected.each_key do |key|
|
73
|
-
next unless expected[key][:required]
|
74
|
-
|
75
|
-
expected_type = expected[key][:type]
|
76
|
-
expected_attributes = expected[key][:attributes]
|
77
30
|
|
78
|
-
|
79
|
-
|
31
|
+
##
|
32
|
+
# Returns a hash from an object
|
33
|
+
#
|
34
|
+
# @param value [Hash,Class] A hash or something with a "body" (as responses object in tests)
|
35
|
+
#
|
36
|
+
# @return [Hash]
|
37
|
+
def hash_from_response(value)
|
38
|
+
return JSON.parse(value.body) if value.respond_to? :body
|
80
39
|
|
81
|
-
|
82
|
-
return false unless validate_deep_object expected_type, expected_attributes, actual[key.to_s]
|
40
|
+
value
|
83
41
|
end
|
84
|
-
|
85
|
-
true
|
86
|
-
end
|
87
|
-
|
88
|
-
##
|
89
|
-
# Validates an array or hash against a definition
|
90
|
-
#
|
91
|
-
# @param expected_type [:object, :array] The expected type
|
92
|
-
# @param expected_attributes [Hash] Attributes configuration
|
93
|
-
# @param actual [Array, Hash] Value to check
|
94
|
-
#
|
95
|
-
# @return [Boolean] True when `actual` is of the expected definition
|
96
|
-
def self.validate_deep_object(expected_type, expected_attributes, actual)
|
97
|
-
if %i[object array].include?(expected_type) && expected_attributes.is_a?(Hash)
|
98
|
-
case expected_type
|
99
|
-
when :object
|
100
|
-
return false unless validate_object_structure actual, expected_attributes
|
101
|
-
when :array
|
102
|
-
return false unless validate_deep_object_array actual, expected_attributes
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
true
|
107
|
-
end
|
108
|
-
|
109
|
-
##
|
110
|
-
# Validates each entry of an array
|
111
|
-
#
|
112
|
-
# @param array [Array] The array to check
|
113
|
-
# @param expected_attributes [Hash] Attributes configuration
|
114
|
-
#
|
115
|
-
# @return [Boolean] True when all values matches the attribute configuration
|
116
|
-
def self.validate_deep_object_array(array, expected_attributes)
|
117
|
-
array.each do |array_entry|
|
118
|
-
if expected_attributes[:type]
|
119
|
-
return false unless check_value_type expected_attributes[:type], array_entry
|
120
|
-
else
|
121
|
-
return false unless validate_object_structure array_entry, expected_attributes
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
true
|
126
|
-
end
|
127
|
-
|
128
|
-
##
|
129
|
-
# Checks if a hash have the required keys in it
|
130
|
-
#
|
131
|
-
# @param actual [Hash] The hash
|
132
|
-
# @param expected [Hash] Attributes definitions
|
133
|
-
#
|
134
|
-
# @return [Boolean] True when the object is valid
|
135
|
-
def self.same_keys?(actual, expected)
|
136
|
-
optional = expected.reject { |_key, value| value[:required] }.keys
|
137
|
-
actual.symbolize_keys.keys.sort - optional == expected.keys.sort - optional
|
138
|
-
end
|
139
|
-
|
140
|
-
def self.check_attribute_type(type, except: [])
|
141
|
-
keys = PARAM_TYPES.keys.reject { |key| except.include? key }
|
142
|
-
keys.include?(type)
|
143
42
|
end
|
144
43
|
end
|
145
44
|
end
|