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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +10 -1
- data/.gitignore +12 -12
- data/.idea/.rakeTasks +5 -5
- data/.idea/inspectionProfiles/Project_Default.xml +5 -5
- data/.idea/runConfigurations/test.xml +19 -19
- data/CHANGELOG.md +18 -0
- data/CODE_OF_CONDUCT.md +74 -74
- data/Gemfile +4 -4
- data/Gemfile.lock +58 -61
- data/LICENSE.txt +21 -21
- data/README.md +640 -173
- data/Rakefile +10 -10
- data/bin/console +14 -14
- data/bin/setup +8 -8
- data/lib/media_types/problem.rb +64 -0
- data/lib/media_types/serialization.rb +431 -172
- data/lib/media_types/serialization/base.rb +111 -91
- data/lib/media_types/serialization/error.rb +178 -0
- data/lib/media_types/serialization/fake_validator.rb +52 -0
- data/lib/media_types/serialization/serialization_dsl.rb +117 -0
- data/lib/media_types/serialization/serialization_registration.rb +235 -0
- data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
- data/lib/media_types/serialization/serializers/common_css.rb +168 -0
- data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
- data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
- data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
- data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
- data/lib/media_types/serialization/serializers/problem_serializer.rb +87 -0
- data/lib/media_types/serialization/version.rb +1 -1
- data/media_types-serialization.gemspec +50 -50
- metadata +40 -43
- data/.travis.yml +0 -17
- data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
- data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
- data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
- data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
- data/lib/media_types/serialization/media_type/register.rb +0 -4
- data/lib/media_types/serialization/migrations_command.rb +0 -38
- data/lib/media_types/serialization/migrations_support.rb +0 -50
- data/lib/media_types/serialization/mime_type_support.rb +0 -64
- data/lib/media_types/serialization/no_content_type_given.rb +0 -11
- data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
- data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
- data/lib/media_types/serialization/renderer.rb +0 -41
- data/lib/media_types/serialization/renderer/register.rb +0 -4
- data/lib/media_types/serialization/wrapper.rb +0 -13
- data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
- data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -61
- data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -61
- data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
- 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
|
[](https://badge.fury.io/rb/media_types-serialization)
|
5
5
|
[](http://opensource.org/licenses/MIT)
|
6
6
|
|
7
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
46
|
+
To convert a ruby object to a json representation:
|
32
47
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
48
|
+
```ruby
|
49
|
+
class Book
|
50
|
+
attr_accessor :title
|
51
|
+
end
|
37
52
|
|
38
|
-
|
53
|
+
book = Book.new
|
54
|
+
book.title = 'Everything, abridged'
|
39
55
|
|
40
|
-
|
56
|
+
BookSerializer.serialize(book, 'vnd.acme.book.v1+json', context: nil)
|
57
|
+
# => { "book": { "title": "Everything, abridged" } }
|
58
|
+
```
|
41
59
|
|
42
|
-
###
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
169
|
+
This returns the following response:
|
90
170
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
97
|
-
content-type.
|
185
|
+
### Collections
|
98
186
|
|
99
|
-
|
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
|
-
####
|
189
|
+
#### Indexes
|
103
190
|
|
104
|
-
|
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
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
151
|
-
|
292
|
+
class BookSerializer < MediaTypes::Serialization::Base
|
293
|
+
validator BookValidator
|
152
294
|
|
153
|
-
|
154
|
-
|
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 <
|
306
|
+
class BookController < ActionController::API
|
307
|
+
include MediaTypes::Serialization
|
158
308
|
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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
|
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
|
178
|
-
|
179
|
-
|
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
|
-
|
352
|
+
class BookController < ActionController::API
|
353
|
+
include MediaTypes::Serialization
|
185
354
|
|
186
|
-
|
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
|
-
|
189
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
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
|
-
|
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
|
-
###
|
397
|
+
### Raw input
|
222
398
|
|
223
|
-
|
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
|
-
|
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
|
-
|
414
|
+
### Remapping media type identifiers
|
231
415
|
|
232
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
452
|
+
```ruby
|
453
|
+
class BookController < ActionController::API
|
454
|
+
include MediaTypes::Serialization
|
245
455
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
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
|
478
|
+
You can also output custom HTML:
|
258
479
|
|
259
480
|
```ruby
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
267
|
-
`extract_links(view:)`. This will be called by the `to_link_header` function.
|
504
|
+
#### Errors
|
268
505
|
|
269
|
-
|
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
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
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
|
-
|
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
|