media_types-serialization 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +32 -32
  3. data/.github/workflows/publish-bookworm.yml +33 -0
  4. data/.github/workflows/publish-sid.yml +33 -0
  5. data/.gitignore +22 -14
  6. data/.idea/.rakeTasks +7 -7
  7. data/.idea/dictionaries/Derk_Jan.xml +6 -6
  8. data/.idea/encodings.xml +3 -3
  9. data/.idea/inspectionProfiles/Project_Default.xml +5 -5
  10. data/.idea/media_types-serialization.iml +76 -76
  11. data/.idea/misc.xml +6 -6
  12. data/.idea/modules.xml +7 -7
  13. data/.idea/runConfigurations/test.xml +19 -19
  14. data/.idea/vcs.xml +5 -5
  15. data/CHANGELOG.md +190 -182
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/LICENSE.txt +21 -21
  19. data/README.md +1048 -1048
  20. data/Rakefile +10 -10
  21. data/bin/console +14 -14
  22. data/bin/setup +8 -8
  23. data/lib/media_types/problem.rb +67 -67
  24. data/lib/media_types/serialization/base.rb +269 -216
  25. data/lib/media_types/serialization/error.rb +193 -193
  26. data/lib/media_types/serialization/fake_validator.rb +53 -53
  27. data/lib/media_types/serialization/serialization_dsl.rb +135 -135
  28. data/lib/media_types/serialization/serialization_registration.rb +245 -245
  29. data/lib/media_types/serialization/serializers/api_viewer.rb +383 -136
  30. data/lib/media_types/serialization/serializers/common_css.rb +212 -168
  31. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -80
  32. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -85
  33. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -58
  34. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +93 -93
  35. data/lib/media_types/serialization/serializers/problem_serializer.rb +111 -111
  36. data/lib/media_types/serialization/utils/accept_header.rb +77 -77
  37. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -82
  38. data/lib/media_types/serialization/version.rb +7 -7
  39. data/lib/media_types/serialization.rb +682 -675
  40. data/media_types-serialization.gemspec +48 -48
  41. metadata +9 -8
  42. data/Gemfile.lock +0 -167
