media_types-serialization 0.8.1 → 1.0.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +10 -1
  3. data/.gitignore +12 -12
  4. data/.idea/.rakeTasks +5 -5
  5. data/.idea/inspectionProfiles/Project_Default.xml +5 -5
  6. data/.idea/runConfigurations/test.xml +19 -19
  7. data/CHANGELOG.md +18 -0
  8. data/CODE_OF_CONDUCT.md +74 -74
  9. data/Gemfile +4 -4
  10. data/Gemfile.lock +58 -61
  11. data/LICENSE.txt +21 -21
  12. data/README.md +640 -173
  13. data/Rakefile +10 -10
  14. data/bin/console +14 -14
  15. data/bin/setup +8 -8
  16. data/lib/media_types/problem.rb +64 -0
  17. data/lib/media_types/serialization.rb +431 -172
  18. data/lib/media_types/serialization/base.rb +111 -91
  19. data/lib/media_types/serialization/error.rb +178 -0
  20. data/lib/media_types/serialization/fake_validator.rb +52 -0
  21. data/lib/media_types/serialization/serialization_dsl.rb +117 -0
  22. data/lib/media_types/serialization/serialization_registration.rb +235 -0
  23. data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
  24. data/lib/media_types/serialization/serializers/common_css.rb +168 -0
  25. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
  26. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
  27. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
  28. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
  29. data/lib/media_types/serialization/serializers/problem_serializer.rb +87 -0
  30. data/lib/media_types/serialization/version.rb +1 -1
  31. data/media_types-serialization.gemspec +50 -50
  32. metadata +40 -43
  33. data/.travis.yml +0 -17
  34. data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
  35. data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
  36. data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
  37. data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
  38. data/lib/media_types/serialization/media_type/register.rb +0 -4
  39. data/lib/media_types/serialization/migrations_command.rb +0 -38
  40. data/lib/media_types/serialization/migrations_support.rb +0 -50
  41. data/lib/media_types/serialization/mime_type_support.rb +0 -64
  42. data/lib/media_types/serialization/no_content_type_given.rb +0 -11
  43. data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
  44. data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
  45. data/lib/media_types/serialization/renderer.rb +0 -41
  46. data/lib/media_types/serialization/renderer/register.rb +0 -4
  47. data/lib/media_types/serialization/wrapper.rb +0 -13
  48. data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
  49. data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -61
  50. data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -61
  51. data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
  52. data/lib/media_types/serialization/wrapper_support.rb +0 -38
