media_types-serialization 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +34 -34
  4. data/.github/workflows/publish-sid.yml +34 -34
  5. data/.gitignore +22 -22
  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 +207 -200
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/Gemfile.lock +176 -169
  19. data/LICENSE.txt +21 -21
  20. data/README.md +1058 -1048
  21. data/Rakefile +10 -10
  22. data/bin/console +14 -14
  23. data/bin/setup +8 -8
  24. data/lib/media_types/problem.rb +67 -67
  25. data/lib/media_types/serialization/base.rb +269 -269
  26. data/lib/media_types/serialization/error.rb +193 -193
  27. data/lib/media_types/serialization/fake_validator.rb +53 -53
  28. data/lib/media_types/serialization/serialization_dsl.rb +139 -135
  29. data/lib/media_types/serialization/serialization_registration.rb +245 -245
  30. data/lib/media_types/serialization/serializers/api_viewer.rb +383 -383
  31. data/lib/media_types/serialization/serializers/common_css.rb +212 -212
  32. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -80
  33. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -85
  34. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -58
  35. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +95 -93
  36. data/lib/media_types/serialization/serializers/problem_serializer.rb +111 -111
  37. data/lib/media_types/serialization/utils/accept_header.rb +77 -77
  38. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -82
  39. data/lib/media_types/serialization/version.rb +7 -7
  40. data/lib/media_types/serialization.rb +689 -689
  41. data/media_types-serialization.gemspec +48 -48
  42. metadata +3 -3