data/README.md CHANGED
@@ -1,1048 +1,1048 @@
1
- # MediaTypes::Serialization
2
-
3
- [![Build Status](https://github.com/XPBytes/media_types-serialization/actions/workflows/ci.yml/badge.svg)](https://github.com/XPBytes/media_types-serialization/actions/workflows/ci.yml)
4
- [![Gem Version](https://badge.fury.io/rb/media_types-serialization.svg)](https://badge.fury.io/rb/media_types-serialization)
5
- [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
6
-
7
- `respond_to` on steroids. Add [HATEOAS](https://docs.delftsolutions.nl/wiki/HATEOAS_API) compatible serialization and deserialization to your Rails projects.
8
-
9
- ## Installation
10
-
11
- Add this line to your application's Gemfile:
12
-
13
- ```ruby
14
- gem 'media_types-serialization'
15
- ```
16
-
17
- And then execute:
18
-
19
- ```shell
20
- bundle
21
- ```
22
-
23
- Or install it yourself as:
24
-
25
- ```shell
26
- gem install media_types-serialization
27
- ```
28
-
29
- ## Usage
30
-
31
- Serializers help you in converting a ruby object to a representation matching a specified [Media Type validator](https://github.com/SleeplessByte/media-types-ruby) and the other way around.
32
-
33
- ### Creating a serializer
34
-
35
- ```ruby
36
- class BookSerializer < MediaTypes::Serialization::Base
37
- unvalidated 'application/vnd.acme.book'
38
-
39
- # outputs with a Content-Type of application/vnd.acme.book.v1+json
40
- output version: 1 do |obj, version, context|
41
- {
42
- book: {
43
- title: obj.title
44
- }
45
- }
46
- end
47
- end
48
- ```
49
-
50
- To convert a ruby object to a json representation:
51
-
52
- ```ruby
53
- class Book
54
- attr_accessor :title
55
- end
56
-
57
- book = Book.new
58
- book.title = 'Everything, abridged'
59
-
60
- BookSerializer.serialize(book, 'vnd.acme.book.v1+json', context: nil)
61
- # => { "book": { "title": "Everything, abridged" } }
62
- ```
63
-
64
- ### Controller integration
65
-
66
- You can integrate the serialization system in rails, giving you automatic [Content-Type negotiation](https://en.wikipedia.org/wiki/Content_negotiation) using the `Accept` header:
67
-
68
- ```ruby
69
- require 'media_types/serialization'
70
-
71
- class BookController < ActionController::API
72
- include MediaTypes::Serialization
73
-
74
- allow_output_serializer(BookSerializer, only: %i[show])
75
- freeze_io!
76
-
77
- def show
78
- book = Book.new
79
- book.title = 'Everything, abridged'
80
-
81
- render_media book
82
- end
83
- end
84
- ```
85
-
86
- While using the controller integration the context will always be set to the current controller.
87
- This allows you to construct urls.
88
-
89
- ### Adding HATEOAS responses to existing routes
90
-
91
- When creating a mobile application it's often useful to allow the app to request a non-html representation of a specific url.
92
- If you have an existing route:
93
-
94
- ```ruby
95
- class BookController < ApplicationController
96
- def show
97
- @book = Book.new
98
-
99
- # Use view corresponding to the controller
100
- end
101
- end
102
- ```
103
-
104
- You can add a json representation as follows:
105
-
106
- ```ruby
107
- class BookController < ApplicationController
108
- allow_output_serializer(BookSerializer, only: %i[show])
109
- allow_output_html
110
- freeze_io!
111
-
112
- def show
113
- @book = Book.new
114
-
115
- render_media @book
116
- end
117
- end
118
- ```
119
-
120
- ### Validations
121
-
122
- Right now the serializer does not validate incoming or outgoing information.
123
- This can cause issues when you accidentally emit non-conforming data that people start to depend on.
124
- To make sure you don't do that you can specify a [Media Type validator](https://github.com/SleeplessByte/media-types-ruby):
125
-
126
- ```ruby
127
- require 'media_types'
128
-
129
- class BookValidator
130
- include MediaTypes::Dsl
131
-
132
- def self.organisation
133
- 'acme'
134
- end
135
-
136
- use_name 'book'
137
-
138
- validations do
139
- version 1 do
140
- attribute :book do
141
- attribute :title, String
142
- end
143
- end
144
- end
145
- end
146
-
147
- class BookSerializer < MediaTypes::Serialization::Base
148
- validator BookValidator
149
-
150
- # outputs with a Content-Type of application/vnd.acme.book.v1+json
151
- output version: 1 do |obj, version, context|
152
- {
153
- book: {
154
- title: obj.title
155
- }
156
- }
157
- end
158
- end
159
- ```
160
-
161
- For more information, see the [Media Types docs](https://github.com/SleeplessByte/media-types-ruby).
162
-
163
- ### Versioning
164
-
165
- To help with supporting older versions, serializers have a [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) to construct json objects:
166
-
167
- ```ruby
168
- class BookSerializer < MediaTypes::Serialization::Base
169
- validator BookValidator
170
-
171
- output versions: [1, 2] do |obj, version, context|
172
- attribute :book do
173
- attribute :title, obj.title
174
- attribute :description, obj.description if version >= 2
175
- end
176
- end
177
- end
178
- ```
179
-
180
- ```ruby
181
- BookSerializer.serialize(book, BookValidator.version(1), context: nil)
182
- # => { "book": { "title": "Everything, abridged" } }
183
-
184
- BookSerializer.serialize(book, BookValidator.version(2), context: nil)
185
- # => { "book": { "title": "Everything, abridged", "description": "Mu" } }
186
- ```
187
-
188
- ### Links
189
-
190
- When making [HATEOAS](https://docs.delftsolutions.nl/wiki/HATEOAS_API) compliant applications it's very useful to include `Link` headers in your response so clients can use a `HEAD` request instead of having to fetch the entire resource.
191
- Serializers have convenience methods to help with this:
192
-
193
- ```ruby
194
- class BookSerializer < MediaTypes::Serialization::Base
195
- validator BookValidator
196
-
197
- output versions: [1, 2, 3] do |obj, version, context|
198
- attribute :book do
199
- link :self, href: context.book_url(obj) if version >= 3
200
-
201
- attribute :title, obj.title
202
- attribute :description, obj.description if version >= 2
203
- end
204
- end
205
- end
206
- ```
207
-
208
- This returns the following response:
209
-
210
- ```ruby
211
- BookSerializer.serialize(book, BookValidator.version(3), context: controller)
212
- # header = Link: <https://example.org/>; rel="self"
213
- # => {
214
- # "book": {
215
- # "_links": {
216
- # "self": { "href": "https://example.org" }
217
- # },
218
- # "title": "Everything, abridged",
219
- # "description": "Mu"
220
- # }
221
- # }
222
- ```
223
-
224
- ### Collections
225
-
226
- There are convenience methods for serializing arrays of objects based on a template.
227
-
228
- #### Indexes (index based collections)
229
-
230
- An index is a collection of urls that point to members of the array.
231
- The index method automatically generates it based on the `self` links defined in the default view (`view: nil`) of the given version.
232
-
233
- ```ruby
234
- class BookSerializer < MediaTypes::Serialization::Base
235
- validator BookValidator
236
-
237
- output versions: [1, 2, 3] do |obj, version, context|
238
- attribute :book do
239
- link :self, href: context.book_url(obj) if version >= 3
240
-
241
- attribute :title, obj.title
242
- attribute :description, obj.description if version >= 2
243
- end
244
- end
245
-
246
- output view: :index, version: 3 do |arr, version, context|
247
- attribute :books do
248
- link :self, href: context.book_index_url
249
-
250
- index arr, version: version
251
- end
252
- end
253
- end
254
- ```
255
-
256
- ```ruby
257
- BookSerializer.serialize([book], BookValidator.view(:index).version(3), context: controller)
258
- # header = Link: <https://example.org/index>; rel="self"
259
- # => {
260
- # "books": {
261
- # "_links": {
262
- # "self": { "href": "https://example.org" }
263
- # },
264
- # "_index": [
265
- # { "href": "https://example.org" }
266
- # ]
267
- # }
268
- # }
269
- ```
270
-
271
- ##### How to validate?
272
-
273
- The `index` dsl does _not_ exist in the validation gem.
274
- This is how you validate indices:
275
-
276
- ```ruby
277
- class BookValidator
278
- include MediaTypes::Dsl
279
-
280
- def self.organisation
281
- 'acme'
282
- end
283
-
284
- use_name 'book'
285
-
286
- validations do
287
- view :index do
288
- version 3 do
289
- attribute :books do
290
- link :self
291
-
292
- collection :_index, allow_empty: true do
293
- attribute :href, String
294
- end
295
- end
296
- end
297
- end
298
- end
299
- end
300
- ```
301
-
302
- If the `:self` link contains _more attributes_, they will show up here too.
303
-
304
- ```ruby
305
- class BookSerializer < MediaTypes::Serialization::Base
306
- validator BookValidator
307
-
308
- output versions: [1, 2, 3] do |obj, version, context|
309
- attribute :book do
310
- link :self, href: context.book_url(obj), isbn: obj.isbn if version >= 3
311
-
312
- attribute :title, obj.title
313
- attribute :description, obj.description if version >= 2
314
- end
315
- end
316
-
317
- output view: :index, version: 3 do |arr, version, context|
318
- attribute :books do
319
- link :self, href: context.book_index_url
320
-
321
- index arr, version: version
322
- end
323
- end
324
- end
325
- ```
326
-
327
- ```ruby
328
- class BookValidator
329
- include MediaTypes::Dsl
330
-
331
- def self.organisation
332
- 'acme'
333
- end
334
-
335
- use_name 'book'
336
-
337
- validations do
338
- view :index do
339
- version 3 do
340
- attribute :books do
341
- link :self
342
-
343
- collection :_index, allow_empty: true do
344
- attribute :href, String
345
- attribute :isbn, AllowNil(String)
346
- end
347
- end
348
- end
349
- end
350
- end
351
- end
352
- ```
353
-
354
- #### Collections (embedding collections)
355
-
356
- A collection inlines the member objects.
357
- The collection method automatically generates it based on the default view of the same version.
358
-
359
- ```ruby
360
- class BookSerializer < MediaTypes::Serialization::Base
361
- validator BookValidator
362
-
363
- output versions: [1, 2, 3] do |obj, version, context|
364
- attribute :book do
365
- link :self, href: context.book_url(obj) if version >= 3
366
-
367
- attribute :title, obj.title
368
- attribute :description, obj.description if version >= 2
369
- end
370
- end
371
-
372
- output view: :index, version: 3 do |arr, version, context|
373
- attribute :books do
374
- link :self, href: context.book_index_url
375
-
376
- index arr, version: version
377
- end
378
- end
379
-
380
- output view: :collection, version: 3 do |arr, version, context|
381
- attribute :books do
382
- link :self, href: context.book_collection_url
383
-
384
- collection arr, version: version
385
- end
386
- end
387
- end
388
- ```
389
-
390
- ```ruby
391
- BookSerializer.serialize([book], BookValidator.view(:collection).version(3), context: controller)
392
- # header = Link: <https://example.org/collection>; rel="self"
393
- # => {
394
- # "books": {
395
- # "_links": {
396
- # "self": { "href": "https://example.org" }
397
- # },
398
- # "_embedded": [
399
- # {
400
- # "_links": {
401
- # "self": { "href": "https://example.org" }
402
- # },
403
- # "title": "Everything, abridged",
404
- # "description": "Mu"
405
- # }
406
- # ]
407
- # }
408
- # }
409
- ```
410
-
411
- The `collection` dsl is _not_ the same as the one in the validation gem.
412
- This is how you could validate collections:
413
-
414
- ```ruby
415
- class BookValidator
416
- include MediaTypes::Dsl
417
-
418
- def self.organisation
419
- 'acme'
420
- end
421
-
422
- use_name 'book'
423
-
424
- validations do
425
- view :collection do
426
- version 3 do
427
- attribute :books do
428
- link :self
429
-
430
- collection :_embedded, allow_empty: true do
431
- link :self
432
-
433
- attribute :title, String
434
- attribute :description, AllowNil(String)
435
- end
436
- end
437
- end
438
- end
439
- end
440
- end
441
- ```
442
-
443
- ### Input deserialization
444
-
445
- You can mark a media type as something that's allowed to be sent along with a PUT request as follows:
446
-
447
- ```ruby
448
- class BookSerializer < MediaTypes::Serialization::Base
449
- validator BookValidator
450
-
451
- output versions: [1, 2, 3] do |obj, version, context|
452
- attribute :book do
453
- link :self, href: context.book_url(obj) if version >= 3
454
-
455
- attribute :title, obj.title
456
- attribute :description, obj.description if version >= 2
457
- end
458
- end
459
-
460
- input version: 3
461
- end
462
-
463
- class BookController < ActionController::API
464
- include MediaTypes::Serialization
465
-
466
- allow_output_serializer(BookSerializer, only: %i[show])
467
- allow_input_serializer(BookSerializer, only: %i[create])
468
- freeze_io!
469
-
470
- def show
471
- book = Book.new
472
- book.title = 'Everything, abridged'
473
-
474
- render_media serialize_media(book)
475
- end
476
-
477
- def create
478
- json = deserialize(request) # does validation for us
479
- puts json
480
- end
481
- end
482
- ```
483
-
484
- If you use [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) you might want to convert the verified json data during deserialization:
485
-
486
- ```ruby
487
- class BookSerializer < MediaTypes::Serialization::Base
488
- validator BookValidator
489
-
490
- output versions: [1, 2, 3] do |obj, version, context|
491
- attribute :book do
492
- link :self, href: context.book_url(obj) if version >= 3
493
-
494
- attribute :title, obj.title
495
- attribute :description, obj.description if version >= 2
496
- end
497
-
498
- input versions: [1, 2, 3] do |json, version, context|
499
- book = Book.new
500
- book.title = json['book']['title']
501
- book.description = 'Not available'
502
- book.description = json['book']['description'] if version >= 2
503
-
504
- # Best practise is to only save in the controller.
505
- book
506
- end
507
- end
508
-
509
- class BookController < ActionController::API
510
- include MediaTypes::Serialization
511
-
512
- allow_output_serializer(BookSerializer, only: %i[show])
513
- allow_input_serializer(BookSerializer, only: %i[create])
514
- freeze_io!
515
-
516
- def show
517
- book = Book.new
518
- book.title = 'Everything, abridged'
519
-
520
- render_media serialize_media(book)
521
- end
522
-
523
- def create
524
- book = deserialize(request)
525
- book.save!
526
-
527
- render_media serialize_media(book)
528
- end
529
- end
530
- ```
531
-
532
- If you don't want to apply any input validation or deserialization you can use the `allow_all_input` method instead of `allow_input_serialization`.
533
-
534
- ### Raw output
535
-
536
- Sometimes you need to output raw data.
537
- This cannot be validated.
538
- You do this as follows:
539
-
540
- ```ruby
541
- class BookSerializer < MediaTypes::Serialization::Base
542
- validator BookValidator
543
-
544
- output_raw view: :raw, version: 3 do |obj, version, context|
545
- hidden do
546
- # Make sure links are only set in the headers, not in the body.
547
-
548
- link :self, href: context.book_url(obj)
549
- end
550
-
551
- "I'm a non-json output"
552
- end
553
- end
554
- ```
555
-
556
- ### Raw input
557
-
558
- You can do the same with input:
559
-
560
- ```ruby
561
- class BookSerializer < MediaTypes::Serialization::Base
562
- validator BookValidator
563
-
564
- input_raw view: raw, version: 3 do |bytes, version, context|
565
- book = Book.new
566
- book.description = bytes
567
-
568
- book
569
- end
570
- end
571
- ```
572
-
573
- ### Remapping media type identifiers
574
-
575
- Sometimes you already have old clients using an `application/json` media type identifier when they do requests.
576
- While this is not a good practice as this makes it hard to add new fields or remove old ones, this library has support for migrating away:
577
-
578
- ```ruby
579
- class BookSerializer < MediaTypes::Serialization::Base
580
- validator BookValidator
581
-
582
- # applicaton/vnd.acme.book.v3+json
583
- # applicaton/vnd.acme.book.v2+json
584
- # applicaton/vnd.acme.book.v1+json
585
- output versions: [1, 2, 3] do |obj, version, context|
586
- attribute :book do
587
- link :self, href: context.book_url(obj) if version >= 3
588
-
589
- attribute :title, obj.title
590
- attribute :description, obj.description if version >= 2
591
- end
592
- end
593
-
594
- # applicaton/vnd.acme.book+json
595
- output version: nil do |obj, version, context|
596
- attribute :book do
597
- attribute :title, obj.title
598
- end
599
- end
600
- output_alias 'application/json' # maps application/json to to applicaton/vnd.acme.book+json
601
-
602
- # applicaton/vnd.acme.book.v3.create+json
603
- # applicaton/vnd.acme.book.v2.create+json
604
- # applicaton/vnd.acme.book.v1.create+json
605
- input view: :create, versions: [1, 2, 3] do |json, version, context|
606
- book = Book.new
607
- book.title = json['book']['title']
608
- book.description = 'Not available'
609
- book.description = json['book']['description'] if version >= 2
610
-
611
- # Make sure not to save here but only save in the controller
612
- book
613
- end
614
-
615
- # applicaton/vnd.acme.book.create+json
616
- input view: :create, version: nil do |json, version, context|
617
- book = Book.new
618
- book.title = json['book']['title']
619
- book.description = 'Not available'
620
-
621
- # Make sure not to save here but only save in the controller
622
- book
623
- end
624
- input_alias 'application/json', view: :create # maps application/json to to applicaton/vnd.acme.book.create+json
625
- ```
626
-
627
- Validation will be done using the remapped validator. Aliasses map to version `nil`.
628
- It is not possible to configure this version.
629
-
630
- ### HTML
631
-
632
- This library has a built in API viewer.
633
- The viewer can be accessed by by appending a `?api_viewer=last` query parameter to the URL.
634
-
635
- To enable the API viewer, use: `allow_api_viewer` in the controller.
636
-
637
- ```ruby
638
- class BookController < ActionController::API
639
- include MediaTypes::Serialization
640
-
641
- allow_api_viewer
642
-
643
- allow_output_serializer(BookSerializer, only: %i[show])
644
- allow_input_serializer(BookSerializer, only: %i[create])
645
- freeze_io!
646
-
647
- def show
648
- book = Book.new
649
- book.title = 'Everything, abridged'
650
-
651
- render_media serialize_media(book)
652
- end
653
-
654
- def create
655
- json = deserialize(request) # does validation for us
656
- puts json
657
- end
658
- end
659
- ```
660
-
661
- You can also output custom HTML:
662
-
663
- ```ruby
664
- class BookSerializer < MediaTypes::Serialization::Base
665
- validator BookValidator
666
-
667
- output versions: [1, 2, 3] do |obj, version, context|
668
- attribute :book do
669
- link :self, href: context.book_url(obj) if version >= 3
670
-
671
- attribute :title, obj.title
672
- attribute :description, obj.description if version >= 2
673
- end
674
- end
675
-
676
- output_raw view: :html do |obj, context|
677
- render_view 'book/show', context: context, assigns: {
678
- title: obj.title,
679
- description: obj.description
680
- }
681
- end
682
-
683
- output_alias 'text/html', view: :html
684
- end
685
- ```
686
-
687
- #### Errors
688
-
689
- This library adds support for returning errors to clients using the [`application/problem+json`](https://tools.ietf.org/html/rfc7231) media type.
690
- You can catch and transform application errors by adding an `output_error` call before `freeze_io!`:
691
-
692
- ```ruby
693
- class BookController < ActionController::API
694
- include MediaTypes::Serialization
695
-
696
- output_error CanCan::AccessDenied do |problem_output, error|
697
- problem_output.title 'You do not have enough permissions to perform this action.', lang: 'en'
698
- problem_output.title 'Je hebt geen toestemming om deze actie uit te voeren.', lang: 'nl-NL'
699
-
700
- problem_output.status_code :forbidden
701
- end
702
-
703
- freeze_io!
704
-
705
- # ...
706
- end
707
- ```
708
-
709
- The exception you specified will be rescued by the controller and will be displayed to the user along with a link to the shared wiki page for that error type.
710
- Feel free to add instructions there on how clients should solve this problem.
711
- You can find more information at: [https://docs.delftsolutions.nl/wiki/Error](https://docs.delftsolutions.nl/wiki/Error)
712
- If you want to override this url you can use the `problem_output.url(href)` function.
713
-
714
- By default the `message` property of the error is used to fill the `details` field.
715
- You can override this by using the `problem_output.override_detail(description, lang:)` function.
716
-
717
- Custom attributes can be added using the `problem_output.attribute(name, value)` function.
718
-
719
- ### Related
720
-
721
- - [`MediaTypes`](https://github.com/SleeplessByte/media-types-ruby): :gem: Library to create media type validators.
722
-
723
- ## API
724
-
725
- ### Serializer class definition
726
-
727
- These methods become available during class definition if you inherit from `MediaTypes::Serialization::Base`.
728
-
729
- #### `unvalidated( prefix )`
730
-
731
- Disabled validation for this serializer. Prefix is of the form `application/vnd.<organisation>.<name>`.
732
-
733
- Either unvalidated or validator must be used while defining a serializer.
734
-
735
- #### `validator( media_type_validator )`
736
-
737
- Enabled validation for this serializer using a [Media Type Validator](https://github.com/SleeplessByte/media-types-ruby).
738
-
739
- Either validator or unvalidated must be used while defining a serializer.
740
-
741
- #### `output( view:, version:, versions: ) do |obj, version, context|`
742
-
743
- Defines a serialization block. Either version or versions can be set.
744
- `nil` is allowed for unversioned.
745
- View should be a symbol or unset.
746
-
747
- Obj is the object to be serialized, version is the negotiated version and context is the context passed in from the serialize function.
748
- When using the controller integration, context is the current controller.
749
-
750
- The block should return an object to convert into JSON.
751
-
752
- #### `output_raw( view:, version:, versions:, suffix: ) do |obj, version, context|`
753
-
754
- This has the same behavior as `output` but should return a string instead of an object.
755
- Output is not validated.
756
- By default, `input_raw` is expected to _not_ be JSON.
757
- Override `suffix` with `:json` if it _is_ JSON.
758
-
759
- #### `output_alias( media_type_identifier, view:, hide_variant: false, suffix: '~' )`
760
-
761
- Defines a legacy mapping. This will make the deserializer parse the media type `media_type_identifier` as if it was version `nil` of the specified view.
762
- If `view` is undefined it will use the output serializer without a view defined.
763
- By default, suffix is `:json` if `media_type_identifier` is a JSON type.
764
-
765
- Response will have a content type equal to `[media_type_identifier]; variant=[mapped_media_type_identifier]`.
766
- If `hide_variant:` is true, the content type emitted will only be `[media_type_identifier]`.
767
-
768
- > You cannot alias a _versioned_ media type, otherwise it would be easy to later break the definition by changing the version it aliases.
769
-
770
- #### `output_alias_optional( media_type_identifier, view:, hide_variant: false, suffix: '~' )`
771
-
772
- Has the same behavior as `output_alias` but can be used by multiple serializers.
773
- The serializer that is loaded last in the controller 'wins' control over this media type identifier.
774
- If any of the serializers have an `output_alias` defined with the same media type identifier that one will win instead.
775
- By default, suffix is `:json` if `media_type_identifier` is a JSON type.
776
-
777
- Response will have a content type equal to `[media_type_identifier]; variant=[mapped_media_type_identifier]`. If `hide_variant:` is true, the content type emitted will only be `[media_type_identifier]`.
778
-
779
- #### `input( view:, version:, versions: ) do |obj, version, context|`
780
-
781
- Defines a deserialization block. Either version or versions can be set.
782
- View should be a symbol or unset.
783
-
784
- Obj is the object to be serialized, version is the negotiated version and context is the context passed in from the serialize function.
785
- When using the controller integration, context is the current controller.
786
-
787
- The block should return the internal representation of the object.
788
- Best practise is to make sure not to change state in this function but to leave that up to the controller.
789
-
790
- #### `input_raw( view:, version:, versions:, suffix: nil ) do |bytes, version, context|`
791
-
792
- This has the same behavior as `input` but takes in raw data.
793
- Input is not validated.
794
- By default, `input_raw` is expected to _not_ be JSON.
795
- Override `suffix` with `:json` if it _is_ JSON.
796
-
797
- #### `input_alias( media_type_identifier, view:, suffix: '~' )`
798
-
799
- Defines a legacy mapping.
800
- This will make the serializer parse the media type `media_type_identifier` as if it was version `nil` of the specified view.
801
- If view is undefined it will use the input serializer without a view defined.
802
- By default, suffix is `:json` if `media_type_identifier` is a JSON type.
803
-
804
- > You cannot alias a _versioned_ media type, otherwise it would be easy to later break the definition by changing the version it aliases.
805
-
806
- #### `input_alias_optional( media_type_identifier, view:, suffix: '~' )`
807
-
808
- Has the same behavior as `input_alias` but can be used by multiple serializers.
809
- The serializer that is loaded last in the controller 'wins' control over this media type identifier.
810
- If any of the serializers have an `input_alias` defined with the same media type identifier that one will win instead.
811
- By default, suffix is `:json` if `media_type_identifier` is a JSON type.
812
-
813
- #### `disable_wildcards`
814
-
815
- Disables registering wildcard media types.
816
-
817
- ### Serializer output definition
818
-
819
- The following methods are available within an `output ... do` block.
820
-
821
- #### `attribute( key, value = {} ) do`
822
-
823
- Sets a value for the given key.
824
- If a block is given, any `attribute`, `link`, `collection` and `index` statements are run in context of `value`.
825
-
826
- Returns the built up context so far.
827
-
828
- #### `link( rel, href:, emit_header: true, **attributes )`
829
-
830
- Adds a `_link` block to the current context. Also adds the specified link to the HTTP Link header.
831
- `attributes` allows passing in custom attributes.
832
-
833
- If `emit_header` is `true` the link will also be emitted as a http header.
834
-
835
- Returns the built up context so far.
836
-
837
- #### `index( array, serializer, version:, view: nil )`
838
-
839
- > Not the same as a validator `collection`.
840
-
841
- Adds an `_index` block to the current context. Uses the self links of the specified view to construct an index of urls to the child objects.
842
-
843
- Returns the built up context so far.
844
-
845
- #### `collection( array, serializer, version:, view: nil )`
846
-
847
- > Not the same as a validator `collection`.
848
-
849
- Adds an `_embedded` block to the current context. Uses the specified serializer to embed the child objects.
850
- Optionally a block can be used to modify the output from the child serializer.
851
-
852
- Returns the built up context so far.
853
-
854
- #### `hidden do`
855
-
856
- Sometimes you want to add links without actually modifying the object.
857
- Calls to `attribute`, `link`, `index`, `collection` made inside this block won't modify the context.
858
- Any calls to link will only set the HTTP Link header.
859
-
860
- Returns the unmodified context.
861
-
862
- #### `emit`
863
-
864
- Can be added to the end of a block to fix up the return value to return the built up context so far.
865
-
866
- Returns the built up context so far.
867
-
868
- #### `object do`
869
-
870
- Runs a block in a new context and returns the result
871
-
872
- > Most common use-case is emitting from an enumerable.
873
- >
874
- > ```ruby
875
- > results = [item, item, item].map do |current_item|
876
- > object do
877
- > attribute :foo, current_item.bar
878
- > end
879
- > end
880
- >
881
- > attribute :items, results
882
- > ```
883
-
884
- #### `render_view( view, context:, **args)`
885
-
886
- Can be used to render a view.
887
- You can set local variables in the view by assigning a hash to the `assigns:` parameter.
888
- Returns a `string`
889
-
890
- > When possible, prefer `output_raw` with context.render_to_string(params)`
891
-
892
- #### `redirect_to(url, context, **options)`
893
-
894
- This redirects the user to the specified url when this serializer is rendered. The output of the serializer is still shown in the API viewer.
895
-
896
- #### `suppress_render do |result|`
897
-
898
- Replaces the render at the end of `render_media` and substitutes it with the contents of the block.
899
-
900
- ### Controller definition
901
-
902
- These functions are available during the controller definition if you add `include MediaTypes::Serialization`.
903
-
904
- #### `allow_output_serializer( serializer, views: nil, **filters )`
905
-
906
- Configure the controller to allow the client to request responses emitted by the specified serializer.
907
- Optionally allows you to specify which views to allow by passing an array in the views parameter.
908
-
909
- Accepts the same filters as `before_action`.
910
-
911
- #### `allow_output_html( as: nil, view: nil, layout: nil, **filters )`
912
-
913
- Allows falling back to the default Rails view rendering when the client asks for the media type in the `as:` parameter or `text/html` if `as:` is unset.
914
-
915
- The `Content-Type` of the response will be `text/html` if the `as:` parameter is unset.
916
- If the `as:` parameter is set, it will include it in the variant parameter: `text/html; variant=application/vnd.xpbytes.borderless`.
917
-
918
- Accepts the same filters as `before_action`.
919
- You can set the template to use using the `view:` parameter.
920
-
921
- #### `allow_output_docs( description, **filters )`
922
-
923
- Outputs the specified description as help information.
924
-
925
- Accepts the same filters as `before_action`.
926
-
927
- #### `output_error(klazz, serializers = []) do`
928
-
929
- Wraps the controller method in a `rescue_from` and presents the users with a `text/html` or `application/problem+json` representation of the error.
930
- The `text/html` response can be overridden by supplying an additional serializer in the `serializers` array. It will use the nil view for the given serializers.
931
-
932
- #### `allow_input_serializer( serializer, views: nil, **filters )`
933
-
934
- Configure the controller to allow the client to send bodies with a `Content-Type` that can be deserialized using the specified serializer.
935
- Optionally allows you to specify which views to allow by passing an array in the views parameter.
936
-
937
- Accepts the same filters as `before_action`.
938
-
939
- #### `allow_all_input( **filters )`
940
-
941
- Disables input deserialization. Running `deserialize` while allowing all input will result in an error being thrown.
942
-
943
- #### `not_acceptable_serializer( serializer )`
944
-
945
- Replaces the serializer used to render the error page when no media type could be negotiated using the `Accept` header.
946
-
947
- #### `unsupported_media_type_serializer( serializer )`
948
-
949
- Adds a serializer that can be used to render the error page when the client submits a body with a `Content-Type` that was not added to the whitelist using `allow_input_serialization`.
950
-
951
- #### `clear_unsupported_media_type_serializers!`
952
-
953
- Clears the list of serializers used to render the error when the client supplies non-valid input.
954
-
955
- #### `input_validation_failed_serializer( serializer )`
956
-
957
- Adds a serializer that can be used to render the error page when input validation fails.
958
-
959
- #### `clear_input_validation_failed_serializers!`
960
-
961
- Clears the list of serializers used to render the error when the client supplies non-valid input.
962
-
963
- #### `allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)`
964
-
965
- Enables rendering the api viewer when adding the `api_viewer=last` query parameter to the url.
966
-
967
- #### `freeze_io!(**filter_opts)`
968
-
969
- Registers serialization and deserialization in the controller.
970
- This function must be called before using the controller.
971
-
972
- ### Controller usage
973
-
974
- These functions are available during method execution in the controller.
975
-
976
- #### `render_media( obj, serializers: nil, not_acceptable_serializer: nil, **options ) do`
977
-
978
- Serializes an object and renders it using the appropriate content type.
979
- Options are passed through to the controller `render` function. Allows you to specify different objects to different serializers using a block:
980
-
981
- ```ruby
982
- render_media do
983
- serializer BookSerializer, book
984
- serializer BooksSerializer do
985
- [ book ]
986
- end
987
- end
988
- ```
989
-
990
- **Warning**: this block can be called multiple times when used together with recursive serializers like the API viewer.
991
- Try to _avoid changing state_ in this block.
992
-
993
- If you want to render with different serializers than defined in the controller you can pass an array of serializers in the `serializers` property.
994
-
995
- If you want to override the serializer that is used to render the response when no acceptable Content-Type could be negotiated you can pass the desired serializer in the `not_acceptable_serializer` property.
996
-
997
- This method throws a `MediaTypes::Serialization::OutputValidationFailedError` error if the output does not conform to the format defined by the configured validator.
998
- Best practise is to return a 500 error to the client.
999
-
1000
- If no acceptable Content-Type could be negotiated the response will be rendered using the serialized defined by the class `not_acceptable_serializer` function or by the `not_acceptable_serializer` property.
1001
-
1002
- Due to the way this gem is implemented it is not possible to use instance variables (`@variable`) in the `render_media` do block.
1003
-
1004
- #### `deserialize( request )`
1005
-
1006
- Deserializes the request body using the configured input serializers and returns the deserialized object.
1007
-
1008
- Returns nil if no input body was given by the client.
1009
-
1010
- This method throws a `MediaTypes::Serialization::InputValidationFailedError` error if the incoming data does not conform to the specified schema.
1011
-
1012
- #### `deserialize!( request )`
1013
-
1014
- Does the same as `deserialize( request )` but gives the client an error page if no input was supplied.
1015
-
1016
- #### `resolve_serializer(request, identifier = nil, registration = @serialization_output_registration)`
1017
-
1018
- Returns the serializer class that will handle the given request.
1019
-
1020
- ## Customization
1021
-
1022
- The easiest way to customize the look and feel of the built in pages is to provide your own logo and background in an initializer:
1023
-
1024
- ```ruby
1025
- # config/initializers/serialization.rb
1026
-
1027
- MediaTypes::Serialization::Serializers::CommonCSS.background = 'linear-gradient(245deg, #3a2f28 0%, #201a16 100%)'
1028
- MediaTypes::Serialization::Serializers::CommonCSS.logo_width = 12
1029
- MediaTypes::Serialization::Serializers::CommonCSS.logo_data = <<-HERE
1030
- <svg height="150" width="500">
1031
- <ellipse cx="240" cy="100" rx="220" ry="30" style="fill:purple" />
1032
- <ellipse cx="220" cy="70" rx="190" ry="20" style="fill:lime" />
1033
- <ellipse cx="210" cy="45" rx="170" ry="15" style="fill:yellow" />
1034
- </svg>
1035
- HERE
1036
- ```
1037
-
1038
- ## Development
1039
-
1040
- After checking out the repo, run `bin/setup` to install dependencies.
1041
- Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1042
-
1043
- To install this gem onto your local machine, run `bundle exec rake install`.
1044
- To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1045
-
1046
- ## Contributing
1047
-
1048
- Bug reports and pull requests are welcome on GitHub at [XPBytes/media_types-serialization](https://github.com/XPBytes/media_types-serialization).
1
+ # MediaTypes::Serialization
2
+
3
+ [![Build Status](https://github.com/XPBytes/media_types-serialization/actions/workflows/ci.yml/badge.svg)](https://github.com/XPBytes/media_types-serialization/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/media_types-serialization.svg)](https://badge.fury.io/rb/media_types-serialization)
5
+ [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
6
+
7
+ `respond_to` on steroids. Add [HATEOAS](https://docs.delftsolutions.nl/wiki/HATEOAS_API) compatible serialization and deserialization to your Rails projects.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'media_types-serialization'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```shell
20
+ bundle
21
+ ```
22
+
23
+ Or install it yourself as:
24
+
25
+ ```shell
26
+ gem install media_types-serialization
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Serializers help you in converting a ruby object to a representation matching a specified [Media Type validator](https://github.com/SleeplessByte/media-types-ruby) and the other way around.
32
+
33
+ ### Creating a serializer
34
+
35
+ ```ruby
36
+ class BookSerializer < MediaTypes::Serialization::Base
37
+ unvalidated 'application/vnd.acme.book'
38
+
39
+ # outputs with a Content-Type of application/vnd.acme.book.v1+json
40
+ output version: 1 do |obj, version, context|
41
+ {
42
+ book: {
43
+ title: obj.title
44
+ }
45
+ }
46
+ end
47
+ end
48
+ ```
49
+
50
+ To convert a ruby object to a json representation:
51
+
52
+ ```ruby
53
+ class Book
54
+ attr_accessor :title
55
+ end
56
+
57
+ book = Book.new
58
+ book.title = 'Everything, abridged'
59
+
60
+ BookSerializer.serialize(book, 'vnd.acme.book.v1+json', context: nil)
61
+ # => { "book": { "title": "Everything, abridged" } }
62
+ ```
63
+
64
+ ### Controller integration
65
+
66
+ You can integrate the serialization system in rails, giving you automatic [Content-Type negotiation](https://en.wikipedia.org/wiki/Content_negotiation) using the `Accept` header:
67
+
68
+ ```ruby
69
+ require 'media_types/serialization'
70
+
71
+ class BookController < ActionController::API
72
+ include MediaTypes::Serialization
73
+
74
+ allow_output_serializer(BookSerializer, only: %i[show])
75
+ freeze_io!
76
+
77
+ def show
78
+ book = Book.new
79
+ book.title = 'Everything, abridged'
80
+
81
+ render_media book
82
+ end
83
+ end
84
+ ```
85
+
86
+ While using the controller integration the context will always be set to the current controller.
87
+ This allows you to construct urls.
88
+
89
+ ### Adding HATEOAS responses to existing routes
90
+
91
+ When creating a mobile application it's often useful to allow the app to request a non-html representation of a specific url.
92
+ If you have an existing route:
93
+
94
+ ```ruby
95
+ class BookController < ApplicationController
96
+ def show
97
+ @book = Book.new
98
+
99
+ # Use view corresponding to the controller
100
+ end
101
+ end
102
+ ```
103
+
104
+ You can add a json representation as follows:
105
+
106
+ ```ruby
107
+ class BookController < ApplicationController
108
+ allow_output_serializer(BookSerializer, only: %i[show])
109
+ allow_output_html
110
+ freeze_io!
111
+
112
+ def show
113
+ @book = Book.new
114
+
115
+ render_media @book
116
+ end
117
+ end
118
+ ```
119
+
120
+ ### Validations
121
+
122
+ Right now the serializer does not validate incoming or outgoing information.
123
+ This can cause issues when you accidentally emit non-conforming data that people start to depend on.
124
+ To make sure you don't do that you can specify a [Media Type validator](https://github.com/SleeplessByte/media-types-ruby):
125
+
126
+ ```ruby
127
+ require 'media_types'
128
+
129
+ class BookValidator
130
+ include MediaTypes::Dsl
131
+
132
+ def self.organisation
133
+ 'acme'
134
+ end
135
+
136
+ use_name 'book'
137
+
138
+ validations do
139
+ version 1 do
140
+ attribute :book do
141
+ attribute :title, String
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ class BookSerializer < MediaTypes::Serialization::Base
148
+ validator BookValidator
149
+
150
+ # outputs with a Content-Type of application/vnd.acme.book.v1+json
151
+ output version: 1 do |obj, version, context|
152
+ {
153
+ book: {
154
+ title: obj.title
155
+ }
156
+ }
157
+ end
158
+ end
159
+ ```
160
+
161
+ For more information, see the [Media Types docs](https://github.com/SleeplessByte/media-types-ruby).
162
+
163
+ ### Versioning
164
+
165
+ To help with supporting older versions, serializers have a [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) to construct json objects:
166
+
167
+ ```ruby
168
+ class BookSerializer < MediaTypes::Serialization::Base
169
+ validator BookValidator
170
+
171
+ output versions: [1, 2] do |obj, version, context|
172
+ attribute :book do
173
+ attribute :title, obj.title
174
+ attribute :description, obj.description if version >= 2
175
+ end
176
+ end
177
+ end
178
+ ```
179
+
180
+ ```ruby
181
+ BookSerializer.serialize(book, BookValidator.version(1), context: nil)
182
+ # => { "book": { "title": "Everything, abridged" } }
183
+
184
+ BookSerializer.serialize(book, BookValidator.version(2), context: nil)
185
+ # => { "book": { "title": "Everything, abridged", "description": "Mu" } }
186
+ ```
187
+
188
+ ### Links
189
+
190
+ When making [HATEOAS](https://docs.delftsolutions.nl/wiki/HATEOAS_API) compliant applications it's very useful to include `Link` headers in your response so clients can use a `HEAD` request instead of having to fetch the entire resource.
191
+ Serializers have convenience methods to help with this:
192
+
193
+ ```ruby
194
+ class BookSerializer < MediaTypes::Serialization::Base
195
+ validator BookValidator
196
+
197
+ output versions: [1, 2, 3] do |obj, version, context|
198
+ attribute :book do
199
+ link :self, href: context.book_url(obj) if version >= 3
200
+
201
+ attribute :title, obj.title
202
+ attribute :description, obj.description if version >= 2
203
+ end
204
+ end
205
+ end
206
+ ```
207
+
208
+ This returns the following response:
209
+
210
+ ```ruby
211
+ BookSerializer.serialize(book, BookValidator.version(3), context: controller)
212
+ # header = Link: <https://example.org/>; rel="self"
213
+ # => {
214
+ # "book": {
215
+ # "_links": {
216
+ # "self": { "href": "https://example.org" }
217
+ # },
218
+ # "title": "Everything, abridged",
219
+ # "description": "Mu"
220
+ # }
221
+ # }
222
+ ```
223
+
224
+ ### Collections
225
+
226
+ There are convenience methods for serializing arrays of objects based on a template.
227
+
228
+ #### Indexes (index based collections)
229
+
230
+ An index is a collection of urls that point to members of the array.
231
+ The index method automatically generates it based on the `self` links defined in the default view (`view: nil`) of the given version.
232
+
233
+ ```ruby
234
+ class BookSerializer < MediaTypes::Serialization::Base
235
+ validator BookValidator
236
+
237
+ output versions: [1, 2, 3] do |obj, version, context|
238
+ attribute :book do
239
+ link :self, href: context.book_url(obj) if version >= 3
240
+
241
+ attribute :title, obj.title
242
+ attribute :description, obj.description if version >= 2
243
+ end
244
+ end
245
+
246
+ output view: :index, version: 3 do |arr, version, context|
247
+ attribute :books do
248
+ link :self, href: context.book_index_url
249
+
250
+ index arr, version: version
251
+ end
252
+ end
253
+ end
254
+ ```
255
+
256
+ ```ruby
257
+ BookSerializer.serialize([book], BookValidator.view(:index).version(3), context: controller)
258
+ # header = Link: <https://example.org/index>; rel="self"
259
+ # => {
260
+ # "books": {
261
+ # "_links": {
262
+ # "self": { "href": "https://example.org" }
263
+ # },
264
+ # "_index": [
265
+ # { "href": "https://example.org" }
266
+ # ]
267
+ # }
268
+ # }
269
+ ```
270
+
271
+ ##### How to validate?
272
+
273
+ The `index` dsl does _not_ exist in the validation gem.
274
+ This is how you validate indices:
275
+
276
+ ```ruby
277
+ class BookValidator
278
+ include MediaTypes::Dsl
279
+
280
+ def self.organisation
281
+ 'acme'
282
+ end
283
+
284
+ use_name 'book'
285
+
286
+ validations do
287
+ view :index do
288
+ version 3 do
289
+ attribute :books do
290
+ link :self
291
+
292
+ collection :_index, allow_empty: true do
293
+ attribute :href, String
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
300
+ ```
301
+
302
+ If the `:self` link contains _more attributes_, they will show up here too.
303
+
304
+ ```ruby
305
+ class BookSerializer < MediaTypes::Serialization::Base
306
+ validator BookValidator
307
+
308
+ output versions: [1, 2, 3] do |obj, version, context|
309
+ attribute :book do
310
+ link :self, href: context.book_url(obj), isbn: obj.isbn if version >= 3
311
+
312
+ attribute :title, obj.title
313
+ attribute :description, obj.description if version >= 2
314
+ end
315
+ end
316
+
317
+ output view: :index, version: 3 do |arr, version, context|
318
+ attribute :books do
319
+ link :self, href: context.book_index_url
320
+
321
+ index arr, version: version
322
+ end
323
+ end
324
+ end
325
+ ```
326
+
327
+ ```ruby
328
+ class BookValidator
329
+ include MediaTypes::Dsl
330
+
331
+ def self.organisation
332
+ 'acme'
333
+ end
334
+
335
+ use_name 'book'
336
+
337
+ validations do
338
+ view :index do
339
+ version 3 do
340
+ attribute :books do
341
+ link :self
342
+
343
+ collection :_index, allow_empty: true do
344
+ attribute :href, String
345
+ attribute :isbn, AllowNil(String)
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
352
+ ```
353
+
354
+ #### Collections (embedding collections)
355
+
356
+ A collection inlines the member objects.
357
+ The collection method automatically generates it based on the default view of the same version.
358
+
359
+ ```ruby
360
+ class BookSerializer < MediaTypes::Serialization::Base
361
+ validator BookValidator
362
+
363
+ output versions: [1, 2, 3] do |obj, version, context|
364
+ attribute :book do
365
+ link :self, href: context.book_url(obj) if version >= 3
366
+
367
+ attribute :title, obj.title
368
+ attribute :description, obj.description if version >= 2
369
+ end
370
+ end
371
+
372
+ output view: :index, version: 3 do |arr, version, context|
373
+ attribute :books do
374
+ link :self, href: context.book_index_url
375
+
376
+ index arr, version: version
377
+ end
378
+ end
379
+
380
+ output view: :collection, version: 3 do |arr, version, context|
381
+ attribute :books do
382
+ link :self, href: context.book_collection_url
383
+
384
+ collection arr, version: version
385
+ end
386
+ end
387
+ end
388
+ ```
389
+
390
+ ```ruby
391
+ BookSerializer.serialize([book], BookValidator.view(:collection).version(3), context: controller)
392
+ # header = Link: <https://example.org/collection>; rel="self"
393
+ # => {
394
+ # "books": {
395
+ # "_links": {
396
+ # "self": { "href": "https://example.org" }
397
+ # },
398
+ # "_embedded": [
399
+ # {
400
+ # "_links": {
401
+ # "self": { "href": "https://example.org" }
402
+ # },
403
+ # "title": "Everything, abridged",
404
+ # "description": "Mu"
405
+ # }
406
+ # ]
407
+ # }
408
+ # }
409
+ ```
410
+
411
+ The `collection` dsl is _not_ the same as the one in the validation gem.
412
+ This is how you could validate collections:
413
+
414
+ ```ruby
415
+ class BookValidator
416
+ include MediaTypes::Dsl
417
+
418
+ def self.organisation
419
+ 'acme'
420
+ end
421
+
422
+ use_name 'book'
423
+
424
+ validations do
425
+ view :collection do
426
+ version 3 do
427
+ attribute :books do
428
+ link :self
429
+
430
+ collection :_embedded, allow_empty: true do
431
+ link :self
432
+
433
+ attribute :title, String
434
+ attribute :description, AllowNil(String)
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end
441
+ ```
442
+
443
+ ### Input deserialization
444
+
445
+ You can mark a media type as something that's allowed to be sent along with a PUT request as follows:
446
+
447
+ ```ruby
448
+ class BookSerializer < MediaTypes::Serialization::Base
449
+ validator BookValidator
450
+
451
+ output versions: [1, 2, 3] do |obj, version, context|
452
+ attribute :book do
453
+ link :self, href: context.book_url(obj) if version >= 3
454
+
455
+ attribute :title, obj.title
456
+ attribute :description, obj.description if version >= 2
457
+ end
458
+ end
459
+
460
+ input version: 3
461
+ end
462
+
463
+ class BookController < ActionController::API
464
+ include MediaTypes::Serialization
465
+
466
+ allow_output_serializer(BookSerializer, only: %i[show])
467
+ allow_input_serializer(BookSerializer, only: %i[create])
468
+ freeze_io!
469
+
470
+ def show
471
+ book = Book.new
472
+ book.title = 'Everything, abridged'
473
+
474
+ render_media book
475
+ end
476
+
477
+ def create
478
+ json = deserialize(request) # does validation for us
479
+ puts json
480
+ end
481
+ end
482
+ ```
483
+
484
+ If you use [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) you might want to convert the verified json data during deserialization:
485
+
486
+ ```ruby
487
+ class BookSerializer < MediaTypes::Serialization::Base
488
+ validator BookValidator
489
+
490
+ output versions: [1, 2, 3] do |obj, version, context|
491
+ attribute :book do
492
+ link :self, href: context.book_url(obj) if version >= 3
493
+
494
+ attribute :title, obj.title
495
+ attribute :description, obj.description if version >= 2
496
+ end
497
+
498
+ input versions: [1, 2, 3] do |json, version, context|
499
+ book = Book.new
500
+ book.title = json['book']['title']
501
+ book.description = 'Not available'
502
+ book.description = json['book']['description'] if version >= 2
503
+
504
+ # Best practise is to only save in the controller.
505
+ book
506
+ end
507
+ end
508
+
509
+ class BookController < ActionController::API
510
+ include MediaTypes::Serialization
511
+
512
+ allow_output_serializer(BookSerializer, only: %i[show])
513
+ allow_input_serializer(BookSerializer, only: %i[create])
514
+ freeze_io!
515
+
516
+ def show
517
+ book = Book.new
518
+ book.title = 'Everything, abridged'
519
+
520
+ render_media book
521
+ end
522
+
523
+ def create
524
+ book = deserialize(request)
525
+ book.save!
526
+
527
+ render_media book
528
+ end
529
+ end
530
+ ```
531
+
532
+ If you don't want to apply any input validation or deserialization you can use the `allow_all_input` method instead of `allow_input_serialization`.
533
+
534
+ ### Raw output
535
+
536
+ Sometimes you need to output raw data.
537
+ This cannot be validated.
538
+ You do this as follows:
539
+
540
+ ```ruby
541
+ class BookSerializer < MediaTypes::Serialization::Base
542
+ validator BookValidator
543
+
544
+ output_raw view: :raw, version: 3 do |obj, version, context|
545
+ hidden do
546
+ # Make sure links are only set in the headers, not in the body.
547
+
548
+ link :self, href: context.book_url(obj)
549
+ end
550
+
551
+ "I'm a non-json output"
552
+ end
553
+ end
554
+ ```
555
+
556
+ ### Raw input
557
+
558
+ You can do the same with input:
559
+
560
+ ```ruby
561
+ class BookSerializer < MediaTypes::Serialization::Base
562
+ validator BookValidator
563
+
564
+ input_raw view: raw, version: 3 do |bytes, version, context|
565
+ book = Book.new
566
+ book.description = bytes
567
+
568
+ book
569
+ end
570
+ end
571
+ ```
572
+
573
+ ### Remapping media type identifiers
574
+
575
+ Sometimes you already have old clients using an `application/json` media type identifier when they do requests.
576
+ While this is not a good practice as this makes it hard to add new fields or remove old ones, this library has support for migrating away:
577
+
578
+ ```ruby
579
+ class BookSerializer < MediaTypes::Serialization::Base
580
+ validator BookValidator
581
+
582
+ # applicaton/vnd.acme.book.v3+json
583
+ # applicaton/vnd.acme.book.v2+json
584
+ # applicaton/vnd.acme.book.v1+json
585
+ output versions: [1, 2, 3] do |obj, version, context|
586
+ attribute :book do
587
+ link :self, href: context.book_url(obj) if version >= 3
588
+
589
+ attribute :title, obj.title
590
+ attribute :description, obj.description if version >= 2
591
+ end
592
+ end
593
+
594
+ # applicaton/vnd.acme.book+json
595
+ output version: nil do |obj, version, context|
596
+ attribute :book do
597
+ attribute :title, obj.title
598
+ end
599
+ end
600
+ output_alias 'application/json' # maps application/json to to applicaton/vnd.acme.book+json
601
+
602
+ # applicaton/vnd.acme.book.v3.create+json
603
+ # applicaton/vnd.acme.book.v2.create+json
604
+ # applicaton/vnd.acme.book.v1.create+json
605
+ input view: :create, versions: [1, 2, 3] do |json, version, context|
606
+ book = Book.new
607
+ book.title = json['book']['title']
608
+ book.description = 'Not available'
609
+ book.description = json['book']['description'] if version >= 2
610
+
611
+ # Make sure not to save here but only save in the controller
612
+ book
613
+ end
614
+
615
+ # applicaton/vnd.acme.book.create+json
616
+ input view: :create, version: nil do |json, version, context|
617
+ book = Book.new
618
+ book.title = json['book']['title']
619
+ book.description = 'Not available'
620
+
621
+ # Make sure not to save here but only save in the controller
622
+ book
623
+ end
624
+ input_alias 'application/json', view: :create # maps application/json to to applicaton/vnd.acme.book.create+json
625
+ ```
626
+
627
+ Validation will be done using the remapped validator. Aliasses map to version `nil`.
628
+ It is not possible to configure this version.
629
+
630
+ ### HTML
631
+
632
+ This library has a built in API viewer.
633
+ The viewer can be accessed by by appending a `?api_viewer=last` query parameter to the URL.
634
+
635
+ To enable the API viewer, use: `allow_api_viewer` in the controller.
636
+
637
+ ```ruby
638
+ class BookController < ActionController::API
639
+ include MediaTypes::Serialization
640
+
641
+ allow_api_viewer
642
+
643
+ allow_output_serializer(BookSerializer, only: %i[show])
644
+ allow_input_serializer(BookSerializer, only: %i[create])
645
+ freeze_io!
646
+
647
+ def show
648
+ book = Book.new
649
+ book.title = 'Everything, abridged'
650
+
651
+ render_media book
652
+ end
653
+
654
+ def create
655
+ json = deserialize(request) # does validation for us
656
+ puts json
657
+ end
658
+ end
659
+ ```
660
+
661
+ You can also output custom HTML:
662
+
663
+ ```ruby
664
+ class BookSerializer < MediaTypes::Serialization::Base
665
+ validator BookValidator
666
+
667
+ output versions: [1, 2, 3] do |obj, version, context|
668
+ attribute :book do
669
+ link :self, href: context.book_url(obj) if version >= 3
670
+
671
+ attribute :title, obj.title
672
+ attribute :description, obj.description if version >= 2
673
+ end
674
+ end
675
+
676
+ output_raw view: :html do |obj, context|
677
+ render_view 'book/show', context: context, assigns: {
678
+ title: obj.title,
679
+ description: obj.description
680
+ }
681
+ end
682
+
683
+ output_alias 'text/html', view: :html
684
+ end
685
+ ```
686
+
687
+ #### Errors
688
+
689
+ This library adds support for returning errors to clients using the [`application/problem+json`](https://tools.ietf.org/html/rfc7231) media type.
690
+ You can catch and transform application errors by adding an `output_error` call before `freeze_io!`:
691
+
692
+ ```ruby
693
+ class BookController < ActionController::API
694
+ include MediaTypes::Serialization
695
+
696
+ output_error CanCan::AccessDenied do |problem_output, error|
697
+ problem_output.title 'You do not have enough permissions to perform this action.', lang: 'en'
698
+ problem_output.title 'Je hebt geen toestemming om deze actie uit te voeren.', lang: 'nl-NL'
699
+
700
+ problem_output.status_code :forbidden
701
+ end
702
+
703
+ freeze_io!
704
+
705
+ # ...
706
+ end
707
+ ```
708
+
709
+ The exception you specified will be rescued by the controller and will be displayed to the user along with a link to the shared wiki page for that error type.
710
+ Feel free to add instructions there on how clients should solve this problem.
711
+ You can find more information at: [https://docs.delftsolutions.nl/wiki/Error](https://docs.delftsolutions.nl/wiki/Error)
712
+ If you want to override this url you can use the `problem_output.url(href)` function.
713
+
714
+ By default the `message` property of the error is used to fill the `details` field.
715
+ You can override this by using the `problem_output.override_detail(description, lang:)` function.
716
+
717
+ Custom attributes can be added using the `problem_output.attribute(name, value)` function.
718
+
719
+ ### Related
720
+
721
+ - [`MediaTypes`](https://github.com/SleeplessByte/media-types-ruby): :gem: Library to create media type validators.
722
+
723
+ ## API
724
+
725
+ ### Serializer class definition
726
+
727
+ These methods become available during class definition if you inherit from `MediaTypes::Serialization::Base`.
728
+
729
+ #### `unvalidated( prefix )`
730
+
731
+ Disabled validation for this serializer. Prefix is of the form `application/vnd.<organisation>.<name>`.
732
+
733
+ Either unvalidated or validator must be used while defining a serializer.
734
+
735
+ #### `validator( media_type_validator )`
736
+
737
+ Enabled validation for this serializer using a [Media Type Validator](https://github.com/SleeplessByte/media-types-ruby).
738
+
739
+ Either validator or unvalidated must be used while defining a serializer.
740
+
741
+ #### `output( view:, version:, versions: ) do |obj, version, context|`
742
+
743
+ Defines a serialization block. Either version or versions can be set.
744
+ `nil` is allowed for unversioned.
745
+ View should be a symbol or unset.
746
+
747
+ Obj is the object to be serialized, version is the negotiated version and context is the context passed in from the serialize function.
748
+ When using the controller integration, context is the current controller.
749
+
750
+ The block should return an object to convert into JSON.
751
+
752
+ #### `output_raw( view:, version:, versions:, suffix: ) do |obj, version, context|`
753
+
754
+ This has the same behavior as `output` but should return a string instead of an object.
755
+ Output is not validated.
756
+ By default, `input_raw` is expected to _not_ be JSON.
757
+ Override `suffix` with `:json` if it _is_ JSON.
758
+
759
+ #### `output_alias( media_type_identifier, view:, hide_variant: false, suffix: '~' )`
760
+
761
+ Defines a legacy mapping. This will make the deserializer parse the media type `media_type_identifier` as if it was version `nil` of the specified view.
762
+ If `view` is undefined it will use the output serializer without a view defined.
763
+ By default, suffix is `:json` if `media_type_identifier` is a JSON type.
764
+
765
+ Response will have a content type equal to `[media_type_identifier]; variant=[mapped_media_type_identifier]`.
766
+ If `hide_variant:` is true, the content type emitted will only be `[media_type_identifier]`.
767
+
768
+ > You cannot alias a _versioned_ media type, otherwise it would be easy to later break the definition by changing the version it aliases.
769
+
770
+ #### `output_alias_optional( media_type_identifier, view:, hide_variant: false, suffix: '~' )`
771
+
772
+ Has the same behavior as `output_alias` but can be used by multiple serializers.
773
+ The serializer that is loaded last in the controller 'wins' control over this media type identifier.
774
+ If any of the serializers have an `output_alias` defined with the same media type identifier that one will win instead.
775
+ By default, suffix is `:json` if `media_type_identifier` is a JSON type.
776
+
777
+ Response will have a content type equal to `[media_type_identifier]; variant=[mapped_media_type_identifier]`. If `hide_variant:` is true, the content type emitted will only be `[media_type_identifier]`.
778
+
779
+ #### `input( view:, version:, versions: ) do |obj, version, context|`
780
+
781
+ Defines a deserialization block. Either version or versions can be set.
782
+ View should be a symbol or unset.
783
+
784
+ Obj is the object to be serialized, version is the negotiated version and context is the context passed in from the serialize function.
785
+ When using the controller integration, context is the current controller.
786
+
787
+ The block should return the internal representation of the object.
788
+ Best practise is to make sure not to change state in this function but to leave that up to the controller.
789
+
790
+ #### `input_raw( view:, version:, versions:, suffix: nil ) do |bytes, version, context|`
791
+
792
+ This has the same behavior as `input` but takes in raw data.
793
+ Input is not validated.
794
+ By default, `input_raw` is expected to _not_ be JSON.
795
+ Override `suffix` with `:json` if it _is_ JSON.
796
+
797
+ #### `input_alias( media_type_identifier, view:, suffix: '~' )`
798
+
799
+ Defines a legacy mapping.
800
+ This will make the serializer parse the media type `media_type_identifier` as if it was version `nil` of the specified view.
801
+ If view is undefined it will use the input serializer without a view defined.
802
+ By default, suffix is `:json` if `media_type_identifier` is a JSON type.
803
+
804
+ > You cannot alias a _versioned_ media type, otherwise it would be easy to later break the definition by changing the version it aliases.
805
+
806
+ #### `input_alias_optional( media_type_identifier, view:, suffix: '~' )`
807
+
808
+ Has the same behavior as `input_alias` but can be used by multiple serializers.
809
+ The serializer that is loaded last in the controller 'wins' control over this media type identifier.
810
+ If any of the serializers have an `input_alias` defined with the same media type identifier that one will win instead.
811
+ By default, suffix is `:json` if `media_type_identifier` is a JSON type.
812
+
813
+ #### `disable_wildcards`
814
+
815
+ Disables registering wildcard media types.
816
+
817
+ ### Serializer output definition
818
+
819
+ The following methods are available within an `output ... do` block.
820
+
821
+ #### `attribute( key, value = {} ) do`
822
+
823
+ Sets a value for the given key.
824
+ If a block is given, any `attribute`, `link`, `collection` and `index` statements are run in context of `value`.
825
+
826
+ Returns the built up context so far.
827
+
828
+ #### `link( rel, href:, emit_header: true, **attributes )`
829
+
830
+ Adds a `_link` block to the current context. Also adds the specified link to the HTTP Link header.
831
+ `attributes` allows passing in custom attributes.
832
+
833
+ If `emit_header` is `true` the link will also be emitted as a http header.
834
+
835
+ Returns the built up context so far.
836
+
837
+ #### `index( array, serializer, version:, view: nil )`
838
+
839
+ > Not the same as a validator `collection`.
840
+
841
+ Adds an `_index` block to the current context. Uses the self links of the specified view to construct an index of urls to the child objects.
842
+
843
+ Returns the built up context so far.
844
+
845
+ #### `collection( array, serializer, version:, view: nil )`
846
+
847
+ > Not the same as a validator `collection`.
848
+
849
+ Adds an `_embedded` block to the current context. Uses the specified serializer to embed the child objects.
850
+ Optionally a block can be used to modify the output from the child serializer.
851
+
852
+ Returns the built up context so far.
853
+
854
+ #### `hidden do`
855
+
856
+ Sometimes you want to add links without actually modifying the object.
857
+ Calls to `attribute`, `link`, `index`, `collection` made inside this block won't modify the context.
858
+ Any calls to link will only set the HTTP Link header.
859
+
860
+ Returns the unmodified context.
861
+
862
+ #### `emit`
863
+
864
+ Can be added to the end of a block to fix up the return value to return the built up context so far.
865
+
866
+ Returns the built up context so far.
867
+
868
+ #### `object do`
869
+
870
+ Runs a block in a new context and returns the result
871
+
872
+ > Most common use-case is emitting from an enumerable.
873
+ >
874
+ > ```ruby
875
+ > results = [item, item, item].map do |current_item|
876
+ > object do
877
+ > attribute :foo, current_item.bar
878
+ > end
879
+ > end
880
+ >
881
+ > attribute :items, results
882
+ > ```
883
+
884
+ #### `render_view( view, context:, **args)`
885
+
886
+ Can be used to render a view.
887
+ You can set local variables in the view by assigning a hash to the `assigns:` parameter.
888
+ Returns a `string`
889
+
890
+ > When possible, prefer `output_raw` with context.render_to_string(params)`
891
+
892
+ #### `redirect_to(url, context, **options)`
893
+
894
+ This redirects the user to the specified url when this serializer is rendered. The output of the serializer is still shown in the API viewer.
895
+
896
+ #### `suppress_render do |result|`
897
+
898
+ Replaces the render at the end of `render_media` and substitutes it with the contents of the block.
899
+
900
+ ### Controller definition
901
+
902
+ These functions are available during the controller definition if you add `include MediaTypes::Serialization`.
903
+
904
+ #### `allow_output_serializer( serializer, views: nil, **filters )`
905
+
906
+ Configure the controller to allow the client to request responses emitted by the specified serializer.
907
+ Optionally allows you to specify which views to allow by passing an array in the views parameter.
908
+
909
+ Accepts the same filters as `before_action`.
910
+
911
+ #### `allow_output_html( as: nil, view: nil, layout: nil, **filters )`
912
+
913
+ Allows falling back to the default Rails view rendering when the client asks for the media type in the `as:` parameter or `text/html` if `as:` is unset.
914
+
915
+ The `Content-Type` of the response will be `text/html` if the `as:` parameter is unset.
916
+ If the `as:` parameter is set, it will include it in the variant parameter: `text/html; variant=application/vnd.xpbytes.borderless`.
917
+
918
+ Accepts the same filters as `before_action`.
919
+ You can set the template to use using the `view:` parameter.
920
+
921
+ #### `allow_output_docs( description, **filters )`
922
+
923
+ Outputs the specified description as help information.
924
+
925
+ Accepts the same filters as `before_action`.
926
+
927
+ #### `output_error(klazz, serializers = []) do`
928
+
929
+ Wraps the controller method in a `rescue_from` and presents the users with a `text/html` or `application/problem+json` representation of the error.
930
+ The `text/html` response can be overridden by supplying an additional serializer in the `serializers` array. It will use the nil view for the given serializers.
931
+
932
+ #### `allow_input_serializer( serializer, views: nil, **filters )`
933
+
934
+ Configure the controller to allow the client to send bodies with a `Content-Type` that can be deserialized using the specified serializer.
935
+ Optionally allows you to specify which views to allow by passing an array in the views parameter.
936
+
937
+ Accepts the same filters as `before_action`.
938
+
939
+ #### `allow_all_input( **filters )`
940
+
941
+ Disables input deserialization. Running `deserialize` while allowing all input will result in an error being thrown.
942
+
943
+ #### `not_acceptable_serializer( serializer )`
944
+
945
+ Replaces the serializer used to render the error page when no media type could be negotiated using the `Accept` header.
946
+
947
+ #### `unsupported_media_type_serializer( serializer )`
948
+
949
+ Adds a serializer that can be used to render the error page when the client submits a body with a `Content-Type` that was not added to the whitelist using `allow_input_serialization`.
950
+
951
+ #### `clear_unsupported_media_type_serializers!`
952
+
953
+ Clears the list of serializers used to render the error when the client supplies non-valid input.
954
+
955
+ #### `input_validation_failed_serializer( serializer )`
956
+
957
+ Adds a serializer that can be used to render the error page when input validation fails.
958
+
959
+ #### `clear_input_validation_failed_serializers!`
960
+
961
+ Clears the list of serializers used to render the error when the client supplies non-valid input.
962
+
963
+ #### `allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)`
964
+
965
+ Enables rendering the api viewer when adding the `api_viewer=last` query parameter to the url.
966
+
967
+ #### `freeze_io!(**filter_opts)`
968
+
969
+ Registers serialization and deserialization in the controller.
970
+ This function must be called before using the controller.
971
+
972
+ ### Controller usage
973
+
974
+ These functions are available during method execution in the controller.
975
+
976
+ #### `render_media( obj, serializers: nil, not_acceptable_serializer: nil, **options ) do`
977
+
978
+ Serializes an object and renders it using the appropriate content type.
979
+ Options are passed through to the controller `render` function. Allows you to specify different objects to different serializers using a block:
980
+
981
+ ```ruby
982
+ render_media do
983
+ serializer BookSerializer, book
984
+ serializer BooksSerializer do
985
+ [ book ]
986
+ end
987
+ end
988
+ ```
989
+
990
+ **Warning**: this block can be called multiple times when used together with recursive serializers like the API viewer.
991
+ Try to _avoid changing state_ in this block.
992
+
993
+ If you want to render with different serializers than defined in the controller you can pass an array of serializers in the `serializers` property.
994
+
995
+ If you want to override the serializer that is used to render the response when no acceptable Content-Type could be negotiated you can pass the desired serializer in the `not_acceptable_serializer` property.
996
+
997
+ This method throws a `MediaTypes::Serialization::OutputValidationFailedError` error if the output does not conform to the format defined by the configured validator.
998
+ Best practise is to return a 500 error to the client.
999
+
1000
+ If no acceptable Content-Type could be negotiated the response will be rendered using the serialized defined by the class `not_acceptable_serializer` function or by the `not_acceptable_serializer` property.
1001
+
1002
+ Due to the way this gem is implemented it is not possible to use instance variables (`@variable`) in the `render_media` do block.
1003
+
1004
+ #### `deserialize( request )`
1005
+
1006
+ Deserializes the request body using the configured input serializers and returns the deserialized object.
1007
+
1008
+ Returns nil if no input body was given by the client.
1009
+
1010
+ This method throws a `MediaTypes::Serialization::InputValidationFailedError` error if the incoming data does not conform to the specified schema.
1011
+
1012
+ #### `deserialize!( request )`
1013
+
1014
+ Does the same as `deserialize( request )` but gives the client an error page if no input was supplied.
1015
+
1016
+ #### `resolve_serializer(request, identifier = nil, registration = @serialization_output_registration)`
1017
+
1018
+ Returns the serializer class that will handle the given request.
1019
+
1020
+ ## Customization
1021
+
1022
+ The easiest way to customize the look and feel of the built in pages is to provide your own logo and background in an initializer:
1023
+
1024
+ ```ruby
1025
+ # config/initializers/serialization.rb
1026
+
1027
+ MediaTypes::Serialization::Serializers::CommonCSS.background = 'linear-gradient(245deg, #3a2f28 0%, #201a16 100%)'
1028
+ MediaTypes::Serialization::Serializers::CommonCSS.logo_width = 12
1029
+ MediaTypes::Serialization::Serializers::CommonCSS.logo_data = <<-HERE
1030
+ <svg height="150" width="500">
1031
+ <ellipse cx="240" cy="100" rx="220" ry="30" style="fill:purple" />
1032
+ <ellipse cx="220" cy="70" rx="190" ry="20" style="fill:lime" />
1033
+ <ellipse cx="210" cy="45" rx="170" ry="15" style="fill:yellow" />
1034
+ </svg>
1035
+ HERE
1036
+ ```
1037
+
1038
+ ## Development
1039
+
1040
+ After checking out the repo, run `bin/setup` to install dependencies.
1041
+ Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1042
+
1043
+ To install this gem onto your local machine, run `bundle exec rake install`.
1044
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1045
+
1046
+ ## Contributing
1047
+
1048
+ Bug reports and pull requests are welcome on GitHub at [XPBytes/media_types-serialization](https://github.com/XPBytes/media_types-serialization).