data/LICENSE.txt CHANGED
@@ -1,21 +1,21 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2019 Derk-Jan Karrenbeld
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Derk-Jan Karrenbeld
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/media_types-serialization.svg)](https://badge.fury.io/rb/media_types-serialization)
5
5
  [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
6
6
 
7
- Add media types supported serialization using your favourite serializer
7
+ `respond_to` on steroids. Add versioned serialization and deserialization to your rails projects.
8
8
 
9
9
  ## Installation
10
10
 
@@ -21,269 +21,513 @@ And then execute:
21
21
  Or install it yourself as:
22
22
 
23
23
  $ gem install media_types-serialization
24
-
25
- If you have not done this before, and you're using `rails`, install the necessary parts using:
26
24
 
27
- ```bash
28
- rails g media_types:serialization:api_viewer
25
+ ## Usage
26
+
27
+ 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.
28
+
29
+ ### Creating a serializer
30
+
31
+ ```ruby
32
+ class BookSerializer < MediaTypes::Serialization::Base
33
+ unvalidated 'application/vnd.acme.book'
34
+
35
+ # outputs with a Content-Type of application/vnd.acme.book.v1+json
36
+ output version: 1 do |obj, version, context|
37
+ {
38
+ book: {
39
+ title: obj.title
40
+ }
41
+ }
42
+ end
43
+ end
29
44
  ```
30
45
 
31
- This will:
46
+ To convert a ruby object to a json representation:
32
47
 
33
- - Add the default `html_wrapper` layout which is an API Viewer used as fallback or the `.api_viewer` format
34
- - Add the default `template_controller` which allows the API Viewer to post templated links
35
- - Add the `route` for these templated link forms
36
- - Add an initializer that registers the `media` renderer and `api_viewer` media type
48
+ ```ruby
49
+ class Book
50
+ attr_accessor :title
51
+ end
37
52
 
38
- ## Usage
53
+ book = Book.new
54
+ book.title = 'Everything, abridged'
39
55
 
40
- In order to use media type serialization you only need to do 2 things:
56
+ BookSerializer.serialize(book, 'vnd.acme.book.v1+json', context: nil)
57
+ # => { "book": { "title": "Everything, abridged" } }
58
+ ```
41
59
 
42
- ### Serializer
60
+ ### Validations
61
+
62
+ Right now the serializer does not validate incoming or outgoing information. This can cause issues when you accidentally emit non-conforming data that people start to depend on. To make sure you don't do that you can specify a [Media Type validator](https://github.com/SleeplessByte/media-types-ruby):
43
63
 
44
- Add a serializer that can serialize a certain media type. The `to_hash` function will be called _explicitly_ in your
45
- controller, so you can always use your own, favourite serializer here to do the hefty work. This gem does provide some
46
- easy tools, usually enough to do most serialization.
47
-
48
64
  ```ruby
49
- class Book < ApplicationRecord
50
- class Serializer < MediaTypes::Serialization::Base
51
- serializes_media_type MyNamespace::MediaTypes::Book
52
-
53
- def fields
54
- if current_media_type.create?
55
- return %i[name author]
56
- end
57
-
58
- %i[name author updated_at views]
59
- end
65
+ require 'media_types'
66
+
67
+ class BookValidator
68
+ include MediaTypes::Dsl
60
69
 
61
- def to_hash
62
- extract(serializable, fields).tap do |result|
63
- result[:_links] = extract_links unless current_media_type.create?
70
+ def self.organisation
71
+ 'acme'
72
+ end
73
+
74
+ use_name 'book'
75
+
76
+ validations do
77
+ version 1 do
78
+ attribute :book do
79
+ attribute :title, String
64
80
  end
65
81
  end
82
+ end
83
+ end
66
84
 
67
- alias to_h to_hash
68
-
69
- protected
70
-
71
- def extract_self
72
- # A serializer gets the controller as context
73
- { href: context.api_book_url(serializable) }
74
- end
85
+ class BookSerializer < MediaTypes::Serialization::Base
86
+ validator BookValidator
75
87
 
76
- def extract_links
77
- {
78
- 'self': extract_self,
79
- 'signatures': { href: context.api_book_signatures_url(serializable) }
88
+ # outputs with a Content-Type of application/vnd.acme.book.v1+json
89
+ output version: 1 do |obj, version, context|
90
+ {
91
+ book: {
92
+ title: obj.title
80
93
  }
94
+ }
95
+ end
96
+ end
97
+ ```
98
+
99
+ For more information, see the [Media Types docs](https://github.com/SleeplessByte/media-types-ruby).
100
+
101
+ ### Controller integration
102
+
103
+ 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:
104
+
105
+ ```ruby
106
+ require 'media_types/serialization'
107
+
108
+ class BookController < ActionController::API
109
+ include MediaTypes::Serialization
110
+
111
+ allow_output_serializer(BookSerializer, only: %i[show])
112
+ freeze_io!
113
+
114
+ def show
115
+ book = Book.new
116
+ book.title = 'Everything, abridged'
117
+
118
+ render_media book
119
+ end
120
+ end
121
+ ```
122
+
123
+ While using the controller integration the context will always be set to the current controller. This allows you to construct urls.
124
+
125
+ ### Versioning
126
+
127
+ To help with supporting older versions, serializers have a [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) to construct json objects:
128
+
129
+ ```ruby
130
+ class BookSerializer < MediaTypes::Serialization::Base
131
+ validator BookValidator
132
+
133
+ output versions: [1, 2] do |obj, version, context|
134
+ attribute :book do
135
+ attribute :title, obj.title
136
+ attribute :description, obj.description if version >= 2
137
+ end
138
+ end
139
+ end
140
+ ```
141
+
142
+ ```ruby
143
+ BookSerializer.serialize(book, BookValidator.version(1), context: nil)
144
+ # => { "book": { "title": "Everything, abridged" } }
145
+
146
+ BookSerializer.serialize(book, BookValidator.version(2), context: nil)
147
+ # => { "book": { "title": "Everything, abridged", "description": "Mu" } }
148
+ ```
149
+
150
+ ### Links
151
+
152
+ When making [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) 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. Serializers have convenience methods to help with this:
153
+
154
+ ```ruby
155
+ class BookSerializer < MediaTypes::Serialization::Base
156
+ validator BookValidator
157
+
158
+ output versions: [1, 2, 3] do |obj, version, context|
159
+ attribute :book do
160
+ link :self, href: context.book_url(obj) if version >= 3
161
+
162
+ attribute :title, obj.title
163
+ attribute :description, obj.description if version >= 2
81
164
  end
82
165
  end
83
166
  end
84
167
  ```
85
- By default, The passed in `MediaType` gets converted into a constructable (via `to_constructable`) and invoked with the
86
- current `view` (e.g. `create`, `index`, `collection` or ` `). This means that by default it will be able to serialize
87
- the latest version you `MediaType` is reporting. The best way to supply your media type is via the [`media_types`](https://github.com/SleeplessByte/media-types-ruby) gem.
88
168
 
89
- #### Multiple suffixes, one serializer
169
+ This returns the following response:
90
170
 
91
- By default, the media renderer will automatically detect and inject the following:
92
- - suffix `+json` if you define `to_json`
93
- - suffix `+xml` if you define `to_xml`
94
- - type `text/html` if you define `to_html`
171
+ ```ruby
172
+ BookSerializer.serialize(book, BookValidator.version(3), context: controller)
173
+ # header = Link: <https://example.org/>; rel="self"
174
+ # => {
175
+ # "book": {
176
+ # "_links": {
177
+ # "self": { "href": "https://example.org" }
178
+ # },
179
+ # "title": "Everything, abridged",
180
+ # "description": "Mu"
181
+ # }
182
+ # }
183
+ ```
95
184
 
96
- If you do _not_ define these methods, only the `default` suffix / type will be used, `accepts_html` for the `text/html`
97
- content-type.
185
+ ### Collections
98
186
 
99
- If you don't define `to_html`, but try to make a serializer output `html`, it will be rendered in the layout at:
100
- `serializers/wrapper/html_wrapper.html.erb` (or any other templating extension).
187
+ There are convenience methods for serializing arrays of objects based on a template.
101
188
 
102
- #### Migrations (versions)
189
+ #### Indexes
103
190
 
104
- If the serializer can serialize multiple _versions_, you can supply them through `additional_versions: [2, 3]`. A way to
105
- handle this is via backward migrations, meaning you'll migrate from the current version back to an older version.
191
+ An index is a collection of urls that point to members of the array. The index method automatically generates it based on the self links defined in the default view of the same version.
106
192
 
107
193
  ```ruby
108
- class Book < ApplicationRecord
109
- class BasicSerializer < MediaTypes::Serialization::Base
110
-
111
- # Maybe it's currently at version 2, so tell the base that this also serializes version 1
112
- # You can also use a range to_a: (1...4).to_a
113
- #
114
- serializes_media_type MyNamespace::MediaTypes::Book, additional_versions: [1]
115
-
116
- def to_hash
117
- # This enables migrations right when it's being serialized
118
- #
119
- migrate do
120
- extract(serializable, fields).tap do |result|
121
- result[:_links] = extract_links unless current_media_type.create?
122
- end
123
- end
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
124
203
  end
204
+ end
205
+
206
+ output view: :index, version: 3 do |arr, version, context|
207
+ attribute :books do
208
+ link :self, href: context.book_index_url
209
+
210
+ index arr, version: version
211
+ end
212
+ end
213
+ end
214
+ ```
215
+
216
+ ```ruby
217
+ BookSerializer.serialize([book], BookValidator.view(:index).version(3), context: controller)
218
+ # header = Link: <https://example.org/index>; rel="self"
219
+ # => {
220
+ # "books": {
221
+ # "_links": {
222
+ # "self": { "href": "https://example.org" }
223
+ # },
224
+ # "_index": [
225
+ # { "href": "https://example.org" }
226
+ # ]
227
+ # }
228
+ # }
229
+ ```
230
+
231
+ #### Collections
232
+
233
+ A collection inlines the member objects. The collection method automatically generates it based on the default view of the same version.
234
+
235
+ ```ruby
236
+ class BookSerializer < MediaTypes::Serialization::Base
237
+ validator BookValidator
238
+
239
+ output versions: [1, 2, 3] do |obj, version, context|
240
+ attribute :book do
241
+ link :self, href: context.book_url(obj) if version >= 3
242
+
243
+ attribute :title, obj.title
244
+ attribute :description, obj.description if version >= 2
245
+ end
246
+ end
247
+
248
+ output view: :index, version: 3 do |arr, version, context|
249
+ attribute :books do
250
+ link :self, href: context.book_index_url
251
+
252
+ index arr, version: version
253
+ end
254
+ end
125
255
 
126
- # This defines migrations. You can use classes, commands or anything else to execute this code
127
- # but inline migrations work fine if you don't have a lot of them.
128
- backward_migrations do
129
-
130
- # This is called if the version requested is 1 _or_ lower. This means you can compose your migrations. The
131
- # migrations with a _lower_ version than the requested version are NOT executed.
132
- version 1 do |result|
133
- result.tap do |r|
134
- if r.key?(:views)
135
- r[:views_count] = r.delete(:views)
136
- end
137
- end
138
- end
256
+ output view: :collection, version: 3 do |arr, version, context|
257
+ attribute :books do
258
+ link :self, href: context.book_collection_url
259
+
260
+ collection arr, version: version
139
261
  end
140
262
  end
141
263
  end
142
264
  ```
143
265
 
144
- ### Controller
266
+ ```ruby
267
+ BookSerializer.serialize([book], BookValidator.view(:collection).version(3), context: controller)
268
+ # header = Link: <https://example.org/collection>; rel="self"
269
+ # => {
270
+ # "books": {
271
+ # "_links": {
272
+ # "self": { "href": "https://example.org" }
273
+ # },
274
+ # "_embedded": [
275
+ # {
276
+ # "_links": {
277
+ # "self": { "href": "https://example.org" }
278
+ # },
279
+ # "title": "Everything, abridged",
280
+ # "description": "Mu"
281
+ # }
282
+ # ]
283
+ # }
284
+ # }
285
+ ```
286
+
287
+ ### Input deserialization
145
288
 
146
- In your base controller, or wherever you'd like, include the `MediaTypes::Serialization` concern. In the controller that
147
- uses the serialization, you need to explicitly `accept` it if you want to use the built-in lookups.
289
+ You can mark a media type as something that's allowed to be sent along with a PUT request as follows:
148
290
 
149
291
  ```ruby
150
- require 'media_types/serialization'
151
- require 'media_types/serialization/renderer/register'
292
+ class BookSerializer < MediaTypes::Serialization::Base
293
+ validator BookValidator
152
294
 
153
- class ApiController < ActionController::API
154
- include MediaTypes::Serialization
295
+ output versions: [1, 2, 3] do |obj, version, context|
296
+ attribute :book do
297
+ link :self, href: context.book_url(obj) if version >= 3
298
+
299
+ attribute :title, obj.title
300
+ attribute :description, obj.description if version >= 2
301
+ end
302
+
303
+ input version: 3
155
304
  end
156
305
 
157
- class BookController < ApiController
306
+ class BookController < ActionController::API
307
+ include MediaTypes::Serialization
158
308
 
159
- accept_serialization(Book::BasicSerializer, accept_html: false, only: %i[show])
160
- accept_html(Book::CoverHtmlSerializer, only: %i[show])
161
- freeze_accepted_media!
309
+ allow_output_serializer(BookSerializer, only: %i[show])
310
+ allow_input_serializer(BookSerializer, only: %i[create])
311
+ freeze_io!
162
312
 
163
313
  def show
164
- # If you do NOT pass in the content_type, it will re-use the current content_type of the response if set or
165
- # use the default content type of the serializer. This is fine if you only output one Content-Type in the
166
- # action, but not if you are relying on content-negotiation.
167
-
168
- render media: serialize_media(@book), content_type: request.format.to_s
314
+ book = Book.new
315
+ book.title = 'Everything, abridged'
316
+
317
+ render media: serialize_media(book), content_type: request.format.to_s
318
+ end
319
+
320
+ def create
321
+ json = deserialize(request, context: self) # does validation for us
322
+ puts json
169
323
  end
170
324
  end
171
325
  ```
172
326
 
173
- If you have normalized your resources (e.g. into `@resource`), you can add a `render_media` method to your
174
- `BaseController` and render resources like so:
327
+ If you use [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) you might want to convert the verified json data during deserialization:
175
328
 
176
329
  ```ruby
177
- class ApiController < ActionController::API
178
- def render_media(**opts)
179
- render media: serialize_media(@resource), content_type: request.format.to_s, **opts
330
+ class BookSerializer < MediaTypes::Serialization::Base
331
+ validator BookValidator
332
+
333
+ output versions: [1, 2, 3] do |obj, version, context|
334
+ attribute :book do
335
+ link :self, href: context.book_url(obj) if version >= 3
336
+
337
+ attribute :title, obj.title
338
+ attribute :description, obj.description if version >= 2
339
+ end
340
+
341
+ input versions: [1, 2, 3] do |json, version, context|
342
+ book = Book.new
343
+ book.title = json['book']['title']
344
+ book.description = 'Not available'
345
+ book.description = json['book']['description'] if version >= 2
346
+
347
+ # Best practise is to only save in the controller.
348
+ book
180
349
  end
181
350
  end
182
- ```
183
351
 
184
- And then call `render_media` whenever you're ready to render.
352
+ class BookController < ActionController::API
353
+ include MediaTypes::Serialization
185
354
 
186
- ### HTML output
355
+ allow_output_serializer(BookSerializer, only: %i[show])
356
+ allow_input_serializer(BookSerializer, only: %i[create])
357
+ freeze_io!
358
+
359
+ def show
360
+ book = Book.new
361
+ book.title = 'Everything, abridged'
187
362
 
188
- You can define HTML outputs for example by creating a serializer that accepts `text/html`. At this moment, there may
189
- only be one (1) active `text/html` serializer for each action; a single controller can have multiple registered, but
190
- never for the same preconditions in `before_action` (because how else would it know which one to pick?).
363
+ render media: serialize_media(book), content_type: request.format.to_s
364
+ end
191
365
 
192
- Use the `render` method to generate your HTML:
193
- ```ruby
194
- class Book::CoverHtmlSerializer < MediaTypes::Serialization::Base
195
- # Tell the serializer that this accepts HTML, but this is also signaled by `to_html`
196
- serializes_html
197
-
198
- def to_html
199
- ApplicationController.render(
200
- 'serializers/book/cover',
201
- assigns: {
202
- title: extract_title,
203
- image: resolve_file_url(covers.first&.version_url('small')),
204
- description: extract_description,
205
- language_links: language_links,
206
- },
207
- layout: false
208
- )
366
+ def create
367
+ book = deserialize(request, context: self)
368
+ book.save!
369
+
370
+ render media: serialize_media(book), content_type request.format.to_s
209
371
  end
210
-
211
- # Naturally you have to define extract_title, etc etc
212
372
  end
213
373
  ```
214
374
 
215
- You can change the default `wrapper` / `to_html` implementation by setting:
375
+ 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`.
376
+
377
+ ### Raw output
378
+
379
+ Sometimes you need to output raw data. This cannot be validated. You do this as follows:
216
380
 
217
381
  ```ruby
218
- ::MediaTypes::Serialization.html_wrapper_layout = '/path/to/wrapper/layout'
382
+ class BookSerializer < MediaTypes::Serialization::Base
383
+ validator BookValidator
384
+
385
+ output_raw view: :raw, version: 3 do |obj, version, context|
386
+ hidden do
387
+ # Make sure links are only set in the headers, not in the body.
388
+
389
+ link :self, href: context.book_url(obj)
390
+ end
391
+
392
+ "I'm a non-json output"
393
+ end
394
+ end
219
395
  ```
220
396
 
221
- ### API viewer
397
+ ### Raw input
222
398
 
223
- There is a special media type exposed by this gem at `::MediaTypes::Serialization::MEDIA_TYPE_API_VIEWER`. If you're
224
- using `rails` you'll want to register it. You can do so manually, or by `require`ing:
399
+ You can do the same with input:
225
400
 
226
401
  ```ruby
227
- require 'media_types/serialization/media_type/register'
402
+ class BookSerializer < MediaTypes::Serialization::Base
403
+ validator BookValidator
404
+
405
+ input_raw view: raw, version: 3 do |bytes, version, context|
406
+ book = Book.new
407
+ book.description = bytes
408
+
409
+ book
410
+ end
411
+ end
228
412
  ```
229
413
 
230
- If you do so, the `.api_viewer` format becomes available for all actions that call into `render media:`.
414
+ ### Remapping media type identifiers
231
415
 
232
- You can change the default `wrapper` implementation by setting:
416
+ Sometimes you already have old clients using an `application/json` media type identifier when they do requests. While this is not a good practise as this makes it hard to add new fields or remove old ones, this library has support for migrating away:
233
417
 
234
418
  ```ruby
235
- ::MediaTypes::Serialization.api_viewer_layout = '/path/to/wrapper/layout'
419
+ class BookSerializer < MediaTypes::Serialization::Base
420
+ validator BookValidator
421
+
422
+ output versions: [1, 2, 3] do |obj, version, context|
423
+ attribute :book do
424
+ link :self, href: context.book_url(obj) if version >= 3
425
+
426
+ attribute :title, obj.title
427
+ attribute :description, obj.description if version >= 2
428
+ end
429
+ end
430
+ output_alias 'application/json' # maps application/json to to applicaton/vnd.acme.book.v1+json
431
+
432
+ input view: :create, versions: [1, 2, 3] do |json, version, context|
433
+ book = Book.new
434
+ book.title = json['book']['title']
435
+ book.description = 'Not available'
436
+ book.description = json['book']['description'] if version >= 2
437
+
438
+ # Make sure not to save here but only save in the controller
439
+ book
440
+ end
441
+ input_alias 'application/json', view: :create # maps application/json to to applicaton/vnd.acme.book.v1+json
236
442
  ```
237
443
 
238
- ### Wrapping output
444
+ Validation will be done using the remapped validator. Aliasses map to version `nil` if that is available or `1` otherwise. It is not possible to configure this version.
445
+
446
+ ### HTML
447
+
448
+ This library has a built in API viewer. The viewer can be accessed by by appending a `?api_viewer=last` query parameter to the URL.
239
449
 
240
- By convention, `index` views are wrapped in `_index: [items]`, `collection` views are wrapped in `_embedded: [items]`
241
- and `create` / no views are wrapped in `[ROOT_KEY]: item`. This is currently only enabled for `to_json` serialization
242
- but planned for `xml` as well.
450
+ To enable the API viewer, use: `allow_api_viewer` in the controller.
243
451
 
244
- This behaviour can not be turned of as of writing. However, you may _overwrite_ this behaviour via:
452
+ ```ruby
453
+ class BookController < ActionController::API
454
+ include MediaTypes::Serialization
245
455
 
246
- - `self.root_key(view:)`: to define the root key for a specific `view`
247
- - `self.wrap(serializer, view: nil)`: to define the wrapper for a specific `view` and/or `serializer`. For example, if
248
- you never want to wrap anything, you could define:
249
- ```ruby
250
- def self.wrap(serializer, view: nil)
251
- serializer
456
+ allow_api_viewer
457
+
458
+ allow_output_serializer(MediaTypes::ApiViewer)
459
+
460
+ allow_output_serializer(BookSerializer, only: %i[show])
461
+ allow_input_serializer(BookSerializer, only: %i[create])
462
+ freeze_io!
463
+
464
+ def show
465
+ book = Book.new
466
+ book.title = 'Everything, abridged'
467
+
468
+ render media: serialize_media(book), content_type: request.format.to_s
252
469
  end
253
- ```
254
470
 
255
- ### Link header
471
+ def create
472
+ json = deserialize(request, context: self) # does validation for us
473
+ puts json
474
+ end
475
+ end
476
+ ```
256
477
 
257
- You can use `to_link_header` to generate a header value for the `Link` header.
478
+ You can also output custom HTML:
258
479
 
259
480
  ```ruby
260
- entries = @last_media_serializer.to_link_header
261
- if entries.present?
262
- response.header[HEADER_LINK] = entries
481
+ class BookSerializer < MediaTypes::Serialization::Base
482
+ validator BookValidator
483
+
484
+ output versions: [1, 2, 3] do |obj, version, context|
485
+ attribute :book do
486
+ link :self, href: context.book_url(obj) if version >= 3
487
+
488
+ attribute :title, obj.title
489
+ attribute :description, obj.description if version >= 2
490
+ end
491
+ end
492
+
493
+ output_raw view: :html do |obj, context|
494
+ render_view 'book/show', context: context, assigns: {
495
+ title: obj.title,
496
+ description: obj.description
497
+ }
498
+ end
499
+
500
+ output_alias 'text/html', view: :html
263
501
  end
264
502
  ```
265
503
 
266
- If you want the link header to be different from the `_links`, you can implement `header_links(view:)` next to
267
- `extract_links(view:)`. This will be called by the `to_link_header` function.
504
+ #### Errors
268
505
 
269
- ### Validation
270
- If you only have `json`/`xml`/structured data responses and you want to use [`media_types-validation`](https://github.com/XPBytes/media_types-validation) in conjunction with this gem, you can create a concern or add the following two functions to your base controller:
506
+ This library adds support for returning errors to clients using the [`application/problem+json`](https://tools.ietf.org/html/rfc7231) media type. You can catch and transform application errors by adding an `output_error` call before `freeze_io!`:
271
507
 
272
508
  ```ruby
273
- def render_media(resource = @resource, **opts)
274
- serializer = serialize_media(resource)
275
- render media: serializer, content_type: request.format.to_s, **opts
276
- validate_media(serializer)
277
- end
509
+ class BookController < ActionController::API
510
+ include MediaTypes::Serialization
511
+
512
+ output_error CanCan::AccessDenied do |p, error|
513
+ p.title 'You do not have enough permissions to perform this action.', lang: 'en'
514
+ p.title 'Je hebt geen toestemming om deze actie uit te voeren.', lang: 'nl-NL'
515
+
516
+ p.status_code :forbidden
517
+ end
518
+
519
+ freeze_io!
278
520
 
279
- def validate_media(serializer = @last_media_serializer)
280
- media_type = serializer.current_media_type
281
- return true unless media_type && response_body
282
- validate_json_with_media_type(serializer.to_hash, media_type: media_type)
521
+ # ...
283
522
  end
284
523
  ```
285
524
 
286
- As long as the serializer has a `to_json` or `to_hash`, this will work -- but also means that the data will always be validate _as if_ it were json. This covers most use cases.
525
+ 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. Feel free to add instructions there on how clients should solve this problem. You can find more information at: http://docs.delftsolutions.nl/wiki/Error
526
+ If you want to override this url you can use the `p.url(href)` function.
527
+
528
+ By default the `message` property of the error is used to fill the `details` field. You can override this by using the `p.override_details(description, lang:)` function.
529
+
530
+ Custom attributes can be added using the `p.attribute(name, value)` function.
287
531
 
288
532
  ### Related
289
533
 
@@ -291,6 +535,229 @@ As long as the serializer has a `to_json` or `to_hash`, this will work -- but al
291
535
  - [`MediaTypes::Deserialization`](https://github.com/XPBytes/media_types-deserialization): :cyclone: Add media types supported deserialization using your favourite parser, and media type validation.
292
536
  - [`MediaTypes::Validation`](https://github.com/XPBytes/media_types-validation): :heavy_exclamation_mark: Response validations according to a media-type
293
537
 
538
+ ## API
539
+
540
+ ### Serializer definition
541
+
542
+ These methods become available during class definition if you inherit from `MediaTypes::Serialization::Base`.
543
+
544
+ #### `unvalidated( prefix )`
545
+
546
+ Disabled validation for this serializer. Prefix is of the form `application/vnd.<organisation>.<name>`.
547
+
548
+ Either unvalidated or validator must be used while defining a serializer.
549
+
550
+ #### `validator( media_type_validator )`
551
+
552
+ Enabled validation for this serializer using a [Media Type Validator](https://github.com/SleeplessByte/media-types-ruby).
553
+
554
+ Either validator or unvalidated must be used while defining a serializer.
555
+
556
+ #### `output( view:, version:, versions: ) do |obj, version, context|`
557
+
558
+ Defines a serialization block. Either version or versions can be set. View should be a symbol or unset.
559
+
560
+ Obj is the object to be serialized, version is the negotiated version and context is the context passed in from the serialize function. When using the controller integration, context is the current controller.
561
+
562
+ The block should return an object to convert into JSON.
563
+
564
+ #### `output_raw( view:, version:, versions: ) do |obj, version, context|`
565
+
566
+ This has the same behavior as `output` but should return a string instead of an object. Output is not validated.
567
+
568
+ #### `output_alias( media_type_identifier, view: )`
569
+
570
+ Defines a legacy mapping. This will make the deserializer parse the media type `media_type_identifier` as if it was version 1 of the specified view. If view is undefined it will use the output serializer without a view defined.
571
+
572
+ #### `output_alias_optional( media_type_identifier, view: )`
573
+
574
+ Has the same behavior as `output_alias` but can be used by multiple serializers. The serializer that is loaded last in the controller 'wins' control over this media type identifier. If any of the serializers have an `output_alias` defined with the same media type identifier that one will win instead.
575
+
576
+ #### `input( view:, version:, versions: ) do |obj, version, context|`
577
+
578
+ Defines a deserialization block. Either version or versions can be set. View should be a symbol or unset.
579
+
580
+ Obj is the object to be serialized, version is the negotiated version and context is the context passed in from the serialize function. When using the controller integration, context is the current controller.
581
+
582
+ The block should return the internal representation of the object. Best practise is to make sure not to change state in this function but to leave that up to the controller.
583
+
584
+ #### `input_raw( view:, version:, versions: ) do |bytes, version, context|`
585
+
586
+ This has the same behavior as `input` but takes in raw data. Input is not validated.
587
+
588
+ #### `input_alias( media_type_identifier, view: )`
589
+
590
+ Defines a legacy mapping. This will make the serializer parse the media type `media_type_identifier` as if it was version 1 of the specified view. If view is undefined it will use the input serializer without a view defined.
591
+
592
+ #### `input_alias_optional( media_type_identifier, view: )`
593
+
594
+ Has the same behavior as `input_alias` but can be used by multiple serializers. The serializer that is loaded last in the controller 'wins' control over this media type identifier. If any of the serializers have an `input_alias` defined with the same media type identifier that one will win instead.
595
+
596
+ #### `disable_wildcards`
597
+
598
+ Disables registering wildcard media types.
599
+
600
+ ### Serializer definition
601
+
602
+ The following methods are available within an `output ... do` block.
603
+
604
+ #### `attribute( key, value = {} ) do`
605
+
606
+ Sets a value for the given key. If a block is given, any `attribute`, `link`, `collection` and `index` statements are run in context of `value`.
607
+
608
+ Returns the built up context so far.
609
+
610
+ #### `link( rel, href:, emit_header: true, **attributes )`
611
+
612
+ Adds a `_link` block to the current context. Also adds the specified link to the HTTP Link header. `attributes` allows passing in custom attributes.
613
+
614
+ If `emit_header` is `true` the link will also be emitted as a http header.
615
+
616
+ Returns the built up context so far.
617
+
618
+ #### `index( array, serializer, version:, view: nil )`
619
+
620
+ 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.
621
+
622
+ Returns the built up context so far.
623
+
624
+ #### `collection( array, serializer, version:, view: nil )`
625
+
626
+ Adds an `_embedded` block to the current context. Uses the specified serializer to embed the child objects.
627
+ Optionally a block can be used to modify the output from the child serializer.
628
+
629
+ Returns the built up context so far.
630
+
631
+ #### `hidden do`
632
+
633
+ Sometimes you want to add links without actually modifying the object. Calls to `attribute`, `link`, `index`, `collection` made inside this block won't modify the context. Any calls to link will only set the HTTP Link header.
634
+
635
+ Returns the unmodified context.
636
+
637
+ #### `emit`
638
+
639
+ Can be added to the end of a block to fix up the return value to return the built up context so far.
640
+
641
+ Returns the built up context so far.
642
+
643
+ #### `object do`
644
+
645
+ Runs a block in a new context and returns the result
646
+
647
+ #### `render_view( view, context:, **args)`
648
+
649
+ Can be used to render a view. You can set local variables in the view by assigning a hash to the `assigns:` parameter.
650
+
651
+ ### Controller definition
652
+
653
+ These functions are available during the controller definition if you add `include MediaTypes::Serialization`.
654
+
655
+ #### `allow_output_serializer( serializer, views: nil, **filters )`
656
+
657
+ Configure the controller to allow the client to request responses emitted by the specified serializer. Optionally allows you to specify which views to allow by passing an array in the views parameter.
658
+
659
+ Accepts the same filters as `before_action`.
660
+
661
+ #### `allow_input_serializer( serializer, views: nil, **filters )`
662
+
663
+ Configure the controller to allow the client to send bodies with a `Content-Type` that can be deserialized using the specified serializer. Optionally allows you to specify which views to allow by passing an array in the views parameter.
664
+
665
+ Accepts the same filters as `before_action`.
666
+
667
+ #### `allow_all_input( **filters )`
668
+
669
+ Disables input deserialization. Running `deserialize` while allowing all input will result in an error being thrown.
670
+
671
+ #### `not_acceptable_serializer( serializer )`
672
+
673
+ Replaces the serializer used to render the error page when no media type could be negotiated using the `Accept` header.
674
+
675
+ #### `unsupported_media_type_serializer( serializer )`
676
+
677
+ 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`.
678
+
679
+ #### `clear_unsupported_media_type_serializers!`
680
+
681
+ Clears the list of serializers used to render the error when the client supplies non-valid input.
682
+
683
+ #### `input_validation_failed_serializer( serializer )`
684
+
685
+ Adds a serializer that can be used to render the error page when input validation fails.
686
+
687
+ #### `clear_input_validation_failed_serializers!`
688
+
689
+ Clears the list of serializers used to render the error when the client supplies non-valid input.
690
+
691
+ #### `allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)`
692
+
693
+ Enables rendering the api viewer when adding the `api_viewer=last` query parameter to the url.
694
+
695
+ #### `freeze_io!`
696
+
697
+ Registers serialization and deserialization in the controller. This function must be called before using the controller.
698
+
699
+ ### Controller usage
700
+
701
+ These functions are available during method execution in the controller.
702
+
703
+ #### `render_media( obj, serializers: nil, not_acceptable_serializer: nil, **options ) do`
704
+
705
+ Serializes an object and renders it using the appropriate content type. Options are passed through to the controller `render` function. Allows you to specify different objects to different serializers using a block:
706
+
707
+ ```ruby
708
+ render_media do
709
+ serializer BookSerializer, book
710
+ serializer BooksSerializer do
711
+ [ book ]
712
+ end
713
+ end
714
+ ```
715
+
716
+ Warning: this block can be called multiple times when used together with recursive serializers like the API viewer. Try to avoid changing state in this block.
717
+
718
+ If you want to render with different serializers than defined in the controller you can pass an array of serializers in the `serializers` property.
719
+
720
+ 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.
721
+
722
+ This method throws a `MediaTypes::Serialization::OutputValidationFailedError` error if the output does not conform to the format defined by the configured validator. Best practise is to return a 500 error to the client.
723
+
724
+ 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.
725
+
726
+ Due to the way this gem is implemented it is not possible to use instance variables (`@variable`) in the `render_media` do block.
727
+
728
+ #### `deserialize( request )`
729
+
730
+ Deserializes the request body using the configured input serializers and returns the deserialized object.
731
+
732
+ Returns nil if no input body was given by the client.
733
+
734
+ This method throws a `MediaTypes::Serialization::InputValidationFailedError` error if the incoming data does not conform to the specified schema.
735
+
736
+ #### `deserialize!( request )`
737
+
738
+ Does the same as `deserialize( request )` but gives the client an error page if no input was supplied.
739
+
740
+ #### `resolve_serializer(request, identifier = nil, registration = @serialization_output_registration)`
741
+
742
+ Returns the serializer class that will handle the given request.
743
+
744
+ ## Customization
745
+ 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:
746
+
747
+ ```ruby
748
+ # config/initializers/serialization.rb
749
+
750
+ MediaTypes::Serialization::Serializers::CommonCSS.background = 'linear-gradient(245deg, #3a2f28 0%, #201a16 100%)'
751
+ MediaTypes::Serialization::Serializers::CommonCSS.logo_width = 12
752
+ MediaTypes::Serialization::Serializers::CommonCSS.logo_data = <<-HERE
753
+ <svg height="150" width="500">
754
+ <ellipse cx="240" cy="100" rx="220" ry="30" style="fill:purple" />
755
+ <ellipse cx="220" cy="70" rx="190" ry="20" style="fill:lime" />
756
+ <ellipse cx="210" cy="45" rx="170" ry="15" style="fill:yellow" />
757
+ </svg>
758
+ HERE
759
+ ```
760
+
294
761
  ## Development
295
762
 
296
763
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can