data/README.md CHANGED
@@ -1,1048 +1,1058 @@
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).
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
+ #### `varies_on_header(header)`
901
+
902
+ Indicates to clients that they can receive a different response if the indicated header has a different value. This is done using the [Vary header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary). When your responses are internationalized you can use the following example:
903
+
904
+ ```ruby
905
+ varies_on_header 'Accept-Language'
906
+ ```
907
+
908
+ The Vary header is set at two moments during execution. The first moment the header is modified is during the `freeze_io!` handler. The second moment is when `render_media` is called. If you want to modify the Vary header that is returned when the content negotiation fails you need to make sure to run your callback before the `freeze_io!` callback has been run.
909
+
910
+ ### Controller definition
911
+
912
+ These functions are available during the controller definition if you add `include MediaTypes::Serialization`.
913
+
914
+ #### `allow_output_serializer( serializer, views: nil, **filters )`
915
+
916
+ Configure the controller to allow the client to request responses emitted by the specified serializer.
917
+ Optionally allows you to specify which views to allow by passing an array in the views parameter.
918
+
919
+ Accepts the same filters as `before_action`.
920
+
921
+ #### `allow_output_html( as: nil, view: nil, layout: nil, **filters )`
922
+
923
+ 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.
924
+
925
+ The `Content-Type` of the response will be `text/html` if the `as:` parameter is unset.
926
+ If the `as:` parameter is set, it will include it in the variant parameter: `text/html; variant=application/vnd.xpbytes.borderless`.
927
+
928
+ Accepts the same filters as `before_action`.
929
+ You can set the template to use using the `view:` parameter.
930
+
931
+ #### `allow_output_docs( description, **filters )`
932
+
933
+ Outputs the specified description as help information.
934
+
935
+ Accepts the same filters as `before_action`.
936
+
937
+ #### `output_error(klazz, serializers = []) do`
938
+
939
+ Wraps the controller method in a `rescue_from` and presents the users with a `text/html` or `application/problem+json` representation of the error.
940
+ 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.
941
+
942
+ #### `allow_input_serializer( serializer, views: nil, **filters )`
943
+
944
+ Configure the controller to allow the client to send bodies with a `Content-Type` that can be deserialized using the specified serializer.
945
+ Optionally allows you to specify which views to allow by passing an array in the views parameter.
946
+
947
+ Accepts the same filters as `before_action`.
948
+
949
+ #### `allow_all_input( **filters )`
950
+
951
+ Disables input deserialization. Running `deserialize` while allowing all input will result in an error being thrown.
952
+
953
+ #### `not_acceptable_serializer( serializer )`
954
+
955
+ Replaces the serializer used to render the error page when no media type could be negotiated using the `Accept` header.
956
+
957
+ #### `unsupported_media_type_serializer( serializer )`
958
+
959
+ 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`.
960
+
961
+ #### `clear_unsupported_media_type_serializers!`
962
+
963
+ Clears the list of serializers used to render the error when the client supplies non-valid input.
964
+
965
+ #### `input_validation_failed_serializer( serializer )`
966
+
967
+ Adds a serializer that can be used to render the error page when input validation fails.
968
+
969
+ #### `clear_input_validation_failed_serializers!`
970
+
971
+ Clears the list of serializers used to render the error when the client supplies non-valid input.
972
+
973
+ #### `allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)`
974
+
975
+ Enables rendering the api viewer when adding the `api_viewer=last` query parameter to the url.
976
+
977
+ #### `freeze_io!(**filter_opts)`
978
+
979
+ Registers serialization and deserialization in the controller.
980
+ This function must be called before using the controller.
981
+
982
+ ### Controller usage
983
+
984
+ These functions are available during method execution in the controller.
985
+
986
+ #### `render_media( obj, serializers: nil, not_acceptable_serializer: nil, **options ) do`
987
+
988
+ Serializes an object and renders it using the appropriate content type.
989
+ Options are passed through to the controller `render` function. Allows you to specify different objects to different serializers using a block:
990
+
991
+ ```ruby
992
+ render_media do
993
+ serializer BookSerializer, book
994
+ serializer BooksSerializer do
995
+ [ book ]
996
+ end
997
+ end
998
+ ```
999
+
1000
+ **Warning**: this block can be called multiple times when used together with recursive serializers like the API viewer.
1001
+ Try to _avoid changing state_ in this block.
1002
+
1003
+ If you want to render with different serializers than defined in the controller you can pass an array of serializers in the `serializers` property.
1004
+
1005
+ 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.
1006
+
1007
+ This method throws a `MediaTypes::Serialization::OutputValidationFailedError` error if the output does not conform to the format defined by the configured validator.
1008
+ Best practise is to return a 500 error to the client.
1009
+
1010
+ 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.
1011
+
1012
+ Due to the way this gem is implemented it is not possible to use instance variables (`@variable`) in the `render_media` do block.
1013
+
1014
+ #### `deserialize( request )`
1015
+
1016
+ Deserializes the request body using the configured input serializers and returns the deserialized object.
1017
+
1018
+ Returns nil if no input body was given by the client.
1019
+
1020
+ This method throws a `MediaTypes::Serialization::InputValidationFailedError` error if the incoming data does not conform to the specified schema.
1021
+
1022
+ #### `deserialize!( request )`
1023
+
1024
+ Does the same as `deserialize( request )` but gives the client an error page if no input was supplied.
1025
+
1026
+ #### `resolve_serializer(request, identifier = nil, registration = @serialization_output_registration)`
1027
+
1028
+ Returns the serializer class that will handle the given request.
1029
+
1030
+ ## Customization
1031
+
1032
+ 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:
1033
+
1034
+ ```ruby
1035
+ # config/initializers/serialization.rb
1036
+
1037
+ MediaTypes::Serialization::Serializers::CommonCSS.background = 'linear-gradient(245deg, #3a2f28 0%, #201a16 100%)'
1038
+ MediaTypes::Serialization::Serializers::CommonCSS.logo_width = 12
1039
+ MediaTypes::Serialization::Serializers::CommonCSS.logo_data = <<-HERE
1040
+ <svg height="150" width="500">
1041
+ <ellipse cx="240" cy="100" rx="220" ry="30" style="fill:purple" />
1042
+ <ellipse cx="220" cy="70" rx="190" ry="20" style="fill:lime" />
1043
+ <ellipse cx="210" cy="45" rx="170" ry="15" style="fill:yellow" />
1044
+ </svg>
1045
+ HERE
1046
+ ```
1047
+
1048
+ ## Development
1049
+
1050
+ After checking out the repo, run `bin/setup` to install dependencies.
1051
+ Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1052
+
1053
+ To install this gem onto your local machine, run `bundle exec rake install`.
1054
+ 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).
1055
+
1056
+ ## Contributing
1057
+
1058
+ Bug reports and pull requests are welcome on GitHub at [XPBytes/media_types-serialization](https://github.com/XPBytes/media_types-serialization).