media_types 2.2.0 → 2.3.0

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