media_types-serialization 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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