media_types 2.3.0 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/debian.yml +43 -43
  3. data/.github/workflows/publish-bookworm.yml +34 -33
  4. data/.github/workflows/publish-sid.yml +34 -33
  5. data/.github/workflows/ruby.yml +22 -22
  6. data/.gitignore +20 -20
  7. data/.rubocop.yml +29 -29
  8. data/CHANGELOG.md +183 -175
  9. data/Gemfile +6 -6
  10. data/Gemfile.lock +43 -43
  11. data/LICENSE +21 -21
  12. data/README.md +666 -666
  13. data/Rakefile +12 -12
  14. data/bin/console +15 -15
  15. data/bin/setup +8 -8
  16. data/lib/media_types/constructable.rb +161 -161
  17. data/lib/media_types/dsl/errors.rb +18 -18
  18. data/lib/media_types/dsl.rb +172 -172
  19. data/lib/media_types/errors.rb +25 -25
  20. data/lib/media_types/formatter.rb +56 -56
  21. data/lib/media_types/hash.rb +21 -21
  22. data/lib/media_types/object.rb +35 -35
  23. data/lib/media_types/scheme/allow_nil.rb +30 -30
  24. data/lib/media_types/scheme/any_of.rb +41 -41
  25. data/lib/media_types/scheme/attribute.rb +46 -46
  26. data/lib/media_types/scheme/enumeration_context.rb +18 -18
  27. data/lib/media_types/scheme/enumeration_of_type.rb +80 -80
  28. data/lib/media_types/scheme/errors.rb +87 -87
  29. data/lib/media_types/scheme/links.rb +54 -54
  30. data/lib/media_types/scheme/missing_validation.rb +41 -41
  31. data/lib/media_types/scheme/not_strict.rb +15 -15
  32. data/lib/media_types/scheme/output_empty_guard.rb +45 -45
  33. data/lib/media_types/scheme/output_iterator_with_predicate.rb +66 -66
  34. data/lib/media_types/scheme/output_type_guard.rb +39 -39
  35. data/lib/media_types/scheme/rules.rb +186 -186
  36. data/lib/media_types/scheme/rules_exhausted_guard.rb +75 -73
  37. data/lib/media_types/scheme/validation_options.rb +44 -44
  38. data/lib/media_types/scheme.rb +535 -535
  39. data/lib/media_types/testing/assertions.rb +20 -20
  40. data/lib/media_types/validations.rb +118 -118
  41. data/lib/media_types/version.rb +5 -5
  42. data/lib/media_types/views.rb +12 -12
  43. data/lib/media_types.rb +73 -73
  44. data/media_types.gemspec +33 -33
  45. metadata +6 -6
