rspec-rails-api 0.4.0 → 0.6.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/.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
|