media_types-serialization 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
- `respond_to` on steroids. Add versioned serialization and deserialization to your Rails projects.
7
+ `respond_to` on steroids. Add [HATEOAS](https://docs.delftsolutions.nl/wiki/HATEOAS_API) compatible serialization and deserialization to your Rails projects.
8
8
 
9
9
  ## Installation
10
10
 
@@ -57,6 +57,60 @@ BookSerializer.serialize(book, 'vnd.acme.book.v1+json', context: nil)
57
57
  # => { "book": { "title": "Everything, abridged" } }
58
58
  ```
59
59
 
60
+ ### Controller integration
61
+
62
+ 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:
63
+
64
+ ```ruby
65
+ require 'media_types/serialization'
66
+
67
+ class BookController < ActionController::API
68
+ include MediaTypes::Serialization
69
+
70
+ allow_output_serializer(BookSerializer, only: %i[show])
71
+ freeze_io!
72
+
73
+ def show
74
+ book = Book.new
75
+ book.title = 'Everything, abridged'
76
+
77
+ render_media book
78
+ end
79
+ end
80
+ ```
81
+
82
+ While using the controller integration the context will always be set to the current controller. This allows you to construct urls.
83
+
84
+ ### Adding HATEOAS responses to existing routes
85
+
86
+ When creating a mobile application it's often useful to allow the app to request a non-html representation of a specific url. If you have an existing route:
87
+
88
+ ```ruby
89
+ class BookController < ApplicationController
90
+ def show
91
+ @book = Book.new
92
+
93
+ # Use view corresponding to the controller
94
+ end
95
+ end
96
+ ```
97
+
98
+ You can add a json representation as follows:
99
+
100
+ ```ruby
101
+ class BookController < ApplicationController
102
+ allow_output_serializer(BookSerializer, only: %i[show])
103
+ allow_output_html
104
+ freeze_io!
105
+
106
+ def show
107
+ @book = Book.new
108
+
109
+ render_media @book
110
+ end
111
+ end
112
+ ```
113
+
60
114
  ### Validations
61
115
 
62
116
  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):
@@ -98,30 +152,6 @@ end
98
152
 
99
153
  For more information, see the [Media Types docs](https://github.com/SleeplessByte/media-types-ruby).
100
154
 
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
155
  ### Versioning
126
156
 
127
157
  To help with supporting older versions, serializers have a [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) to construct json objects:
@@ -149,7 +179,7 @@ BookSerializer.serialize(book, BookValidator.version(2), context: nil)
149
179
 
150
180
  ### Links
151
181
 
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:
182
+ 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. Serializers have convenience methods to help with this:
153
183
 
154
184
  ```ruby
155
185
  class BookSerializer < MediaTypes::Serialization::Base
@@ -206,7 +236,7 @@ class BookSerializer < MediaTypes::Serialization::Base
206
236
  output view: :index, version: 3 do |arr, version, context|
207
237
  attribute :books do
208
238
  link :self, href: context.book_index_url
209
-
239
+
210
240
  index arr, version: version
211
241
  end
212
242
  end
@@ -248,15 +278,15 @@ class BookSerializer < MediaTypes::Serialization::Base
248
278
  output view: :index, version: 3 do |arr, version, context|
249
279
  attribute :books do
250
280
  link :self, href: context.book_index_url
251
-
281
+
252
282
  index arr, version: version
253
283
  end
254
284
  end
255
-
285
+
256
286
  output view: :collection, version: 3 do |arr, version, context|
257
287
  attribute :books do
258
288
  link :self, href: context.book_collection_url
259
-
289
+
260
290
  collection arr, version: version
261
291
  end
262
292
  end
@@ -309,8 +339,8 @@ class BookController < ActionController::API
309
339
  allow_output_serializer(BookSerializer, only: %i[show])
310
340
  allow_input_serializer(BookSerializer, only: %i[create])
311
341
  freeze_io!
312
-
313
- def show
342
+
343
+ def show
314
344
  book = Book.new
315
345
  book.title = 'Everything, abridged'
316
346
 
@@ -355,8 +385,8 @@ class BookController < ActionController::API
355
385
  allow_output_serializer(BookSerializer, only: %i[show])
356
386
  allow_input_serializer(BookSerializer, only: %i[create])
357
387
  freeze_io!
358
-
359
- def show
388
+
389
+ def show
360
390
  book = Book.new
361
391
  book.title = 'Everything, abridged'
362
392
 
@@ -385,7 +415,7 @@ class BookSerializer < MediaTypes::Serialization::Base
385
415
  output_raw view: :raw, version: 3 do |obj, version, context|
386
416
  hidden do
387
417
  # Make sure links are only set in the headers, not in the body.
388
-
418
+
389
419
  link :self, href: context.book_url(obj)
390
420
  end
391
421
 
@@ -454,14 +484,12 @@ class BookController < ActionController::API
454
484
  include MediaTypes::Serialization
455
485
 
456
486
  allow_api_viewer
457
-
458
- allow_output_serializer(MediaTypes::ApiViewer)
459
487
 
460
488
  allow_output_serializer(BookSerializer, only: %i[show])
461
489
  allow_input_serializer(BookSerializer, only: %i[create])
462
490
  freeze_io!
463
-
464
- def show
491
+
492
+ def show
465
493
  book = Book.new
466
494
  book.title = 'Everything, abridged'
467
495
 
@@ -489,14 +517,14 @@ class BookSerializer < MediaTypes::Serialization::Base
489
517
  attribute :description, obj.description if version >= 2
490
518
  end
491
519
  end
492
-
520
+
493
521
  output_raw view: :html do |obj, context|
494
522
  render_view 'book/show', context: context, assigns: {
495
523
  title: obj.title,
496
524
  description: obj.description
497
525
  }
498
526
  end
499
-
527
+
500
528
  output_alias 'text/html', view: :html
501
529
  end
502
530
  ```
@@ -518,11 +546,11 @@ class BookController < ActionController::API
518
546
 
519
547
  freeze_io!
520
548
 
521
- # ...
549
+ # ...
522
550
  end
523
551
  ```
524
552
 
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
553
+ 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: [https://docs.delftsolutions.nl/wiki/Error](https://docs.delftsolutions.nl/wiki/Error)
526
554
  If you want to override this url you can use the `p.url(href)` function.
527
555
 
528
556
  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.
@@ -563,14 +591,18 @@ The block should return an object to convert into JSON.
563
591
 
564
592
  This has the same behavior as `output` but should return a string instead of an object. Output is not validated.
565
593
 
566
- #### `output_alias( media_type_identifier, view: )`
594
+ #### `output_alias( media_type_identifier, view:, hide_variant: false )`
567
595
 
568
- 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.
596
+ 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. If view is undefined it will use the output serializer without a view defined.
569
597
 
570
- #### `output_alias_optional( media_type_identifier, view: )`
598
+ 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]`.
599
+
600
+ #### `output_alias_optional( media_type_identifier, view:, hide_variant: false )`
571
601
 
572
602
  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.
573
603
 
604
+ 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]`.
605
+
574
606
  #### `input( view:, version:, versions: ) do |obj, version, context|`
575
607
 
576
608
  Defines a deserialization block. Either version or versions can be set. View should be a symbol or unset.
@@ -656,6 +688,20 @@ Configure the controller to allow the client to request responses emitted by the
656
688
 
657
689
  Accepts the same filters as `before_action`.
658
690
 
691
+ #### `allow_output_html( as: nil, layout: nil, **filters )`
692
+
693
+ 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.
694
+
695
+ The `Content-Type` of the response will be `text/html` if the `as:` parameter is unset. If the `as:` parameter is set, it will include it in the variant parameter: `text/html; variant=application/vnd.xpbytes.borderless`.
696
+
697
+ Accepts the same filters as `before_action`.
698
+
699
+ #### `allow_output_docs( description, **filters )`
700
+
701
+ Outputs the specified description as help information.
702
+
703
+ Accepts the same filters as `before_action`.
704
+
659
705
  #### `allow_input_serializer( serializer, views: nil, **filters )`
660
706
 
661
707
  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.
@@ -690,7 +736,7 @@ Clears the list of serializers used to render the error when the client supplies
690
736
 
691
737
  Enables rendering the api viewer when adding the `api_viewer=last` query parameter to the url.
692
738
 
693
- #### `freeze_io!`
739
+ #### `freeze_io!(**filter_opts)`
694
740
 
695
741
  Registers serialization and deserialization in the controller. This function must be called before using the controller.
696
742
 
@@ -740,6 +786,7 @@ Does the same as `deserialize( request )` but gives the client an error page if
740
786
  Returns the serializer class that will handle the given request.
741
787
 
742
788
  ## Customization
789
+
743
790
  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:
744
791
 
745
792
  ```ruby
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
8
- end
9
-
10
- task :default => :test
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "media_types/serialization"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "media_types/serialization"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -15,8 +15,7 @@ require 'active_support/concern'
15
15
  require 'active_support/core_ext/module/attribute_accessors'
16
16
  require 'active_support/core_ext/object/blank'
17
17
 
18
- require 'http_headers/accept'
19
-
18
+ require 'media_types/serialization/utils/accept_header'
20
19
  require 'media_types/serialization/base'
21
20
  require 'media_types/serialization/error'
22
21
  require 'media_types/serialization/serialization_dsl'
@@ -44,7 +43,8 @@ end
44
43
  module MediaTypes
45
44
  module Serialization
46
45
 
47
- HEADER_ACCEPT = 'HTTP_ACCEPT'
46
+ HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
47
+ HEADER_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
48
48
 
49
49
  mattr_accessor :json_encoder, :json_decoder
50
50
  if defined?(::Oj)
@@ -183,7 +183,58 @@ module MediaTypes
183
183
  @serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
184
184
  end
185
185
  end
186
-
186
+
187
+ def allow_output_html(as: nil, layout: nil, **filter_opts)
188
+ before_action(**filter_opts) do
189
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
190
+
191
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
192
+
193
+ html_registration = SerializationRegistration.new(:output)
194
+ output_identifier = 'text/html'
195
+ output_identifier += "; variant=#{as}" unless as.nil?
196
+
197
+ validator = FakeValidator.new(as.nil? ? 'text/html' : as)
198
+
199
+ block = lambda { |_, _, controller|
200
+ if layout.nil?
201
+ controller.render_to_string
202
+ else
203
+ controller.render_to_string(layout: layout)
204
+ end
205
+ }
206
+
207
+ html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
208
+ html_registration.registrations[validator.identifier].display_identifier = output_identifier
209
+ html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
210
+ html_registration.registrations['*/*'].display_identifier = output_identifier
211
+
212
+ @serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
213
+ end
214
+ end
215
+
216
+ def allow_output_docs(description, **filter_opts)
217
+ before_action(**filter_opts) do
218
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
219
+
220
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
221
+
222
+ docs_registration = SerializationRegistration.new(:output)
223
+ validator = FakeValidator.new('text/vnd.delftsolutions.docs')
224
+
225
+ block = lambda { |_, _, _|
226
+ description
227
+ }
228
+
229
+ docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
230
+ docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
231
+ docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
232
+ docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
233
+
234
+ @serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
235
+ end
236
+ end
237
+
187
238
  def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
188
239
  before_action do
189
240
  @serialization_api_viewer_enabled ||= {}
@@ -216,7 +267,7 @@ module MediaTypes
216
267
  raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
217
268
  views = [view] if views.nil?
218
269
  raise ViewsNotAnArrayError unless views.is_a? Array
219
-
270
+
220
271
  before_action do
221
272
  @serialization_available_serializers ||= {}
222
273
  @serialization_available_serializers[:input] ||= {}
@@ -259,8 +310,8 @@ module MediaTypes
259
310
  ##
260
311
  # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
261
312
  #
262
- def freeze_io!
263
- before_action :serializer_freeze_io_internal
313
+ def freeze_io!(**filter_opts)
314
+ before_action :serializer_freeze_io_internal, **filter_opts
264
315
 
265
316
  output_error MediaTypes::Serialization::NoInputReceivedError do |p, error|
266
317
  p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
@@ -366,7 +417,7 @@ module MediaTypes
366
417
  return nil if identifier.nil?
367
418
 
368
419
  registration = registration.registrations[identifier]
369
-
420
+
370
421
  raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
371
422
  registration.serializer
372
423
  end
@@ -386,7 +437,7 @@ module MediaTypes
386
437
  #
387
438
  #
388
439
 
389
- accept_header = HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT)) || ''
440
+ accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
390
441
  accept_header.each do |mime_type|
391
442
  stripped = mime_type.to_s.split(';')[0]
392
443
  next unless registration.has? stripped
@@ -403,7 +454,7 @@ module MediaTypes
403
454
  identifier = serializer.validator.identifier
404
455
  obj = { request: request, registrations: registrations }
405
456
  new_registrations = serializer.outputs_for(views: [nil])
406
-
457
+
407
458
  serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: new_registrations, options: {})
408
459
  response.status = :not_acceptable
409
460
  end