data/README.md CHANGED
@@ -1,666 +1,666 @@
1
- # MediaTypes
2
- [![Build Status](https://github.com/SleeplessByte/media-types-ruby/workflows/Ruby/badge.svg?branch=master)](https://github.com/SleeplessByte/media-types-ruby/actions?query=workflow%3ARuby)
3
- [![Gem Version](https://badge.fury.io/rb/media_types.svg)](https://badge.fury.io/rb/media_types)
4
- [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
5
- [![Maintainability](https://api.codeclimate.com/v1/badges/6f2dc1fb37ecb98c4363/maintainability)](https://codeclimate.com/github/SleeplessByte/media-types-ruby/maintainability)
6
-
7
- Media Types based on scheme, with versioning, views, suffixes and validations.
8
-
9
- This library makes it easy to define schemas that can be used to validate JSON objects based on their Content-Type.
10
-
11
- ## Installation
12
-
13
- Add this line to your application's Gemfile:
14
-
15
- ```ruby
16
- gem 'media_types'
17
- ```
18
-
19
- And then execute:
20
-
21
- $ bundle
22
-
23
- Or install it yourself as:
24
-
25
- $ gem install media_types
26
-
27
- ## Usage
28
-
29
- Define a validation:
30
-
31
- ```ruby
32
- require 'media_types'
33
-
34
- module Acme
35
- MediaTypes::set_organisation Acme, 'acme'
36
-
37
- class FooValidator
38
- include MediaTypes::Dsl
39
-
40
- use_name 'foo'
41
-
42
- validations do
43
- attribute :foo, String
44
- end
45
- end
46
- end
47
- ```
48
-
49
- Validate an object:
50
-
51
- ```ruby
52
- Acme::FooValidator.validate!({ foo: 'bar' })
53
- ```
54
-
55
- ## Full example
56
-
57
- ```Ruby
58
- require 'media_types'
59
-
60
- class Venue
61
- include MediaTypes::Dsl
62
-
63
- def self.organisation
64
- 'mydomain'
65
- end
66
-
67
- use_name 'venue'
68
-
69
- validations do
70
- version 2 do
71
- attribute :name, String
72
- collection :location do
73
- attribute :latitude, Numeric
74
- attribute :longitude, Numeric
75
- attribute :altitude, AllowNil(Numeric)
76
- end
77
-
78
- link :self
79
- link :route, allow_nil: true
80
- end
81
-
82
- version 1 do
83
- attribute :name, String
84
- attribute :coords, String, optional: :loose
85
- attribute :updated_at, String
86
-
87
- link :self
88
- end
89
-
90
- view 'create' do
91
- collection :location do
92
- attribute :latitude, Numeric
93
- attribute :longitude, Numeric
94
- attribute :altitude, AllowNil(Numeric)
95
- end
96
-
97
- versions [1, 2] do |v|
98
- collection :location do
99
- link :extra if v > 1
100
-
101
- attribute :latitude, Numeric
102
- attribute :longitude, Numeric
103
- attribute :altitude, AllowNil(Numeric)
104
- end
105
- end
106
- end
107
- end
108
- end
109
- ```
110
-
111
- ## Schema Definitions
112
-
113
- If you include 'MediaTypes::Dsl' in your class you can use the following functions within a `validation do` block to define your schema:
114
-
115
- ### `attribute`
116
-
117
- Adds an attribute to the schema, if a +block+ is given, uses that to test against instead of +type+
118
-
119
- | param | type | description |
120
- | --------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
- | key | `Symbol` | the attribute name |
122
- | opts | `Hash` | options to pass to `Scheme` or `Attribute` |
123
- | type | `Class`, `===`, Scheme | The type of the value can be anything that responds to `===`, or scheme to use if no `&block` is given. Defaults to `Object` without a `&block` and to Hash with a `&block`. |
124
- | optional: | `TrueClass`, `FalseClass` | if true, key may be absent, defaults to `false` |
125
- | &block | `Block` | defines the scheme of the value of this attribute |
126
-
127
- #### Add an attribute named foo, expecting a string
128
- ```Ruby
129
- require 'media_types'
130
-
131
- class MyMedia
132
- include MediaTypes::Dsl
133
-
134
- validations do
135
- attribute :foo, String
136
- end
137
- end
138
-
139
- MyMedia.valid?({ foo: 'my-string' })
140
- # => true
141
- ```
142
-
143
- #### Add an attribute named foo, expecting nested scheme
144
-
145
- ```Ruby
146
- class MyMedia
147
- include MediaTypes::Dsl
148
-
149
- validations do
150
- attribute :foo do
151
- attribute :bar, String
152
- end
153
- end
154
- end
155
-
156
- MyMedia.valid?({ foo: { bar: 'my-string' }})
157
- # => true
158
- ```
159
-
160
- ### `any`
161
- Allow for any key. The `&block` defines the Schema for each value.
162
-
163
- | param | type | description |
164
- | -------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
165
- | scheme | `Scheme`, `NilClass` | scheme to use if no `&block` is given |
166
- | allow_empty: | `TrueClass`, `FalsClass` | if true, empty (no key/value present) is allowed |
167
- | expected_type: | `Class`, | forces the validated value to have this type, defaults to `Hash`. Use `Object` if either `Hash` or `Array` is fine |
168
- | &block | `Block` | defines the scheme of the value of this attribute |
169
-
170
- #### Add a collection named foo, expecting any key with a defined value
171
- ```Ruby
172
- class MyMedia
173
- include MediaTypes::Dsl
174
-
175
- validations do
176
- collection :foo do
177
- any do
178
- attribute :bar, String
179
- end
180
- end
181
- end
182
- end
183
-
184
- MyMedia.valid?({ foo: [{ anything: { bar: 'my-string' }, other_thing: { bar: 'other-string' } }] })
185
- # => true
186
- ````
187
-
188
- ### `not_strict`
189
- Allow for extra keys in the schema/collection even when passing `strict: true` to `#validate!`
190
-
191
- #### Allow for extra keys in collection
192
-
193
- ```Ruby
194
- class MyMedia
195
- include MediaTypes::Dsl
196
-
197
- validations do
198
- collection :foo do
199
- attribute :required, String
200
- not_strict
201
- end
202
- end
203
- end
204
-
205
- MyMedia.valid?({ foo: [{ required: 'test', bar: 42 }] })
206
- # => true
207
- ```
208
-
209
- ### `collection`
210
- Expect a collection such as an array or hash. The `&block` defines the Schema for each item in that collection.
211
-
212
- | param | type | description |
213
- | -------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
214
- | key | `Symbol` | key of the collection (same as `#attribute`) |
215
- | scheme | `Scheme`, `NilClass`, `Class` | scheme to use if no `&block` is given or `Class` of each item in the collection |
216
- | allow_empty: | `TrueClass`, `FalseClass` | if true, empty (no key/value present) is allowed |
217
- | expected_type: | `Class`, | forces the validated value to have this type, defaults to `Array`. Use `Object` if either `Array` or `Hash` is fine. |
218
- | optional: | `TrueClass`, `FalseClass` | if true, key may be absent, defaults to `false` |
219
- | &block | `Block` | defines the scheme of the value of this attribute |
220
-
221
-
222
- #### Collection with an array of string
223
- ```Ruby
224
- class MyMedia
225
- include MediaTypes::Dsl
226
-
227
- validations do
228
- collection :foo, String
229
- end
230
- end
231
-
232
- MyMedia.valid?({ collection: ['foo', 'bar'] })
233
- # => true
234
- ```
235
-
236
- #### Collection with defined scheme
237
-
238
- ```Ruby
239
- class MyMedia
240
- include MediaTypes::Dsl
241
-
242
- validations do
243
- collection :foo do
244
- attribute :required, String
245
- attribute :number, Numeric
246
- end
247
- end
248
- end
249
-
250
- MyMedia.valid?({ foo: [{ required: 'test', number: 42 }, { required: 'other', number: 0 }] })
251
- # => true
252
- ```
253
-
254
- ### `link`
255
-
256
- Expect a link with a required `href: String` attribute
257
-
258
- | param | type | description |
259
- | ---------- | ------------------------- | -------------------------------------------------------------------------------------- |
260
- | key | `Symbol` | key of the link (same as `#attribute`) |
261
- | allow_nil: | `TrueClass`, `FalseClass` | if true, value may be nil |
262
- | optional: | `TrueClass`, `FalseClass` | if true, key may be absent, defaults to `false` |
263
- | &block | `Block` | defines the scheme of the value of this attribute, in addition to the `href` attribute |
264
-
265
- #### Links as defined in HAL, JSON-Links and other specs
266
- ```Ruby
267
- class MyMedia
268
- include MediaTypes::Dsl
269
-
270
- validations do
271
- link :self
272
- link :image
273
- end
274
- end
275
-
276
- MyMedia.valid?({ _links: { self: { href: 'https://example.org/s' }, image: { href: 'https://image.org/i' }} })
277
- # => true
278
- ```
279
-
280
- #### Link with extra attributes
281
- ```Ruby
282
- class MyMedia
283
- include MediaTypes::Dsl
284
-
285
- validations do
286
- link :image do
287
- attribute :templated, TrueClass
288
- end
289
- end
290
- end
291
-
292
- MyMedia.valid?({ _links: { image: { href: 'https://image.org/{md5}', templated: true }} })
293
- # => true
294
- ```
295
-
296
- ## Validation
297
- If your type has a validations, you can now use this media type for validation:
298
-
299
- ```Ruby
300
- Venue.valid?({
301
- #...
302
- })
303
- # => true if valid, false otherwise
304
-
305
- Venue.validate!({
306
- # /*...*/
307
- })
308
- # => raises if it's not valid
309
- ```
310
-
311
- If an array is passed, check the scheme for each value, unless the scheme is defined as expecting a hash:
312
- ```Ruby
313
- expected_hash = Scheme.new(expected_type: Hash) { attribute(:foo) }
314
- expected_object = Scheme.new { attribute(:foo) }
315
-
316
- expected_hash.valid?({ foo: 'string' })
317
- # => true
318
-
319
- expected_hash.valid?([{ foo: 'string' }])
320
- # => false
321
-
322
-
323
- expected_object.valid?({ foo: 'string' })
324
- # => true
325
-
326
- expected_object.valid?([{ foo: 'string' }])
327
- # => true
328
- ```
329
-
330
- ## Formatting for headers
331
- Any media type object can be converted in valid string to be used with `Content-Type` or `Accept`:
332
-
333
- ```Ruby
334
- Venue.mime_type.identifier
335
- # => "application/vnd.mydomain.venue.v2+json"
336
-
337
- Venue.mime_type.version(1).identifier
338
- # => "application/vnd.mydomain.venue.v1+json"
339
-
340
- Venue.mime_type.to_s(0.2)
341
- # => "application/vnd.mydomain.venue.v2+json; q=0.2"
342
-
343
- Venue.mime_type.collection.identifier
344
- # => "application/vnd.mydomain.venue.v2.collection+json"
345
-
346
- Venue.mime_type.view('active').identifier
347
- # => "application/vnd.mydomain.venue.v2.active+json"
348
- ```
349
-
350
- ## API
351
-
352
- A defined schema has the following functions available:
353
-
354
- ### `valid?`
355
-
356
- Example: `Venue.valid?({ foo: 'bar' })`
357
-
358
- Allows passing in validation options as a second parameter.
359
-
360
- ### `validate!`
361
-
362
- Example: `Venue.validate!({ foo: 'bar' })`
363
-
364
- Allows passing in validation options as a second parameter.
365
-
366
- ### `validatable?`
367
-
368
- Example: `Venue.version(42).validatable?`
369
-
370
- Tests whether the current configuration of the schema has a validation defined.
371
-
372
- ### `register`
373
-
374
- Example: `Venue.register`
375
-
376
- Registers the media type to the registry.
377
-
378
- ### `view`
379
-
380
- Example: `Venue.view('create')`
381
-
382
- Returns a schema validator configured with the specified view.
383
-
384
- ### `version`
385
-
386
- Example: `Venue.version(42)`
387
-
388
- Returns a schema validator configured with the specified version.
389
-
390
- ### `suffix`
391
-
392
- Example: `Venue.suffix(:json)`
393
-
394
- Returns a schema validator configured with the specified suffix.
395
-
396
- ### `identifier`
397
-
398
- Example: `Venue.version(2).identifier` (returns `'application/vnd.application.venue.v2'`)
399
-
400
- Returns the IANA compatible [Media Type Identifier](https://en.wikipedia.org/wiki/Media_type) for the configured schema.
401
-
402
- ### `available_validations`
403
-
404
- Example: `Venue.available_validations`
405
-
406
- Returns a list of all the schemas that are defined.
407
-
408
- ## Ensuring Your MediaTypes Work
409
-
410
- ### Overview & Rationale
411
-
412
- If the MediaTypes you create enforce a specification you _do not expect them to_, it will cause problems that will be very difficult to fix, as other code, which utilises your MediaType, would break when you change the specification. This is because the faulty MediaType definition will start to make other code dependent on the specification it defines. For example, consider what would happen if you release a MediaType which defines an attribute `foo` to be a `String`, and run a server which defines such a specification. Later, you realise you _actually_ wanted `foo` to be `Numeric`. What can you do?
413
-
414
- Well, during this time, other people started to write code which conformed to the specification defined by the faulty MediaType. So, it's going to be extremely difficult to revert your mistake. For this reason, it is vital that, when using this library, your MediaTypes define the _correct_ specification.
415
-
416
- To this end, we provide you with a few avenues to check whether MediaTypes define the specifications you actually intend by checking examples of JSON you expect to be compliant/non-compliant with the MediaType definitions you write out.
417
-
418
- These are as follows:
419
-
420
- 1. The library provides [two methods](README.md#media-type-checking-in-test-suites) (`assert_pass` and `assert_fail`), which allow specifying JSON fixtures that are compliant (`assert_pass`) or non-compliant (`assert_fail`).
421
- 2. The library provides a way to validate those fixtures against the MediaType specification with the [`assert_mediatype`](README.md#media-type-checking-in-test-suites) method.
422
- 3. The library automatically performs a MediaType's checks defined by (1) the first time an object is validated against the MediaType, and throws an error if any of the checks fail.
423
- 4. The library provides a way to run the checks carried out by (3) on load, using the method [`assert_sane!`](README.md#validation-checks) so that an application will not run if any of the MediaType's checks don't pass.
424
-
425
- These four options are examined in detail below.
426
-
427
- ### MediaType Checking in Test Suites
428
-
429
- The library provides the `assert_mediatype` method, which allows running the checks for a particular `MediaType` within Minitest with `assert_pass` and `assert_fail`.
430
- If you are using Minitest, you can make `assert_mediatype` available by calling `include MediaTypes::Testing::Assertions` in the test class (e.g. `Minitest::Runnable`):
431
-
432
- ```ruby
433
- module Minitest
434
- class Test < Minitest::Runnable
435
- include MediaTypes::Testing::Assertions
436
- end
437
- end
438
- ```
439
-
440
- The example below demonstrates how to use `assert_pass` and `assert_fail` within a MediaType, and how to use the `assert_mediatype` method in MiniTest tests to validate them.
441
-
442
- ```ruby
443
- class MyMedia
444
- include MediaTypes::Dsl
445
-
446
- def self.organisation
447
- 'acme'
448
- end
449
-
450
- use_name 'test'
451
-
452
- validations do
453
- # Using "any Numeric" this MediaType doesn't care what key names you use.
454
- # However, it does care that those keys point to a Numeric value.
455
- any Numeric
456
-
457
- assert_pass '{"foo": 42}'
458
- assert_pass <<-FIXTURE
459
- { "foo": 42, "bar": 43 }
460
- FIXTURE
461
-
462
- # The keyword "any" means there are no required keys, so having no keys should also pass.
463
- assert_pass '{}'
464
-
465
- # This MediaType should not accept anything other then a Numeric value.
466
- assert_fail <<-FIXTURE
467
- { "foo": { "bar": "string" } }
468
- FIXTURE
469
- assert_fail '{"foo": {}}'
470
- assert_fail '{"foo": null}', loose: true
471
- assert_fail '{"foo": [42]}', loose: false
472
- end
473
- end
474
-
475
- class MyMediaTest < Minitest::Test
476
- include MediaTypes::Testing::Assertions
477
-
478
- def test_mediatype_specification
479
- assert_mediatype MyMedia
480
- end
481
- end
482
-
483
- class MyMediaTest < Minitest::Test
484
- include MediaTypes::Testing::Assertions
485
-
486
- def test_mediatype_specification
487
- assert_mediatype MyMedia
488
- end
489
- end
490
-
491
- ```
492
-
493
- ### Testing Without Minitest
494
-
495
- If you are using another testing framework, you will not be able to use the `assert_mediatype` method. Instead, you can test your MediaTypes by using the `assert_sane!` method (documented below) and rescuing the errors it will throw when it fails. The snippet below shows an example adaptation for MiniTest, which you can use as a guide.
496
-
497
- ```ruby
498
- def test_mediatype(mediatype)
499
- mediatype.assert_sane!
500
- assert mediatype.media_type_validations.scheme.asserted_sane?
501
- rescue MediaTypes::AssertionError => e
502
- flunk e.message
503
- end
504
- end
505
- ```
506
-
507
- ### Validation Checks
508
-
509
- The `assert_pass` and `assert_fail` methods take a JSON string (as shown below). The first time the `validate!` method is called on a MediaType, the assertions for that media type are run.
510
- This is done as a last line of defence against introducing faulty MediaTypes into your software. Ideally, you want to carry out these checks on load rather than on a running application. This functionality is provided by the `assert_sane!` method, which can be called on a particular MediaType:
511
-
512
- ```ruby
513
- MyMedia.assert_sane!
514
- # true
515
- ```
516
-
517
- ### Intermediate Checks
518
-
519
- The fixtures provided to the `assert_pass` and `assert_fail` methods are evaluated within the context of the block they are placed in. It's therefore possible to write a test for a (complex) optional attribute, without that test cluttering the fixtures for the entire mediatype.
520
-
521
- ```ruby
522
- class MyMedia
523
- include MediaTypes::Dsl
524
-
525
- expect_string_keys
526
-
527
- def self.organisation
528
- 'acme'
529
- end
530
-
531
- use_name 'test'
532
-
533
- validations do
534
- attribute :foo, Hash, optional: true do
535
- attribute :bar, Numeric
536
-
537
- # This passes, since in this context the "bar" key is required to have a Numeric value.
538
- assert_pass '{"bar": 42}'
539
- end
540
- attribute :rep, Numeric
541
-
542
- # This passes, since the attribute "foo" is optional.
543
- assert_pass '{"rep": 42}'
544
- end
545
- end
546
- ```
547
-
548
- ## Key Type Validation
549
-
550
- When interacting with Ruby objects defined by your MediaType, you want to avoid getting `nil` values, just because the the wrong key type is being used (e.g. `obj['foo']` instead of `obj[:foo]`).
551
- To this end, the library provides the ability to specify the expected type of keys in a MediaType; by default symbol keys are expected.
552
-
553
- ### Setting Key Type Expectations
554
-
555
- Key type expectations can be set at the module level. Each MediaType within this module will inherit the expectation set by that module.
556
-
557
- ```ruby
558
- module Acme
559
- MediaTypes.expect_string_keys(self)
560
-
561
- # The MyMedia class expects string keys, as inherited from the Acme module.
562
- class MyMedia
563
- include MediaTypes::Dsl
564
-
565
- def self.organisation
566
- 'acme'
567
- end
568
-
569
- use_name 'test'
570
-
571
- validations do
572
- any Numeric
573
- end
574
- end
575
- end
576
- ```
577
-
578
- If you validate an object with a different key type than expected, an error will be thrown:
579
-
580
- ```ruby
581
- Acme::MyMedia.validate! { "something": 42 }
582
- # => passes, because all keys are a string
583
-
584
- Acme::MyMedia.validate! { something: 42 }
585
- # => throws a ValidationError , because 'something' is a symbol key
586
- ```
587
-
588
- ## Overriding Key Type Expectations
589
-
590
- A key type expectation set by a Module can be overridden by calling either `expect_symbol_keys` or `expect_string_keys` inside the MediaType class.
591
-
592
- ```ruby
593
- module Acme
594
- MediaTypes.expect_string_keys(self)
595
-
596
- class MyOverridingMedia
597
- include MediaTypes::Dsl
598
-
599
- def self.organisation
600
- 'acme'
601
- end
602
-
603
- use_name 'test'
604
-
605
- # Expect keys to be symbols
606
- expect_symbol_keys
607
-
608
- validations do
609
- any Numeric
610
- end
611
- end
612
- end
613
- ```
614
-
615
- Now the MediaType throws an error when string keys are used.
616
-
617
- ```ruby
618
- Acme::MyOverridingMedia.validate! { something: 42 }
619
- # => passes, because all keys are a symbol
620
-
621
- Acme::MyOverridingMedia.validate! { "something": 42 }
622
- # => throws a ValidationError , because 'something' is a string key
623
- ```
624
-
625
- ### Setting The JSON Parser With The Wrong Key Type
626
-
627
- If you parse JSON with the wrong key type, as shown below, the resultant object will fail the validations.
628
-
629
- ```ruby
630
- class MyMedia
631
- include MediaTypes::Dsl
632
-
633
- def self.organisation
634
- 'acme'
635
- end
636
-
637
- use_name 'test'
638
-
639
- # Expect keys to be symbols
640
- expect_symbol_keys
641
-
642
- validations do
643
- any Numeric
644
- end
645
- end
646
-
647
- json = JSON.parse('{"foo": {}}', { symbolize_names: false })
648
- # If MyMedia expects symbol keys
649
- MyMedia.valid?(json)
650
- # Returns false
651
- ```
652
-
653
- ## Related
654
-
655
- - [`MediaTypes::Serialization`](https://github.com/XPBytes/media_types-serialization): :cyclone: Add media types supported serialization to Rails.
656
-
657
- ## Development
658
-
659
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
660
-
661
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, call `bundle exec rake release` to create a new git tag, push git commits and tags, and
662
- push the `.gem` file to rubygems.org.
663
-
664
- ## Contributing
665
-
666
- Bug reports and pull requests are welcome on GitHub at [SleeplessByte/media-types-ruby](https://github.com/SleeplessByte/media-types-ruby)
1
+ # MediaTypes
2
+ [![Build Status](https://github.com/SleeplessByte/media-types-ruby/workflows/Ruby/badge.svg?branch=master)](https://github.com/SleeplessByte/media-types-ruby/actions?query=workflow%3ARuby)
3
+ [![Gem Version](https://badge.fury.io/rb/media_types.svg)](https://badge.fury.io/rb/media_types)
4
+ [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/6f2dc1fb37ecb98c4363/maintainability)](https://codeclimate.com/github/SleeplessByte/media-types-ruby/maintainability)
6
+
7
+ Media Types based on scheme, with versioning, views, suffixes and validations.
8
+
9
+ This library makes it easy to define schemas that can be used to validate JSON objects based on their Content-Type.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'media_types'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install media_types
26
+
27
+ ## Usage
28
+
29
+ Define a validation:
30
+
31
+ ```ruby
32
+ require 'media_types'
33
+
34
+ module Acme
35
+ MediaTypes::set_organisation Acme, 'acme'
36
+
37
+ class FooValidator
38
+ include MediaTypes::Dsl
39
+
40
+ use_name 'foo'
41
+
42
+ validations do
43
+ attribute :foo, String
44
+ end
45
+ end
46
+ end
47
+ ```
48
+
49
+ Validate an object:
50
+
51
+ ```ruby
52
+ Acme::FooValidator.validate!({ foo: 'bar' })
53
+ ```
54
+
55
+ ## Full example
56
+
57
+ ```Ruby
58
+ require 'media_types'
59
+
60
+ class Venue
61
+ include MediaTypes::Dsl
62
+
63
+ def self.organisation
64
+ 'mydomain'
65
+ end
66
+
67
+ use_name 'venue'
68
+
69
+ validations do
70
+ version 2 do
71
+ attribute :name, String
72
+ collection :location do
73
+ attribute :latitude, Numeric
74
+ attribute :longitude, Numeric
75
+ attribute :altitude, AllowNil(Numeric)
76
+ end
77
+
78
+ link :self
79
+ link :route, allow_nil: true
80
+ end
81
+
82
+ version 1 do
83
+ attribute :name, String
84
+ attribute :coords, String, optional: :loose
85
+ attribute :updated_at, String
86
+
87
+ link :self
88
+ end
89
+
90
+ view 'create' do
91
+ collection :location do
92
+ attribute :latitude, Numeric
93
+ attribute :longitude, Numeric
94
+ attribute :altitude, AllowNil(Numeric)
95
+ end
96
+
97
+ versions [1, 2] do |v|
98
+ collection :location do
99
+ link :extra if v > 1
100
+
101
+ attribute :latitude, Numeric
102
+ attribute :longitude, Numeric
103
+ attribute :altitude, AllowNil(Numeric)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ ```
110
+
111
+ ## Schema Definitions
112
+
113
+ If you include 'MediaTypes::Dsl' in your class you can use the following functions within a `validation do` block to define your schema:
114
+
115
+ ### `attribute`
116
+
117
+ Adds an attribute to the schema, if a +block+ is given, uses that to test against instead of +type+
118
+
119
+ | param | type | description |
120
+ | --------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
+ | key | `Symbol` | the attribute name |
122
+ | opts | `Hash` | options to pass to `Scheme` or `Attribute` |
123
+ | type | `Class`, `===`, Scheme | The type of the value can be anything that responds to `===`, or scheme to use if no `&block` is given. Defaults to `Object` without a `&block` and to Hash with a `&block`. |
124
+ | optional: | `TrueClass`, `FalseClass` | if true, key may be absent, defaults to `false` |
125
+ | &block | `Block` | defines the scheme of the value of this attribute |
126
+
127
+ #### Add an attribute named foo, expecting a string
128
+ ```Ruby
129
+ require 'media_types'
130
+
131
+ class MyMedia
132
+ include MediaTypes::Dsl
133
+
134
+ validations do
135
+ attribute :foo, String
136
+ end
137
+ end
138
+
139
+ MyMedia.valid?({ foo: 'my-string' })
140
+ # => true
141
+ ```
142
+
143
+ #### Add an attribute named foo, expecting nested scheme
144
+
145
+ ```Ruby
146
+ class MyMedia
147
+ include MediaTypes::Dsl
148
+
149
+ validations do
150
+ attribute :foo do
151
+ attribute :bar, String
152
+ end
153
+ end
154
+ end
155
+
156
+ MyMedia.valid?({ foo: { bar: 'my-string' }})
157
+ # => true
158
+ ```
159
+
160
+ ### `any`
161
+ Allow for any key. The `&block` defines the Schema for each value.
162
+
163
+ | param | type | description |
164
+ | -------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
165
+ | scheme | `Scheme`, `NilClass` | scheme to use if no `&block` is given |
166
+ | allow_empty: | `TrueClass`, `FalsClass` | if true, empty (no key/value present) is allowed |
167
+ | expected_type: | `Class`, | forces the validated value to have this type, defaults to `Hash`. Use `Object` if either `Hash` or `Array` is fine |
168
+ | &block | `Block` | defines the scheme of the value of this attribute |
169
+
170
+ #### Add a collection named foo, expecting any key with a defined value
171
+ ```Ruby
172
+ class MyMedia
173
+ include MediaTypes::Dsl
174
+
175
+ validations do
176
+ collection :foo do
177
+ any do
178
+ attribute :bar, String
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ MyMedia.valid?({ foo: [{ anything: { bar: 'my-string' }, other_thing: { bar: 'other-string' } }] })
185
+ # => true
186
+ ````
187
+
188
+ ### `not_strict`
189
+ Allow for extra keys in the schema/collection even when passing `strict: true` to `#validate!`
190
+
191
+ #### Allow for extra keys in collection
192
+
193
+ ```Ruby
194
+ class MyMedia
195
+ include MediaTypes::Dsl
196
+
197
+ validations do
198
+ collection :foo do
199
+ attribute :required, String
200
+ not_strict
201
+ end
202
+ end
203
+ end
204
+
205
+ MyMedia.valid?({ foo: [{ required: 'test', bar: 42 }] })
206
+ # => true
207
+ ```
208
+
209
+ ### `collection`
210
+ Expect a collection such as an array or hash. The `&block` defines the Schema for each item in that collection.
211
+
212
+ | param | type | description |
213
+ | -------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
214
+ | key | `Symbol` | key of the collection (same as `#attribute`) |
215
+ | scheme | `Scheme`, `NilClass`, `Class` | scheme to use if no `&block` is given or `Class` of each item in the collection |
216
+ | allow_empty: | `TrueClass`, `FalseClass` | if true, empty (no key/value present) is allowed |
217
+ | expected_type: | `Class`, | forces the validated value to have this type, defaults to `Array`. Use `Object` if either `Array` or `Hash` is fine. |
218
+ | optional: | `TrueClass`, `FalseClass` | if true, key may be absent, defaults to `false` |
219
+ | &block | `Block` | defines the scheme of the value of this attribute |
220
+
221
+
222
+ #### Collection with an array of string
223
+ ```Ruby
224
+ class MyMedia
225
+ include MediaTypes::Dsl
226
+
227
+ validations do
228
+ collection :foo, String
229
+ end
230
+ end
231
+
232
+ MyMedia.valid?({ collection: ['foo', 'bar'] })
233
+ # => true
234
+ ```
235
+
236
+ #### Collection with defined scheme
237
+
238
+ ```Ruby
239
+ class MyMedia
240
+ include MediaTypes::Dsl
241
+
242
+ validations do
243
+ collection :foo do
244
+ attribute :required, String
245
+ attribute :number, Numeric
246
+ end
247
+ end
248
+ end
249
+
250
+ MyMedia.valid?({ foo: [{ required: 'test', number: 42 }, { required: 'other', number: 0 }] })
251
+ # => true
252
+ ```
253
+
254
+ ### `link`
255
+
256
+ Expect a link with a required `href: String` attribute
257
+
258
+ | param | type | description |
259
+ | ---------- | ------------------------- | -------------------------------------------------------------------------------------- |
260
+ | key | `Symbol` | key of the link (same as `#attribute`) |
261
+ | allow_nil: | `TrueClass`, `FalseClass` | if true, value may be nil |
262
+ | optional: | `TrueClass`, `FalseClass` | if true, key may be absent, defaults to `false` |
263
+ | &block | `Block` | defines the scheme of the value of this attribute, in addition to the `href` attribute |
264
+
265
+ #### Links as defined in HAL, JSON-Links and other specs
266
+ ```Ruby
267
+ class MyMedia
268
+ include MediaTypes::Dsl
269
+
270
+ validations do
271
+ link :self
272
+ link :image
273
+ end
274
+ end
275
+
276
+ MyMedia.valid?({ _links: { self: { href: 'https://example.org/s' }, image: { href: 'https://image.org/i' }} })
277
+ # => true
278
+ ```
279
+
280
+ #### Link with extra attributes
281
+ ```Ruby
282
+ class MyMedia
283
+ include MediaTypes::Dsl
284
+
285
+ validations do
286
+ link :image do
287
+ attribute :templated, TrueClass
288
+ end
289
+ end
290
+ end
291
+
292
+ MyMedia.valid?({ _links: { image: { href: 'https://image.org/{md5}', templated: true }} })
293
+ # => true
294
+ ```
295
+
296
+ ## Validation
297
+ If your type has a validations, you can now use this media type for validation:
298
+
299
+ ```Ruby
300
+ Venue.valid?({
301
+ #...
302
+ })
303
+ # => true if valid, false otherwise
304
+
305
+ Venue.validate!({
306
+ # /*...*/
307
+ })
308
+ # => raises if it's not valid
309
+ ```
310
+
311
+ If an array is passed, check the scheme for each value, unless the scheme is defined as expecting a hash:
312
+ ```Ruby
313
+ expected_hash = Scheme.new(expected_type: Hash) { attribute(:foo) }
314
+ expected_object = Scheme.new { attribute(:foo) }
315
+
316
+ expected_hash.valid?({ foo: 'string' })
317
+ # => true
318
+
319
+ expected_hash.valid?([{ foo: 'string' }])
320
+ # => false
321
+
322
+
323
+ expected_object.valid?({ foo: 'string' })
324
+ # => true
325
+
326
+ expected_object.valid?([{ foo: 'string' }])
327
+ # => true
328
+ ```
329
+
330
+ ## Formatting for headers
331
+ Any media type object can be converted in valid string to be used with `Content-Type` or `Accept`:
332
+
333
+ ```Ruby
334
+ Venue.mime_type.identifier
335
+ # => "application/vnd.mydomain.venue.v2+json"
336
+
337
+ Venue.mime_type.version(1).identifier
338
+ # => "application/vnd.mydomain.venue.v1+json"
339
+
340
+ Venue.mime_type.to_s(0.2)
341
+ # => "application/vnd.mydomain.venue.v2+json; q=0.2"
342
+
343
+ Venue.mime_type.collection.identifier
344
+ # => "application/vnd.mydomain.venue.v2.collection+json"
345
+
346
+ Venue.mime_type.view('active').identifier
347
+ # => "application/vnd.mydomain.venue.v2.active+json"
348
+ ```
349
+
350
+ ## API
351
+
352
+ A defined schema has the following functions available:
353
+
354
+ ### `valid?`
355
+
356
+ Example: `Venue.valid?({ foo: 'bar' })`
357
+
358
+ Allows passing in validation options as a second parameter.
359
+
360
+ ### `validate!`
361
+
362
+ Example: `Venue.validate!({ foo: 'bar' })`
363
+
364
+ Allows passing in validation options as a second parameter.
365
+
366
+ ### `validatable?`
367
+
368
+ Example: `Venue.version(42).validatable?`
369
+
370
+ Tests whether the current configuration of the schema has a validation defined.
371
+
372
+ ### `register`
373
+
374
+ Example: `Venue.register`
375
+
376
+ Registers the media type to the registry.
377
+
378
+ ### `view`
379
+
380
+ Example: `Venue.view('create')`
381
+
382
+ Returns a schema validator configured with the specified view.
383
+
384
+ ### `version`
385
+
386
+ Example: `Venue.version(42)`
387
+
388
+ Returns a schema validator configured with the specified version.
389
+
390
+ ### `suffix`
391
+
392
+ Example: `Venue.suffix(:json)`
393
+
394
+ Returns a schema validator configured with the specified suffix.
395
+
396
+ ### `identifier`
397
+
398
+ Example: `Venue.version(2).identifier` (returns `'application/vnd.application.venue.v2'`)
399
+
400
+ Returns the IANA compatible [Media Type Identifier](https://en.wikipedia.org/wiki/Media_type) for the configured schema.
401
+
402
+ ### `available_validations`
403
+
404
+ Example: `Venue.available_validations`
405
+
406
+ Returns a list of all the schemas that are defined.
407
+
408
+ ## Ensuring Your MediaTypes Work
409
+
410
+ ### Overview & Rationale
411
+
412
+ If the MediaTypes you create enforce a specification you _do not expect them to_, it will cause problems that will be very difficult to fix, as other code, which utilises your MediaType, would break when you change the specification. This is because the faulty MediaType definition will start to make other code dependent on the specification it defines. For example, consider what would happen if you release a MediaType which defines an attribute `foo` to be a `String`, and run a server which defines such a specification. Later, you realise you _actually_ wanted `foo` to be `Numeric`. What can you do?
413
+
414
+ Well, during this time, other people started to write code which conformed to the specification defined by the faulty MediaType. So, it's going to be extremely difficult to revert your mistake. For this reason, it is vital that, when using this library, your MediaTypes define the _correct_ specification.
415
+
416
+ To this end, we provide you with a few avenues to check whether MediaTypes define the specifications you actually intend by checking examples of JSON you expect to be compliant/non-compliant with the MediaType definitions you write out.
417
+
418
+ These are as follows:
419
+
420
+ 1. The library provides [two methods](README.md#media-type-checking-in-test-suites) (`assert_pass` and `assert_fail`), which allow specifying JSON fixtures that are compliant (`assert_pass`) or non-compliant (`assert_fail`).
421
+ 2. The library provides a way to validate those fixtures against the MediaType specification with the [`assert_mediatype`](README.md#media-type-checking-in-test-suites) method.
422
+ 3. The library automatically performs a MediaType's checks defined by (1) the first time an object is validated against the MediaType, and throws an error if any of the checks fail.
423
+ 4. The library provides a way to run the checks carried out by (3) on load, using the method [`assert_sane!`](README.md#validation-checks) so that an application will not run if any of the MediaType's checks don't pass.
424
+
425
+ These four options are examined in detail below.
426
+
427
+ ### MediaType Checking in Test Suites
428
+
429
+ The library provides the `assert_mediatype` method, which allows running the checks for a particular `MediaType` within Minitest with `assert_pass` and `assert_fail`.
430
+ If you are using Minitest, you can make `assert_mediatype` available by calling `include MediaTypes::Testing::Assertions` in the test class (e.g. `Minitest::Runnable`):
431
+
432
+ ```ruby
433
+ module Minitest
434
+ class Test < Minitest::Runnable
435
+ include MediaTypes::Testing::Assertions
436
+ end
437
+ end
438
+ ```
439
+
440
+ The example below demonstrates how to use `assert_pass` and `assert_fail` within a MediaType, and how to use the `assert_mediatype` method in MiniTest tests to validate them.
441
+
442
+ ```ruby
443
+ class MyMedia
444
+ include MediaTypes::Dsl
445
+
446
+ def self.organisation
447
+ 'acme'
448
+ end
449
+
450
+ use_name 'test'
451
+
452
+ validations do
453
+ # Using "any Numeric" this MediaType doesn't care what key names you use.
454
+ # However, it does care that those keys point to a Numeric value.
455
+ any Numeric
456
+
457
+ assert_pass '{"foo": 42}'
458
+ assert_pass <<-FIXTURE
459
+ { "foo": 42, "bar": 43 }
460
+ FIXTURE
461
+
462
+ # The keyword "any" means there are no required keys, so having no keys should also pass.
463
+ assert_pass '{}'
464
+
465
+ # This MediaType should not accept anything other then a Numeric value.
466
+ assert_fail <<-FIXTURE
467
+ { "foo": { "bar": "string" } }
468
+ FIXTURE
469
+ assert_fail '{"foo": {}}'
470
+ assert_fail '{"foo": null}', loose: true
471
+ assert_fail '{"foo": [42]}', loose: false
472
+ end
473
+ end
474
+
475
+ class MyMediaTest < Minitest::Test
476
+ include MediaTypes::Testing::Assertions
477
+
478
+ def test_mediatype_specification
479
+ assert_mediatype MyMedia
480
+ end
481
+ end
482
+
483
+ class MyMediaTest < Minitest::Test
484
+ include MediaTypes::Testing::Assertions
485
+
486
+ def test_mediatype_specification
487
+ assert_mediatype MyMedia
488
+ end
489
+ end
490
+
491
+ ```
492
+
493
+ ### Testing Without Minitest
494
+
495
+ If you are using another testing framework, you will not be able to use the `assert_mediatype` method. Instead, you can test your MediaTypes by using the `assert_sane!` method (documented below) and rescuing the errors it will throw when it fails. The snippet below shows an example adaptation for MiniTest, which you can use as a guide.
496
+
497
+ ```ruby
498
+ def test_mediatype(mediatype)
499
+ mediatype.assert_sane!
500
+ assert mediatype.media_type_validations.scheme.asserted_sane?
501
+ rescue MediaTypes::AssertionError => e
502
+ flunk e.message
503
+ end
504
+ end
505
+ ```
506
+
507
+ ### Validation Checks
508
+
509
+ The `assert_pass` and `assert_fail` methods take a JSON string (as shown below). The first time the `validate!` method is called on a MediaType, the assertions for that media type are run.
510
+ This is done as a last line of defence against introducing faulty MediaTypes into your software. Ideally, you want to carry out these checks on load rather than on a running application. This functionality is provided by the `assert_sane!` method, which can be called on a particular MediaType:
511
+
512
+ ```ruby
513
+ MyMedia.assert_sane!
514
+ # true
515
+ ```
516
+
517
+ ### Intermediate Checks
518
+
519
+ The fixtures provided to the `assert_pass` and `assert_fail` methods are evaluated within the context of the block they are placed in. It's therefore possible to write a test for a (complex) optional attribute, without that test cluttering the fixtures for the entire mediatype.
520
+
521
+ ```ruby
522
+ class MyMedia
523
+ include MediaTypes::Dsl
524
+
525
+ expect_string_keys
526
+
527
+ def self.organisation
528
+ 'acme'
529
+ end
530
+
531
+ use_name 'test'
532
+
533
+ validations do
534
+ attribute :foo, Hash, optional: true do
535
+ attribute :bar, Numeric
536
+
537
+ # This passes, since in this context the "bar" key is required to have a Numeric value.
538
+ assert_pass '{"bar": 42}'
539
+ end
540
+ attribute :rep, Numeric
541
+
542
+ # This passes, since the attribute "foo" is optional.
543
+ assert_pass '{"rep": 42}'
544
+ end
545
+ end
546
+ ```
547
+
548
+ ## Key Type Validation
549
+
550
+ When interacting with Ruby objects defined by your MediaType, you want to avoid getting `nil` values, just because the the wrong key type is being used (e.g. `obj['foo']` instead of `obj[:foo]`).
551
+ To this end, the library provides the ability to specify the expected type of keys in a MediaType; by default symbol keys are expected.
552
+
553
+ ### Setting Key Type Expectations
554
+
555
+ Key type expectations can be set at the module level. Each MediaType within this module will inherit the expectation set by that module.
556
+
557
+ ```ruby
558
+ module Acme
559
+ MediaTypes.expect_string_keys(self)
560
+
561
+ # The MyMedia class expects string keys, as inherited from the Acme module.
562
+ class MyMedia
563
+ include MediaTypes::Dsl
564
+
565
+ def self.organisation
566
+ 'acme'
567
+ end
568
+
569
+ use_name 'test'
570
+
571
+ validations do
572
+ any Numeric
573
+ end
574
+ end
575
+ end
576
+ ```
577
+
578
+ If you validate an object with a different key type than expected, an error will be thrown:
579
+
580
+ ```ruby
581
+ Acme::MyMedia.validate! { "something": 42 }
582
+ # => passes, because all keys are a string
583
+
584
+ Acme::MyMedia.validate! { something: 42 }
585
+ # => throws a ValidationError , because 'something' is a symbol key
586
+ ```
587
+
588
+ ## Overriding Key Type Expectations
589
+
590
+ A key type expectation set by a Module can be overridden by calling either `expect_symbol_keys` or `expect_string_keys` inside the MediaType class.
591
+
592
+ ```ruby
593
+ module Acme
594
+ MediaTypes.expect_string_keys(self)
595
+
596
+ class MyOverridingMedia
597
+ include MediaTypes::Dsl
598
+
599
+ def self.organisation
600
+ 'acme'
601
+ end
602
+
603
+ use_name 'test'
604
+
605
+ # Expect keys to be symbols
606
+ expect_symbol_keys
607
+
608
+ validations do
609
+ any Numeric
610
+ end
611
+ end
612
+ end
613
+ ```
614
+
615
+ Now the MediaType throws an error when string keys are used.
616
+
617
+ ```ruby
618
+ Acme::MyOverridingMedia.validate! { something: 42 }
619
+ # => passes, because all keys are a symbol
620
+
621
+ Acme::MyOverridingMedia.validate! { "something": 42 }
622
+ # => throws a ValidationError , because 'something' is a string key
623
+ ```
624
+
625
+ ### Setting The JSON Parser With The Wrong Key Type
626
+
627
+ If you parse JSON with the wrong key type, as shown below, the resultant object will fail the validations.
628
+
629
+ ```ruby
630
+ class MyMedia
631
+ include MediaTypes::Dsl
632
+
633
+ def self.organisation
634
+ 'acme'
635
+ end
636
+
637
+ use_name 'test'
638
+
639
+ # Expect keys to be symbols
640
+ expect_symbol_keys
641
+
642
+ validations do
643
+ any Numeric
644
+ end
645
+ end
646
+
647
+ json = JSON.parse('{"foo": {}}', { symbolize_names: false })
648
+ # If MyMedia expects symbol keys
649
+ MyMedia.valid?(json)
650
+ # Returns false
651
+ ```
652
+
653
+ ## Related
654
+
655
+ - [`MediaTypes::Serialization`](https://github.com/XPBytes/media_types-serialization): :cyclone: Add media types supported serialization to Rails.
656
+
657
+ ## Development
658
+
659
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
660
+
661
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, call `bundle exec rake release` to create a new git tag, push git commits and tags, and
662
+ push the `.gem` file to rubygems.org.
663
+
664
+ ## Contributing
665
+
666
+ Bug reports and pull requests are welcome on GitHub at [SleeplessByte/media-types-ruby](https://github.com/SleeplessByte/media-types-ruby)