media_types 2.2.0 → 2.3.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/.github/workflows/debian.yml +43 -43
- data/.github/workflows/publish-bookworm.yml +33 -0
- data/.github/workflows/publish-sid.yml +33 -0
- data/.github/workflows/ruby.yml +22 -22
- data/.gitignore +20 -10
- data/.rubocop.yml +29 -29
- data/CHANGELOG.md +175 -164
- data/Gemfile +6 -6
- data/LICENSE +21 -21
- data/README.md +666 -664
- data/Rakefile +12 -12
- data/bin/console +15 -15
- data/bin/setup +8 -8
- data/lib/media_types/constructable.rb +161 -160
- data/lib/media_types/dsl/errors.rb +18 -18
- data/lib/media_types/dsl.rb +172 -172
- data/lib/media_types/errors.rb +25 -19
- data/lib/media_types/formatter.rb +56 -56
- data/lib/media_types/hash.rb +21 -21
- data/lib/media_types/object.rb +35 -35
- data/lib/media_types/scheme/allow_nil.rb +30 -30
- data/lib/media_types/scheme/any_of.rb +41 -41
- data/lib/media_types/scheme/attribute.rb +46 -46
- data/lib/media_types/scheme/enumeration_context.rb +18 -18
- data/lib/media_types/scheme/enumeration_of_type.rb +80 -80
- data/lib/media_types/scheme/errors.rb +87 -87
- data/lib/media_types/scheme/links.rb +54 -54
- data/lib/media_types/scheme/missing_validation.rb +41 -41
- data/lib/media_types/scheme/not_strict.rb +15 -15
- data/lib/media_types/scheme/output_empty_guard.rb +45 -45
- data/lib/media_types/scheme/output_iterator_with_predicate.rb +66 -66
- data/lib/media_types/scheme/output_type_guard.rb +39 -39
- data/lib/media_types/scheme/rules.rb +186 -173
- data/lib/media_types/scheme/rules_exhausted_guard.rb +73 -73
- data/lib/media_types/scheme/validation_options.rb +44 -43
- data/lib/media_types/scheme.rb +535 -513
- data/lib/media_types/testing/assertions.rb +20 -20
- data/lib/media_types/validations.rb +118 -105
- data/lib/media_types/version.rb +5 -5
- data/lib/media_types/views.rb +12 -12
- data/lib/media_types.rb +73 -73
- data/media_types.gemspec +33 -33
- metadata +8 -6
data/lib/media_types/scheme.rb
CHANGED
@@ -1,513 +1,535 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
require 'media_types/scheme/validation_options'
|
6
|
-
require 'media_types/scheme/enumeration_context'
|
7
|
-
require 'media_types/scheme/errors'
|
8
|
-
require 'media_types/scheme/rules'
|
9
|
-
|
10
|
-
require 'media_types/scheme/allow_nil'
|
11
|
-
require 'media_types/scheme/any_of'
|
12
|
-
require 'media_types/scheme/attribute'
|
13
|
-
require 'media_types/scheme/enumeration_of_type'
|
14
|
-
require 'media_types/scheme/links'
|
15
|
-
require 'media_types/scheme/missing_validation'
|
16
|
-
require 'media_types/scheme/not_strict'
|
17
|
-
|
18
|
-
require 'media_types/scheme/output_empty_guard'
|
19
|
-
require 'media_types/scheme/output_type_guard'
|
20
|
-
require 'media_types/scheme/rules_exhausted_guard'
|
21
|
-
|
22
|
-
module MediaTypes
|
23
|
-
class AssertionError < StandardError
|
24
|
-
def initialize(errors)
|
25
|
-
@fixture_errors = errors
|
26
|
-
end
|
27
|
-
|
28
|
-
def message
|
29
|
-
fixture_errors.map(&:message).join(', ')
|
30
|
-
end
|
31
|
-
|
32
|
-
attr_reader :fixture_errors
|
33
|
-
end
|
34
|
-
|
35
|
-
class UnexpectedValidationResultError < StandardError
|
36
|
-
def initialize(fixture_caller, error)
|
37
|
-
self.fixture_caller = fixture_caller
|
38
|
-
self.error = error
|
39
|
-
end
|
40
|
-
|
41
|
-
def message
|
42
|
-
format(
|
43
|
-
'%<caller_path>s:%<caller_line>s -> %<error>s',
|
44
|
-
caller_path: fixture_caller.path,
|
45
|
-
caller_line: fixture_caller.lineno,
|
46
|
-
error: error.is_a?(MediaTypes::Scheme::ValidationError) ? "#{error.class}:#{error.message}" : error
|
47
|
-
)
|
48
|
-
end
|
49
|
-
|
50
|
-
attr_accessor :fixture_caller, :error
|
51
|
-
end
|
52
|
-
|
53
|
-
class FixtureData
|
54
|
-
def initialize(caller:, fixture:, expect_to_pass:)
|
55
|
-
self.caller = caller
|
56
|
-
self.fixture = fixture
|
57
|
-
self.expect_to_pass = expect_to_pass
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
#
|
91
|
-
#
|
92
|
-
# @param [
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
self.
|
99
|
-
self.
|
100
|
-
self.
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
#
|
115
|
-
#
|
116
|
-
# @
|
117
|
-
#
|
118
|
-
# @
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
#
|
133
|
-
#
|
134
|
-
# @
|
135
|
-
#
|
136
|
-
# @
|
137
|
-
# @
|
138
|
-
#
|
139
|
-
# @raise
|
140
|
-
# @raise
|
141
|
-
#
|
142
|
-
# @
|
143
|
-
#
|
144
|
-
#
|
145
|
-
#
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
# @
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
184
|
-
#
|
185
|
-
#
|
186
|
-
#
|
187
|
-
#
|
188
|
-
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
195
|
-
#
|
196
|
-
#
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
200
|
-
#
|
201
|
-
#
|
202
|
-
#
|
203
|
-
#
|
204
|
-
#
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
# @
|
230
|
-
#
|
231
|
-
#
|
232
|
-
#
|
233
|
-
#
|
234
|
-
#
|
235
|
-
#
|
236
|
-
#
|
237
|
-
#
|
238
|
-
#
|
239
|
-
#
|
240
|
-
#
|
241
|
-
#
|
242
|
-
#
|
243
|
-
#
|
244
|
-
#
|
245
|
-
#
|
246
|
-
#
|
247
|
-
#
|
248
|
-
#
|
249
|
-
#
|
250
|
-
#
|
251
|
-
#
|
252
|
-
#
|
253
|
-
#
|
254
|
-
#
|
255
|
-
#
|
256
|
-
#
|
257
|
-
#
|
258
|
-
#
|
259
|
-
#
|
260
|
-
#
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
#
|
290
|
-
#
|
291
|
-
#
|
292
|
-
#
|
293
|
-
#
|
294
|
-
#
|
295
|
-
#
|
296
|
-
#
|
297
|
-
#
|
298
|
-
#
|
299
|
-
#
|
300
|
-
#
|
301
|
-
#
|
302
|
-
#
|
303
|
-
#
|
304
|
-
#
|
305
|
-
#
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
# @param [
|
318
|
-
#
|
319
|
-
# @
|
320
|
-
#
|
321
|
-
#
|
322
|
-
#
|
323
|
-
#
|
324
|
-
#
|
325
|
-
#
|
326
|
-
#
|
327
|
-
#
|
328
|
-
#
|
329
|
-
#
|
330
|
-
#
|
331
|
-
#
|
332
|
-
#
|
333
|
-
#
|
334
|
-
#
|
335
|
-
#
|
336
|
-
#
|
337
|
-
#
|
338
|
-
#
|
339
|
-
#
|
340
|
-
#
|
341
|
-
#
|
342
|
-
#
|
343
|
-
#
|
344
|
-
#
|
345
|
-
#
|
346
|
-
#
|
347
|
-
#
|
348
|
-
#
|
349
|
-
#
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
#
|
383
|
-
#
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
#
|
392
|
-
#
|
393
|
-
#
|
394
|
-
#
|
395
|
-
#
|
396
|
-
#
|
397
|
-
#
|
398
|
-
#
|
399
|
-
#
|
400
|
-
#
|
401
|
-
#
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
#
|
412
|
-
#
|
413
|
-
#
|
414
|
-
#
|
415
|
-
#
|
416
|
-
#
|
417
|
-
#
|
418
|
-
#
|
419
|
-
#
|
420
|
-
#
|
421
|
-
#
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
end
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
def
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
@
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
end
|
483
|
-
|
484
|
-
def
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'media_types/scheme/validation_options'
|
6
|
+
require 'media_types/scheme/enumeration_context'
|
7
|
+
require 'media_types/scheme/errors'
|
8
|
+
require 'media_types/scheme/rules'
|
9
|
+
|
10
|
+
require 'media_types/scheme/allow_nil'
|
11
|
+
require 'media_types/scheme/any_of'
|
12
|
+
require 'media_types/scheme/attribute'
|
13
|
+
require 'media_types/scheme/enumeration_of_type'
|
14
|
+
require 'media_types/scheme/links'
|
15
|
+
require 'media_types/scheme/missing_validation'
|
16
|
+
require 'media_types/scheme/not_strict'
|
17
|
+
|
18
|
+
require 'media_types/scheme/output_empty_guard'
|
19
|
+
require 'media_types/scheme/output_type_guard'
|
20
|
+
require 'media_types/scheme/rules_exhausted_guard'
|
21
|
+
|
22
|
+
module MediaTypes
|
23
|
+
class AssertionError < StandardError
|
24
|
+
def initialize(errors)
|
25
|
+
@fixture_errors = errors
|
26
|
+
end
|
27
|
+
|
28
|
+
def message
|
29
|
+
fixture_errors.map(&:message).join(', ')
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :fixture_errors
|
33
|
+
end
|
34
|
+
|
35
|
+
class UnexpectedValidationResultError < StandardError
|
36
|
+
def initialize(fixture_caller, error)
|
37
|
+
self.fixture_caller = fixture_caller
|
38
|
+
self.error = error
|
39
|
+
end
|
40
|
+
|
41
|
+
def message
|
42
|
+
format(
|
43
|
+
'%<caller_path>s:%<caller_line>s -> %<error>s',
|
44
|
+
caller_path: fixture_caller.path,
|
45
|
+
caller_line: fixture_caller.lineno,
|
46
|
+
error: error.is_a?(MediaTypes::Scheme::ValidationError) ? "#{error.class}:#{error.message}" : error
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_accessor :fixture_caller, :error
|
51
|
+
end
|
52
|
+
|
53
|
+
class FixtureData
|
54
|
+
def initialize(caller:, fixture:, expect_to_pass:, loose:)
|
55
|
+
self.caller = caller
|
56
|
+
self.fixture = fixture
|
57
|
+
self.expect_to_pass = expect_to_pass
|
58
|
+
self.loose = loose
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_accessor :caller, :fixture, :expect_to_pass, :loose
|
62
|
+
|
63
|
+
alias expect_to_pass? expect_to_pass
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Media Type Schemes can validate content to a media type, by itself. Used by the `validations` dsl.
|
68
|
+
#
|
69
|
+
# @see MediaTypes::Dsl
|
70
|
+
#
|
71
|
+
# @example A scheme to test against
|
72
|
+
#
|
73
|
+
# class MyMedia
|
74
|
+
# include MediaTypes::Dsl
|
75
|
+
#
|
76
|
+
# validations do
|
77
|
+
# attribute :foo do
|
78
|
+
# collection :bar, String
|
79
|
+
# end
|
80
|
+
# attribute :number, Numeric
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# MyMedia.valid?({ foo: { bar: ['test'] }, number: 42 })
|
85
|
+
# #=> true
|
86
|
+
#
|
87
|
+
class Scheme
|
88
|
+
|
89
|
+
##
|
90
|
+
# Creates a new scheme
|
91
|
+
#
|
92
|
+
# @param [TrueClass, FalseClass] allow_empty if true allows to be empty, if false raises EmptyOutputError if empty
|
93
|
+
# @param [NilClass, Class] expected_type forces the type to be this type, if given
|
94
|
+
#
|
95
|
+
# @see MissingValidation
|
96
|
+
#
|
97
|
+
def initialize(allow_empty: false, expected_type: ::Object, current_type: nil, registry: nil, &block)
|
98
|
+
self.rules = Rules.new(allow_empty: allow_empty, expected_type: expected_type)
|
99
|
+
self.type_attributes = {}
|
100
|
+
self.fixtures = []
|
101
|
+
self.asserted_sane = false
|
102
|
+
@registry = registry
|
103
|
+
@current_type = current_type
|
104
|
+
|
105
|
+
instance_exec(&block) if block_given?
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_accessor :type_attributes, :fixtures
|
109
|
+
attr_reader :rules, :asserted_sane
|
110
|
+
|
111
|
+
alias asserted_sane? asserted_sane
|
112
|
+
|
113
|
+
##
|
114
|
+
# Checks if the +output+ is valid
|
115
|
+
#
|
116
|
+
# @param [#each] output the output to test against
|
117
|
+
# @param [Hash] opts the options as defined below
|
118
|
+
# @option exhaustive [TrueClass, FalseClass] opts if true, raises when it expected more data but there wasn't any
|
119
|
+
# @option strict [TrueClass, FalseClass] opts if true, raised when it did not expect more data, but there was more
|
120
|
+
#
|
121
|
+
# @return [TrueClass, FalseClass] true if valid, false otherwise
|
122
|
+
#
|
123
|
+
def valid?(output, **opts)
|
124
|
+
validate(output, **opts)
|
125
|
+
rescue ExhaustedOutputError
|
126
|
+
!opts.fetch(:exhaustive) { true }
|
127
|
+
rescue ValidationError
|
128
|
+
false
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Validates the +output+ and raises on certain validation errors
|
133
|
+
#
|
134
|
+
# @param [#each] output output to validate
|
135
|
+
# @option opts [TrueClass, FalseClass] exhaustive if true, the entire schema needs to be consumed
|
136
|
+
# @option opts [TrueClass, FalseClass] strict if true, no extra keys may be present in +output+
|
137
|
+
# @option opts[Array<String>] backtrace the current backtrace for error messages
|
138
|
+
#
|
139
|
+
# @raise ExhaustedOutputError
|
140
|
+
# @raise StrictValidationError
|
141
|
+
# @raise EmptyOutputError
|
142
|
+
# @raise CollectionTypeError
|
143
|
+
# @raise ValidationError
|
144
|
+
#
|
145
|
+
# @see #validate!
|
146
|
+
#
|
147
|
+
# @return [TrueClass]
|
148
|
+
#
|
149
|
+
def validate(output, options = nil, **opts)
|
150
|
+
options ||= ValidationOptions.new(**opts)
|
151
|
+
options.context = output
|
152
|
+
|
153
|
+
catch(:end) do
|
154
|
+
validate!(output, options, context: nil)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# @private
|
160
|
+
#
|
161
|
+
def validate!(output, call_options, **_opts)
|
162
|
+
OutputTypeGuard.call(output, call_options, rules: rules)
|
163
|
+
OutputEmptyGuard.call(output, call_options, rules: rules)
|
164
|
+
RulesExhaustedGuard.call(output, call_options, rules: rules)
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Adds an attribute to the schema
|
169
|
+
# If a +block+ is given, uses that to test against instead of +type+
|
170
|
+
#
|
171
|
+
# @param key [Symbol] the attribute name
|
172
|
+
# @param opts [Hash] options to pass to Scheme or Attribute
|
173
|
+
# @param type [Class, #===, Scheme] The type of the value, can be anything that responds to #===,
|
174
|
+
# or scheme to use if no +&block+ is given. Defaults to Object without a +&block+ and to Hash with a +&block+.
|
175
|
+
# or scheme to use if no +&block+ is given. Defaults to Object without a +&block+ and to Hash with a +&block+.
|
176
|
+
#
|
177
|
+
# @see Scheme::Attribute
|
178
|
+
# @see Scheme
|
179
|
+
#
|
180
|
+
# @example Add an attribute named foo, expecting a string
|
181
|
+
#
|
182
|
+
# class MyMedia
|
183
|
+
# include MediaTypes::Dsl
|
184
|
+
#
|
185
|
+
# validations do
|
186
|
+
# attribute :foo, String
|
187
|
+
# end
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# MyMedia.valid?({ foo: 'my-string' })
|
191
|
+
# # => true
|
192
|
+
#
|
193
|
+
# @example Add an attribute named foo, expecting nested scheme
|
194
|
+
#
|
195
|
+
# class MyMedia
|
196
|
+
# include MediaTypes::Dsl
|
197
|
+
#
|
198
|
+
# validations do
|
199
|
+
# attribute :foo do
|
200
|
+
# attribute :bar, String
|
201
|
+
# end
|
202
|
+
# end
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# MyMedia.valid?({ foo: { bar: 'my-string' }})
|
206
|
+
# # => true
|
207
|
+
#
|
208
|
+
def attribute(key, type = nil, optional: false, **opts, &block)
|
209
|
+
raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed attribute, either remove the type or the block' if type != ::Hash && block_given? && !type.nil?
|
210
|
+
|
211
|
+
type ||= ::Object
|
212
|
+
|
213
|
+
if block_given?
|
214
|
+
return collection(key, expected_type: ::Hash, optional: optional, **opts, &block)
|
215
|
+
end
|
216
|
+
|
217
|
+
if type.is_a?(Scheme)
|
218
|
+
return rules.add(key, type, optional: optional)
|
219
|
+
end
|
220
|
+
|
221
|
+
rules.add(key, Attribute.new(type, **opts, &block), optional: optional)
|
222
|
+
end
|
223
|
+
|
224
|
+
##
|
225
|
+
# Allow for any key.
|
226
|
+
# The +&block+ defines the Schema for each value.
|
227
|
+
#
|
228
|
+
# @param [Scheme, NilClass] scheme scheme to use if no +&block+ is given
|
229
|
+
# @param [TrueClass, FalseClass] allow_empty if true, empty (no key/value present) is allowed
|
230
|
+
# @param [Class] expected_type forces the validated object to have this type
|
231
|
+
#
|
232
|
+
# @see Scheme
|
233
|
+
#
|
234
|
+
# @example Add a collection named foo, expecting any key with a defined value
|
235
|
+
#
|
236
|
+
# class MyMedia
|
237
|
+
# include MediaTypes::Dsl
|
238
|
+
#
|
239
|
+
# validations do
|
240
|
+
# collection :foo do
|
241
|
+
# any do
|
242
|
+
# attribute :bar, String
|
243
|
+
# end
|
244
|
+
# end
|
245
|
+
# end
|
246
|
+
# end
|
247
|
+
#
|
248
|
+
# MyMedia.valid?({ foo: [{ anything: { bar: 'my-string' }, other_thing: { bar: 'other-string' } }] })
|
249
|
+
# # => true
|
250
|
+
#
|
251
|
+
# @example Any key, but all of them String or Numeric
|
252
|
+
#
|
253
|
+
# class MyMedia
|
254
|
+
# include MediaTypes::Dsl
|
255
|
+
#
|
256
|
+
# validations do
|
257
|
+
# any AnyOf(String, Numeric)
|
258
|
+
# end
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# MyMedia.valid?({ foo: 'my-string', bar: 42 })
|
262
|
+
# # => true
|
263
|
+
#
|
264
|
+
def any(scheme = nil, expected_type: ::Hash, allow_empty: false, &block)
|
265
|
+
raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed property, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?
|
266
|
+
|
267
|
+
unless block_given?
|
268
|
+
if scheme.is_a?(Scheme)
|
269
|
+
return rules.default = scheme
|
270
|
+
end
|
271
|
+
|
272
|
+
return rules.default = Attribute.new(scheme)
|
273
|
+
end
|
274
|
+
|
275
|
+
rules.default = Scheme.new(allow_empty: allow_empty, expected_type: expected_type, registry: @registry, current_type: @current_type, &block)
|
276
|
+
end
|
277
|
+
|
278
|
+
##
|
279
|
+
# Merges a +scheme+ into this scheme without changing the incoming +scheme+
|
280
|
+
#
|
281
|
+
# @param [Scheme] scheme the scheme to merge into this
|
282
|
+
#
|
283
|
+
def merge(scheme, &block)
|
284
|
+
self.rules = rules.merge(scheme.send(:rules))
|
285
|
+
instance_exec(&block) if block_given?
|
286
|
+
end
|
287
|
+
|
288
|
+
##
|
289
|
+
# Allow for extra keys in the schema/collection even when passing strict: true to #validate!
|
290
|
+
#
|
291
|
+
# @see Scheme::NotStrict
|
292
|
+
#
|
293
|
+
# @example Allow for extra keys in collection
|
294
|
+
#
|
295
|
+
# class MyMedia
|
296
|
+
# include MediaTypes::Dsl
|
297
|
+
#
|
298
|
+
# validations do
|
299
|
+
# collection :foo do
|
300
|
+
# attribute :required, String
|
301
|
+
# not_strict
|
302
|
+
# end
|
303
|
+
# end
|
304
|
+
# end
|
305
|
+
#
|
306
|
+
# MyMedia.valid?({ foo: [{ required: 'test', bar: 42 }] })
|
307
|
+
# # => true
|
308
|
+
#
|
309
|
+
def not_strict
|
310
|
+
rules.default = NotStrict.new
|
311
|
+
end
|
312
|
+
|
313
|
+
##
|
314
|
+
# Expect a collection such as an array or hash.
|
315
|
+
# The +block+ defines the Schema for each item in that collection.
|
316
|
+
#
|
317
|
+
# @param [Symbol] key key of the collection (same as #attribute)
|
318
|
+
# @param [NilClass, Scheme, Class] scheme scheme to use if no +&block+ is given, or type of each item in collection
|
319
|
+
# @param [TrueClass, FalseClass] allow_empty if true accepts 0 items in an enumerable
|
320
|
+
# @param [Class] expected_type forces the value of this collection to be this type, defaults to Array.
|
321
|
+
#
|
322
|
+
# @see Scheme
|
323
|
+
#
|
324
|
+
# @example Collection with an array of string
|
325
|
+
#
|
326
|
+
# class MyMedia
|
327
|
+
# include MediaTypes::Dsl
|
328
|
+
#
|
329
|
+
# validations do
|
330
|
+
# collection :foo, String
|
331
|
+
# end
|
332
|
+
# end
|
333
|
+
#
|
334
|
+
# MyMedia.valid?({ collection: ['foo', 'bar'] })
|
335
|
+
# # => true
|
336
|
+
#
|
337
|
+
# @example Collection with defined scheme
|
338
|
+
#
|
339
|
+
# class MyMedia
|
340
|
+
# include MediaTypes::Dsl
|
341
|
+
#
|
342
|
+
# validations do
|
343
|
+
# collection :foo do
|
344
|
+
# attribute :required, String
|
345
|
+
# attribute :number, Numeric
|
346
|
+
# end
|
347
|
+
# end
|
348
|
+
# end
|
349
|
+
#
|
350
|
+
# MyMedia.valid?({ foo: [{ required: 'test', number: 42 }, { required: 'other', number: 0 }] })
|
351
|
+
# # => true
|
352
|
+
#
|
353
|
+
def collection(key, scheme = nil, view: nil, allow_empty: false, expected_type: ::Array, optional: false, &block)
|
354
|
+
raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed collection, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?
|
355
|
+
|
356
|
+
unless block_given?
|
357
|
+
if scheme.nil?
|
358
|
+
dependent_key = @current_type.as_key.dup
|
359
|
+
dependent_key[1] = view
|
360
|
+
|
361
|
+
unless @registry.has_key? dependent_key
|
362
|
+
raise Errors::CollectionDefinitionNotFound.new(@current_type.override_suffix('json').to_s, @current_type.view(view).override_suffix('json').to_s)
|
363
|
+
end
|
364
|
+
scheme = @registry[dependent_key]
|
365
|
+
end
|
366
|
+
|
367
|
+
return rules.add(
|
368
|
+
key,
|
369
|
+
EnumerationOfType.new(
|
370
|
+
scheme,
|
371
|
+
enumeration_type: expected_type,
|
372
|
+
allow_empty: allow_empty
|
373
|
+
),
|
374
|
+
optional: optional
|
375
|
+
)
|
376
|
+
end
|
377
|
+
|
378
|
+
rules.add(key, Scheme.new(allow_empty: allow_empty, expected_type: expected_type, registry: @registry, current_type: @current_type, &block), optional: optional)
|
379
|
+
end
|
380
|
+
|
381
|
+
##
|
382
|
+
# Expect an index of links
|
383
|
+
#
|
384
|
+
def index(optional: false)
|
385
|
+
collection(:_links, optional: optional) do
|
386
|
+
link :_self
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
##
|
391
|
+
# Expect a link
|
392
|
+
#
|
393
|
+
# @see Scheme::Links
|
394
|
+
#
|
395
|
+
# @example Links as defined in HAL, JSON-Links and other specs
|
396
|
+
#
|
397
|
+
# class MyMedia
|
398
|
+
# include MediaTypes::Dsl
|
399
|
+
#
|
400
|
+
# validations do
|
401
|
+
# link :_self
|
402
|
+
# link :image
|
403
|
+
# end
|
404
|
+
# end
|
405
|
+
#
|
406
|
+
# MyMedia.valid?({ _links: { self: { href: 'https://example.org/s' }, image: { href: 'https://image.org/i' }} })
|
407
|
+
# # => true
|
408
|
+
#
|
409
|
+
# @example Link with extra attributes
|
410
|
+
#
|
411
|
+
# class MyMedia
|
412
|
+
# include MediaTypes::Dsl
|
413
|
+
#
|
414
|
+
# validations do
|
415
|
+
# link :image do
|
416
|
+
# attribute :templated, TrueClass
|
417
|
+
# end
|
418
|
+
# end
|
419
|
+
# end
|
420
|
+
#
|
421
|
+
# MyMedia.valid?({ _links: { image: { href: 'https://image.org/{md5}', templated: true }} })
|
422
|
+
# # => true
|
423
|
+
#
|
424
|
+
def link(*args, **opts, &block)
|
425
|
+
rules.fetch(:_links) do
|
426
|
+
Links.new.tap do |links|
|
427
|
+
rules.add(:_links, links)
|
428
|
+
end
|
429
|
+
end.link(*args, **opts, &block)
|
430
|
+
end
|
431
|
+
|
432
|
+
##
|
433
|
+
# Mark object as a valid empty object
|
434
|
+
#
|
435
|
+
# @example Empty object
|
436
|
+
#
|
437
|
+
# class MyMedia
|
438
|
+
# include MediaTypes::Dsl
|
439
|
+
#
|
440
|
+
# validations do
|
441
|
+
# empty
|
442
|
+
# end
|
443
|
+
# end
|
444
|
+
def empty
|
445
|
+
end
|
446
|
+
|
447
|
+
def inspect(indentation = 0)
|
448
|
+
tabs = ' ' * indentation
|
449
|
+
[
|
450
|
+
"#{tabs}[Scheme]",
|
451
|
+
rules.inspect(indentation + 1),
|
452
|
+
"#{tabs}[/Scheme]"
|
453
|
+
].join("\n")
|
454
|
+
end
|
455
|
+
|
456
|
+
def assert_pass(fixture, loose: false)
|
457
|
+
reduced_stack = remove_current_dir_from_stack(caller_locations)
|
458
|
+
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: true, loose: loose)
|
459
|
+
end
|
460
|
+
|
461
|
+
def assert_fail(fixture, loose: false)
|
462
|
+
reduced_stack = remove_current_dir_from_stack(caller_locations)
|
463
|
+
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: false, loose: loose)
|
464
|
+
end
|
465
|
+
|
466
|
+
# Removes all calls originating in current dir from given stack
|
467
|
+
# We need this so that we find out the caller of an assert_pass/fail in the caller_locations
|
468
|
+
# Which gets polluted by Scheme consecutively executing blocks within the validation blocks
|
469
|
+
def remove_current_dir_from_stack(stack)
|
470
|
+
stack.reject { |location| location.path.include?(__dir__) }
|
471
|
+
end
|
472
|
+
|
473
|
+
def validate_scheme_fixtures(expect_symbol_keys, backtrace)
|
474
|
+
@fixtures.map do |fixture_data|
|
475
|
+
begin
|
476
|
+
validate_fixture(fixture_data, expect_symbol_keys, backtrace)
|
477
|
+
nil
|
478
|
+
rescue UnexpectedValidationResultError => e
|
479
|
+
e
|
480
|
+
end
|
481
|
+
end.compact
|
482
|
+
end
|
483
|
+
|
484
|
+
def validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
|
485
|
+
@rules.flat_map do |key, rule|
|
486
|
+
next unless rule.is_a?(Scheme) || rule.is_a?(Links)
|
487
|
+
|
488
|
+
begin
|
489
|
+
rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
|
490
|
+
nil
|
491
|
+
rescue AssertionError => e
|
492
|
+
e.fixture_errors
|
493
|
+
end
|
494
|
+
end.compact
|
495
|
+
end
|
496
|
+
|
497
|
+
def validate_default_scheme_fixtures(expect_symbol_keys, backtrace)
|
498
|
+
return [] unless @rules.default.is_a?(Scheme)
|
499
|
+
|
500
|
+
@rules.default.run_fixture_validations(expect_symbol_keys, backtrace.dup.append('*'))
|
501
|
+
[]
|
502
|
+
rescue AssertionError => e
|
503
|
+
e.fixture_errors
|
504
|
+
end
|
505
|
+
|
506
|
+
def run_fixture_validations(expect_symbol_keys, backtrace = [])
|
507
|
+
fixture_errors = validate_scheme_fixtures(expect_symbol_keys, backtrace)
|
508
|
+
fixture_errors += validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
|
509
|
+
fixture_errors += validate_default_scheme_fixtures(expect_symbol_keys, backtrace)
|
510
|
+
|
511
|
+
raise AssertionError, fixture_errors unless fixture_errors.empty?
|
512
|
+
|
513
|
+
self.asserted_sane = true
|
514
|
+
end
|
515
|
+
|
516
|
+
def validate_fixture(fixture_data, expect_symbol_keys, backtrace = [])
|
517
|
+
json = JSON.parse(fixture_data.fixture, { symbolize_names: expect_symbol_keys })
|
518
|
+
expected_key_type = expect_symbol_keys ? Symbol : String
|
519
|
+
|
520
|
+
begin
|
521
|
+
validate(json, expected_key_type: expected_key_type, backtrace: backtrace, loose: fixture_data.loose)
|
522
|
+
unless fixture_data.expect_to_pass?
|
523
|
+
raise UnexpectedValidationResultError.new(fixture_data.caller, 'No error encounterd whilst expecting to')
|
524
|
+
end
|
525
|
+
rescue MediaTypes::Scheme::ValidationError => e
|
526
|
+
raise UnexpectedValidationResultError.new(fixture_data.caller, e) if fixture_data.expect_to_pass?
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
private
|
531
|
+
|
532
|
+
attr_writer :rules, :asserted_sane
|
533
|
+
|
534
|
+
end
|
535
|
+
end
|