media_types-serialization 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +32 -32
  3. data/.github/workflows/publish-bookworm.yml +33 -0
  4. data/.github/workflows/publish-sid.yml +33 -0
  5. data/.gitignore +22 -14
  6. data/.idea/.rakeTasks +7 -7
  7. data/.idea/dictionaries/Derk_Jan.xml +6 -6
  8. data/.idea/encodings.xml +3 -3
  9. data/.idea/inspectionProfiles/Project_Default.xml +5 -5
  10. data/.idea/media_types-serialization.iml +76 -76
  11. data/.idea/misc.xml +6 -6
  12. data/.idea/modules.xml +7 -7
  13. data/.idea/runConfigurations/test.xml +19 -19
  14. data/.idea/vcs.xml +5 -5
  15. data/CHANGELOG.md +190 -182
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/LICENSE.txt +21 -21
  19. data/README.md +1048 -1048
  20. data/Rakefile +10 -10
  21. data/bin/console +14 -14
  22. data/bin/setup +8 -8
  23. data/lib/media_types/problem.rb +67 -67
  24. data/lib/media_types/serialization/base.rb +269 -216
  25. data/lib/media_types/serialization/error.rb +193 -193
  26. data/lib/media_types/serialization/fake_validator.rb +53 -53
  27. data/lib/media_types/serialization/serialization_dsl.rb +135 -135
  28. data/lib/media_types/serialization/serialization_registration.rb +245 -245
  29. data/lib/media_types/serialization/serializers/api_viewer.rb +383 -136
  30. data/lib/media_types/serialization/serializers/common_css.rb +212 -168
  31. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -80
  32. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -85
  33. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -58
  34. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +93 -93
  35. data/lib/media_types/serialization/serializers/problem_serializer.rb +111 -111
  36. data/lib/media_types/serialization/utils/accept_header.rb +77 -77
  37. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -82
  38. data/lib/media_types/serialization/version.rb +7 -7
  39. data/lib/media_types/serialization.rb +682 -675
  40. data/media_types-serialization.gemspec +48 -48
  41. metadata +9 -8
  42. data/Gemfile.lock +0 -167
@@ -1,675 +1,682 @@
1
- require 'media_types/serialization/version'
2
- require 'media_types/serialization/serializers/common_css'
3
- require 'media_types/serialization/serializers/fallback_not_acceptable_serializer'
4
- require 'media_types/serialization/serializers/fallback_unsupported_media_type_serializer'
5
- require 'media_types/serialization/serializers/input_validation_error_serializer'
6
- require 'media_types/serialization/serializers/endpoint_description_serializer'
7
- require 'media_types/serialization/serializers/problem_serializer'
8
- require 'media_types/serialization/serializers/api_viewer'
9
- require 'media_types/problem'
10
-
11
- require 'abstract_controller'
12
- require 'action_controller/metal/mime_responds'
13
- require 'action_dispatch/http/mime_type'
14
- require 'active_support/concern'
15
- require 'active_support/core_ext/module/attribute_accessors'
16
- require 'active_support/core_ext/object/blank'
17
-
18
- require 'media_types/serialization/utils/accept_header'
19
- require 'media_types/serialization/base'
20
- require 'media_types/serialization/error'
21
- require 'media_types/serialization/serialization_dsl'
22
-
23
- require 'delegate'
24
-
25
- class SerializationSelectorDsl < SimpleDelegator
26
- def initialize(controller, selected_serializer)
27
- @serializer = selected_serializer
28
- self.value = nil
29
- self.matched = false
30
- super controller
31
- end
32
-
33
- attr_accessor :value, :matched
34
-
35
- def serializer(klazz, obj = nil, &block)
36
- return if klazz != @serializer
37
-
38
- self.matched = true
39
- self.value = block.nil? ? obj : yield
40
- end
41
- end
42
-
43
- module MediaTypes
44
- module Serialization
45
-
46
- HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
47
- HEADER_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
48
-
49
- mattr_accessor :json_encoder, :json_decoder
50
- if defined?(::Oj)
51
- self.json_encoder = ->(obj) {
52
- Oj.dump(obj,
53
- mode: :compat,
54
- indent: ' ',
55
- space: ' ',
56
- array_nl: "\n",
57
- object_nl: "\n",
58
- ascii_only: false,
59
- allow_nan: false,
60
- symbol_keys: true,
61
- allow_nil: false,
62
- allow_invalid_unicode: false,
63
- array_class: ::Array,
64
- create_additions: false,
65
- hash_class: ::Hash,
66
- nilnil: false,
67
- quirks_mode: false
68
- )
69
- }
70
- self.json_decoder = ->(obj) {
71
- Oj.load(obj,
72
- mode: :compat,
73
- ascii_only: false,
74
- allow_nan: false,
75
- symbol_keys: true,
76
- allow_nil: false,
77
- allow_invalid_unicode: false,
78
- array_class: ::Array,
79
- create_additions: false,
80
- hash_class: ::Hash,
81
- nilnil: false,
82
- quirks_mode: false
83
- )
84
- }
85
- else
86
- require 'json'
87
- self.json_encoder = JSON.method(:pretty_generate)
88
- self.json_decoder = ->(txt) {
89
- JSON.parse(txt, {
90
- symbolize_names: true,
91
- allow_nan: false,
92
- create_additions: false,
93
- object_class: ::Hash,
94
- array_class: ::Array,
95
- })
96
- }
97
- end
98
-
99
- extend ActiveSupport::Concern
100
-
101
- # rubocop:disable Metrics/BlockLength
102
- class_methods do
103
- def not_acceptable_serializer(serializer, **filter_opts)
104
- before_action(**filter_opts) do
105
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
106
-
107
- @serialization_not_acceptable_serializer = serializer
108
- end
109
- end
110
-
111
- def unsupported_media_type_serializer(serializer, **filter_opts)
112
- before_action(**filter_opts) do
113
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
114
-
115
- @serialization_unsupported_media_type_serializer ||= []
116
- @serialization_unsupported_media_type_serializer.append(serializer)
117
- end
118
- end
119
-
120
- def clear_unsupported_media_type_serializer!(**filter_opts)
121
- before_action(**filter_opts) do
122
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
123
-
124
- @serialization_unsupported_media_type_serializer = []
125
- end
126
- end
127
-
128
- def input_validation_failed_serializer(serializer, **filter_opts)
129
- before_action(**filter_opts) do
130
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
131
-
132
- @serialization_input_validation_failed_serializer ||= []
133
- @serialization_input_validation_failed_serializer.append(serializer)
134
- end
135
- end
136
-
137
- def clear_input_validation_failed_serializers!(**filter_opts)
138
- before_action(**filter_opts) do
139
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
140
-
141
- @serialization_input_validation_failed_serializer = []
142
- end
143
- end
144
-
145
- ##
146
- # Allow output serialization using the passed in +serializer+ for the given +view+
147
- #
148
- # @see #freeze_io!
149
- #
150
- # @param serializer the serializer to use for serialization.
151
- # @param [(String | NilClass|)] view the view it should use the serializer for. Use nil for no view
152
- # @param [(String | NilClass|)[]|NilClass] views the views it should use the serializer for. Use nil for no view
153
- #
154
- def allow_output_serializer(serializer, view: nil, views: nil, **filter_opts)
155
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
156
- raise ArrayInViewParameterError, :allow_output_serializer if view.is_a? Array
157
-
158
- views = [view] if views.nil?
159
- raise ViewsNotAnArrayError unless views.is_a? Array
160
-
161
- before_action do
162
- @serialization_available_serializers ||= {}
163
- @serialization_available_serializers[:output] ||= {}
164
- actions = filter_opts[:only] || :all_actions
165
- actions = [actions] unless actions.is_a?(Array)
166
- actions.each do |action|
167
- @serialization_available_serializers[:output][action.to_s] ||= []
168
- views.each do |v|
169
- @serialization_available_serializers[:output][action.to_s].push({serializer: serializer, view: v})
170
- end
171
- end
172
- end
173
-
174
- before_action(**filter_opts) do
175
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
176
-
177
- @serialization_output_registrations ||= SerializationRegistration.new(:output)
178
-
179
- mergeable_outputs = serializer.outputs_for(views: views)
180
- raise AddedEmptyOutputSerializer, serializer.name if mergeable_outputs.registrations.empty?
181
-
182
- @serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
183
- end
184
- end
185
-
186
- def allow_output_html(as: nil, view: nil, layout: nil, formats: [:html], variants: nil, **filter_opts)
187
- before_action(**filter_opts) do
188
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
189
-
190
- @serialization_output_registrations ||= SerializationRegistration.new(:output)
191
-
192
- html_registration = SerializationRegistration.new(:output)
193
- output_identifier = 'text/html'
194
- output_identifier += "; variant=#{as}" unless as.nil?
195
-
196
- validator = FakeValidator.new(as.nil? ? 'text/html' : as)
197
-
198
- block = lambda { |_, _, controller|
199
- options = {}
200
- options[:layout] = layout unless layout.nil?
201
- options[:template] = view unless view.nil?
202
- options[:formats] = formats unless formats.nil?
203
- options[:variants] = variants unless variants.nil?
204
-
205
- controller.render_to_string(**options)
206
- }
207
-
208
- html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
209
- html_registration.registrations[validator.identifier].display_identifier = output_identifier
210
- html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
211
- html_registration.registrations['*/*'].display_identifier = output_identifier
212
-
213
- @serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
214
- end
215
- end
216
-
217
- def allow_output_docs(description, **filter_opts)
218
- before_action(**filter_opts) do
219
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
220
-
221
- @serialization_output_registrations ||= SerializationRegistration.new(:output)
222
-
223
- docs_registration = SerializationRegistration.new(:output)
224
- validator = FakeValidator.new('text/vnd.delftsolutions.docs')
225
-
226
- block = lambda { |_, _, _|
227
- description
228
- }
229
-
230
- docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
231
- docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
232
- docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
233
- docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
234
-
235
- @serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
236
- end
237
- end
238
-
239
- def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
240
- before_action do
241
- @serialization_api_viewer_enabled ||= {}
242
- actions = filter_opts[:only] || :all_actions
243
- actions = [actions] unless actions.kind_of?(Array)
244
- actions.each do |action|
245
- @serialization_api_viewer_enabled[action.to_s] = true
246
- end
247
- end
248
-
249
- before_action(**filter_opts) do
250
- if request.query_parameters['api_viewer']
251
- @serialization_override_accept = request.query_parameters['api_viewer'].sub ' ', '+'
252
- @serialization_wrapping_renderer = serializer
253
- end
254
- end
255
- end
256
-
257
- ##
258
- # Allow input serialization using the passed in +serializer+ for the given +view+
259
- #
260
- # @see #freeze_io!
261
- #
262
- # @param serializer the serializer to use for deserialization
263
- # @param [(String | NilClass|)] view the view it should serializer for. Use nil for no view
264
- # @param [(String | NilClass|)[]|NilClass] views the views it should serializer for. Use nil for no view
265
- #
266
- def allow_input_serializer(serializer, view: nil, views: nil, **filter_opts)
267
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
268
- raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
269
- views = [view] if views.nil?
270
- raise ViewsNotAnArrayError unless views.is_a? Array
271
-
272
- before_action do
273
- @serialization_available_serializers ||= {}
274
- @serialization_available_serializers[:input] ||= {}
275
- actions = filter_opts[:only] || :all_actions
276
- actions = [actions] unless actions.is_a?(Array)
277
- actions.each do |action|
278
- @serialization_available_serializers[:input][action.to_s] ||= []
279
- views.each do |v|
280
- @serialization_available_serializers[:input][action.to_s].push({serializer: serializer, view: v})
281
- end
282
- end
283
- end
284
-
285
- before_action(**filter_opts) do
286
- raise SerializersAlreadyFrozenError if defined? @serialization_frozen
287
-
288
- @serialization_input_registrations ||= SerializationRegistration.new(:input)
289
-
290
- mergeable_inputs = serializer.inputs_for(views: views)
291
- raise AddedEmptyInputSerializer, serializer.name if mergeable_inputs.registrations.empty?
292
-
293
- @serialization_input_registrations = @serialization_input_registrations.merge(mergeable_inputs)
294
- end
295
- end
296
-
297
- def allow_all_output(**filter_opts)
298
- before_action(**filter_opts) do
299
- @serialization_output_registrations ||= SerializationRegistration.new(:output)
300
- @serialization_output_allow_all ||= true
301
- end
302
- end
303
-
304
- def allow_all_input(**filter_opts)
305
- before_action(**filter_opts) do
306
- @serialization_input_registrations ||= SerializationRegistration.new(:input)
307
- @serialization_input_allow_all ||= true
308
- end
309
- end
310
-
311
- ##
312
- # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
313
- #
314
- def freeze_io!(**filter_opts)
315
- before_action :serializer_freeze_io_internal, **filter_opts
316
-
317
- output_error MediaTypes::Serialization::NoInputReceivedError do |p, _error|
318
- p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
319
-
320
- p.status_code :bad_request
321
- end
322
- end
323
-
324
- def output_error(klazz, serializers: [])
325
- rescue_from klazz do |error|
326
- problem = Problem.new(error)
327
- instance_exec { yield problem, error, self } if block_given?
328
-
329
- serializer = MediaTypes::Serialization::Serializers::ProblemSerializer
330
- registrations = serializer.outputs_for(views: [:html, nil])
331
-
332
- render_media(
333
- problem,
334
- serializers: [registrations] + serializers.map { |s| s.outputs_for(views: [nil])},
335
- status: problem.response_status_code
336
- )
337
- end
338
- end
339
- end
340
- # rubocop:enable Metrics/BlockLength
341
-
342
- protected
343
-
344
- def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
345
- context = SerializationDSL.new(serializer, links, vary, context: self)
346
- context.instance_exec { @serialization_output_registrations.call(victim, media_type, context) }
347
- end
348
-
349
- MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED = ::Object.new
350
-
351
- def render_media(obj = MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED, **options, &block)
352
- serializers = options.delete(:serializers)
353
- not_acceptable_serializer = options.delete(:not_acceptable_serializer)
354
-
355
- if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && options.keys.any? && !block
356
- # options is too greedy :(
357
- obj = options
358
- options = {}
359
- end
360
-
361
- if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
362
- raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
363
- end
364
-
365
- obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
366
-
367
- raise SerializersNotFrozenError unless defined? @serialization_frozen
368
-
369
- if defined? @serialization_not_acceptable_serializer
370
- not_acceptable_serializer ||= @serialization_not_acceptable_serializer
371
- end
372
-
373
- @serialization_output_registrations ||= SerializationRegistration.new(:output)
374
- registration = @serialization_output_registrations
375
- unless serializers.nil?
376
- registration = SerializationRegistration.new(:output)
377
- serializers.each do |s|
378
- registration = registration.merge(s)
379
- end
380
- end
381
-
382
- identifier = resolve_media_type(request, registration)
383
-
384
- if identifier.nil?
385
- serialization_render_not_acceptable(registration, not_acceptable_serializer)
386
- return
387
- end
388
-
389
- serializer = resolve_serializer(request, identifier, registration)
390
-
391
- unless block.nil?
392
- selector = SerializationSelectorDsl.new(self, serializer)
393
- selector.instance_exec(&block)
394
-
395
- raise UnmatchedSerializerError, serializer unless selector.matched
396
-
397
- obj = selector.value
398
- end
399
-
400
- serialization_render_resolved(
401
- obj: obj,
402
- serializer: serializer,
403
- identifier: identifier,
404
- registrations: registration,
405
- options: options
406
- )
407
- end
408
-
409
- def deserialize(request)
410
- raise SerializersNotFrozenError unless defined?(@serialization_frozen)
411
-
412
- result = nil
413
- begin
414
- result = deserialize!(request)
415
- rescue NoInputReceivedError
416
- return nil
417
- end
418
- result
419
- end
420
-
421
- def deserialize!(request)
422
- raise SerializersNotFrozenError unless defined?(@serialization_frozen)
423
- raise NoInputReceivedError if request.content_type.blank?
424
- raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
425
-
426
- @serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
427
- end
428
-
429
- def resolve_serializer(request, identifier = nil, registration = @serialization_output_registrations)
430
- identifier = resolve_media_type(request, registration) if identifier.nil?
431
- return nil if identifier.nil?
432
-
433
- registration = registration.registrations[identifier]
434
-
435
- raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
436
-
437
- registration.serializer
438
- end
439
-
440
- private
441
-
442
- def resolve_media_type(request, registration, allow_last: true)
443
- if defined? @serialization_override_accept
444
- if allow_last && @serialization_override_accept == 'last'
445
- @serialization_override_accept = registration.registrations.keys.last
446
- end
447
-
448
- return nil unless registration.has? @serialization_override_accept
449
-
450
- return @serialization_override_accept
451
- end
452
-
453
- # Ruby negotiation
454
- #
455
- # This is similar to the respond_to logic. It sorts the accept values and tries to match against each option.
456
- #
457
- #
458
-
459
- accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
460
- accept_header.each do |mime_type|
461
- stripped = mime_type.to_s.split(';')[0]
462
- next unless registration.has? stripped
463
-
464
- return stripped
465
- end
466
-
467
- nil
468
- end
469
-
470
- def serialization_render_not_acceptable(registrations, override = nil)
471
- serializer = override
472
- serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
473
- identifier = serializer.validator.identifier
474
- obj = { request: request, registrations: registrations }
475
- new_registrations = serializer.outputs_for(views: [nil])
476
-
477
- serialization_render_resolved(
478
- obj: obj,
479
- serializer: serializer,
480
- identifier: identifier,
481
- registrations: new_registrations,
482
- options: {}
483
- )
484
-
485
- response.status = :not_acceptable
486
- end
487
-
488
- def serializer_freeze_io_internal
489
- raise UnableToRefreezeError if defined? @serialization_frozen
490
-
491
- @serialization_frozen = true
492
- @serialization_input_registrations ||= SerializationRegistration.new(:input)
493
-
494
- raise NoOutputSerializersDefinedError unless defined? @serialization_output_registrations
495
-
496
- # Input content-type negotiation and validation
497
- all_allowed = false
498
- all_allowed ||= @serialization_input_allow_all if defined?(@serialization_input_allow_all)
499
-
500
- input_is_allowed = true
501
- input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
502
-
503
- unless input_is_allowed || all_allowed
504
- serializers = @serialization_unsupported_media_type_serializer || [
505
- MediaTypes::Serialization::Serializers::ProblemSerializer,
506
- MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer
507
- ]
508
- registrations = SerializationRegistration.new(:output)
509
- serializers.each do |s|
510
- registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
511
- end
512
-
513
- input = {
514
- registrations: @serialization_input_registrations
515
- }
516
-
517
- render_media nil, serializers: [registrations], status: :unsupported_media_type do
518
- serializer MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer, input
519
- serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
520
- error = UnsupportedMediaTypeError.new(input[:registrations].registrations.keys)
521
- problem = Problem.new(error)
522
- problem.title 'Unable to process your body Content-Type.', lang: 'en'
523
-
524
- problem
525
- end
526
- end
527
- return
528
- end
529
-
530
- if input_is_allowed && !request.content_type.blank?
531
- begin
532
- input_data = request.body.read
533
- @serialization_decoded_input = @serialization_input_registrations.decode(
534
- input_data,
535
- request.content_type,
536
- self
537
- )
538
- rescue InputValidationFailedError => e
539
- serializers = @serialization_input_validation_failed_serializer || [
540
- MediaTypes::Serialization::Serializers::ProblemSerializer,
541
- MediaTypes::Serialization::Serializers::InputValidationErrorSerializer
542
- ]
543
- registrations = SerializationRegistration.new(:output)
544
- serializers.each do |s|
545
- registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
546
- end
547
-
548
- input = {
549
- identifier: request.content_type,
550
- input: input_data,
551
- error: e
552
- }
553
-
554
- render_media nil, serializers: [registrations], status: :unprocessable_entity do
555
- serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
556
- serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
557
- problem = Problem.new(e)
558
- problem.title 'Input failed to validate.', lang: 'en'
559
-
560
- problem
561
- end
562
- end
563
- return
564
- end
565
- end
566
-
567
- # Endpoint description media type
568
-
569
- description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
570
-
571
- # All endpoints have endpoint description.
572
- # Placed in front of the list to make sure the api viewer doesn't pick it.
573
- @serialization_output_registrations =
574
- description_serializer
575
- .outputs_for(views: [nil])
576
- .merge(@serialization_output_registrations)
577
-
578
- endpoint_matched_identifier = resolve_media_type(
579
- request,
580
- description_serializer.serializer_output_registration,
581
- allow_last: false
582
- )
583
-
584
- if endpoint_matched_identifier
585
- # We picked an endpoint description media type
586
- #
587
- @serialization_available_serializers ||= {}
588
- @serialization_available_serializers[:output] ||= {}
589
- @serialization_api_viewer_enabled ||= {}
590
-
591
- input = {
592
- api_viewer: @serialization_api_viewer_enabled,
593
- actions: @serialization_available_serializers
594
- }
595
-
596
- serialization_render_resolved(
597
- obj: input,
598
- serializer: description_serializer,
599
- identifier: endpoint_matched_identifier,
600
- registrations: @serialization_output_registrations,
601
- options: {}
602
- )
603
- return
604
- end
605
-
606
- # Output content negotiation
607
- resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
608
-
609
- not_acceptable_serializer = nil
610
-
611
- if defined? @serialization_not_acceptable_serializer
612
- not_acceptable_serializer = @serialization_not_acceptable_serializer
613
- end
614
-
615
- not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
616
-
617
- can_satisfy_allow = !resolved_identifier.nil?
618
- can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
619
-
620
- unless can_satisfy_allow
621
- serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer)
622
- end
623
- end
624
-
625
- def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
626
- links = []
627
- vary = []
628
- context = SerializationDSL.new(serializer, links, vary, context: self)
629
- result = registrations.call(obj, identifier, self, dsl: context)
630
-
631
- if links.any?
632
- items = links.map do |l|
633
- href_part = "<#{l[:href]}>"
634
- tags = l.to_a.reject { |k, _| k == :href }.map { |k, v| "#{k}=#{v}" }
635
- ([href_part] + tags).join('; ')
636
- end
637
- response.set_header('Link', items.join(', '))
638
- end
639
-
640
- if vary.any?
641
- current_vary =
642
- (response.headers['Vary'] || '')
643
- .split(',')
644
- .map(&:strip)
645
- .reject(&:empty?)
646
- .sort
647
- merged_vary = (vary.sort + current_vary).uniq
648
-
649
- response.set_header('Vary', merged_vary.join(', '))
650
- end
651
-
652
- if defined? @serialization_wrapping_renderer
653
- input = {
654
- identifier: identifier,
655
- registrations: registrations,
656
- output: result,
657
- links: links
658
- }
659
- wrapped = @serialization_wrapping_renderer.serialize input, '*/*', context: self
660
- render body: wrapped
661
-
662
- response.content_type = 'text/html'
663
- return
664
- end
665
-
666
- if context.serialization_custom_render.nil?
667
- render body: result, **options
668
-
669
- response.content_type = registrations.identifier_for(identifier)
670
- else
671
- context.serialization_custom_render.call(result)
672
- end
673
- end
674
- end
675
- end
1
+ require 'media_types/serialization/version'
2
+ require 'media_types/serialization/serializers/common_css'
3
+ require 'media_types/serialization/serializers/fallback_not_acceptable_serializer'
4
+ require 'media_types/serialization/serializers/fallback_unsupported_media_type_serializer'
5
+ require 'media_types/serialization/serializers/input_validation_error_serializer'
6
+ require 'media_types/serialization/serializers/endpoint_description_serializer'
7
+ require 'media_types/serialization/serializers/problem_serializer'
8
+ require 'media_types/serialization/serializers/api_viewer'
9
+ require 'media_types/problem'
10
+
11
+ require 'abstract_controller'
12
+ require 'action_controller/metal/mime_responds'
13
+ require 'action_dispatch/http/mime_type'
14
+ require 'active_support/concern'
15
+ require 'active_support/core_ext/module/attribute_accessors'
16
+ require 'active_support/core_ext/object/blank'
17
+
18
+ require 'media_types/serialization/utils/accept_header'
19
+ require 'media_types/serialization/base'
20
+ require 'media_types/serialization/error'
21
+ require 'media_types/serialization/serialization_dsl'
22
+
23
+ require 'delegate'
24
+
25
+ class SerializationSelectorDsl < SimpleDelegator
26
+ def initialize(controller, selected_serializer)
27
+ @serializer = selected_serializer
28
+ self.value = nil
29
+ self.matched = false
30
+ super controller
31
+ end
32
+
33
+ attr_accessor :value, :matched
34
+
35
+ def serializer(klazz, obj = nil, &block)
36
+ return if klazz != @serializer
37
+
38
+ self.matched = true
39
+ self.value = block.nil? ? obj : yield
40
+ end
41
+ end
42
+
43
+ module MediaTypes
44
+ module Serialization
45
+
46
+ HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
47
+ HEADER_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
48
+
49
+ mattr_accessor :json_encoder, :json_decoder
50
+ if defined?(::Oj)
51
+ self.json_encoder = ->(obj) {
52
+ Oj.dump(obj,
53
+ mode: :compat,
54
+ indent: ' ',
55
+ space: ' ',
56
+ array_nl: "\n",
57
+ object_nl: "\n",
58
+ ascii_only: false,
59
+ allow_nan: false,
60
+ symbol_keys: true,
61
+ allow_nil: false,
62
+ allow_invalid_unicode: false,
63
+ array_class: ::Array,
64
+ create_additions: false,
65
+ hash_class: ::Hash,
66
+ nilnil: false,
67
+ quirks_mode: false
68
+ )
69
+ }
70
+ self.json_decoder = ->(obj) {
71
+ Oj.load(obj,
72
+ mode: :compat,
73
+ ascii_only: false,
74
+ allow_nan: false,
75
+ symbol_keys: true,
76
+ allow_nil: false,
77
+ allow_invalid_unicode: false,
78
+ array_class: ::Array,
79
+ create_additions: false,
80
+ hash_class: ::Hash,
81
+ nilnil: false,
82
+ quirks_mode: false
83
+ )
84
+ }
85
+ else
86
+ require 'json'
87
+ self.json_encoder = JSON.method(:pretty_generate)
88
+ self.json_decoder = ->(txt) {
89
+ JSON.parse(txt, {
90
+ symbolize_names: true,
91
+ allow_nan: false,
92
+ create_additions: false,
93
+ object_class: ::Hash,
94
+ array_class: ::Array,
95
+ })
96
+ }
97
+ end
98
+
99
+ extend ActiveSupport::Concern
100
+
101
+ # rubocop:disable Metrics/BlockLength
102
+ class_methods do
103
+ def not_acceptable_serializer(serializer, **filter_opts)
104
+ before_action(**filter_opts) do
105
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
106
+
107
+ @serialization_not_acceptable_serializer = serializer
108
+ end
109
+ end
110
+
111
+ def unsupported_media_type_serializer(serializer, **filter_opts)
112
+ before_action(**filter_opts) do
113
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
114
+
115
+ @serialization_unsupported_media_type_serializer ||= []
116
+ @serialization_unsupported_media_type_serializer.append(serializer)
117
+ end
118
+ end
119
+
120
+ def clear_unsupported_media_type_serializer!(**filter_opts)
121
+ before_action(**filter_opts) do
122
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
123
+
124
+ @serialization_unsupported_media_type_serializer = []
125
+ end
126
+ end
127
+
128
+ def input_validation_failed_serializer(serializer, **filter_opts)
129
+ before_action(**filter_opts) do
130
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
131
+
132
+ @serialization_input_validation_failed_serializer ||= []
133
+ @serialization_input_validation_failed_serializer.append(serializer)
134
+ end
135
+ end
136
+
137
+ def clear_input_validation_failed_serializers!(**filter_opts)
138
+ before_action(**filter_opts) do
139
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
140
+
141
+ @serialization_input_validation_failed_serializer = []
142
+ end
143
+ end
144
+
145
+ ##
146
+ # Allow output serialization using the passed in +serializer+ for the given +view+
147
+ #
148
+ # @see #freeze_io!
149
+ #
150
+ # @param serializer the serializer to use for serialization.
151
+ # @param [(String | NilClass|)] view the view it should use the serializer for. Use nil for no view
152
+ # @param [(String | NilClass|)[]|NilClass] views the views it should use the serializer for. Use nil for no view
153
+ #
154
+ def allow_output_serializer(serializer, view: nil, views: nil, **filter_opts)
155
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
156
+ raise ArrayInViewParameterError, :allow_output_serializer if view.is_a? Array
157
+
158
+ views = [view] if views.nil?
159
+ raise ViewsNotAnArrayError unless views.is_a? Array
160
+
161
+ before_action do
162
+ @serialization_available_serializers ||= {}
163
+ @serialization_available_serializers[:output] ||= {}
164
+ actions = filter_opts[:only] || :all_actions
165
+ actions = [actions] unless actions.is_a?(Array)
166
+ actions.each do |action|
167
+ @serialization_available_serializers[:output][action.to_s] ||= []
168
+ views.each do |v|
169
+ @serialization_available_serializers[:output][action.to_s].push({serializer: serializer, view: v})
170
+ end
171
+ end
172
+ end
173
+
174
+ before_action(**filter_opts) do
175
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
176
+
177
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
178
+
179
+ mergeable_outputs = serializer.outputs_for(views: views)
180
+ raise AddedEmptyOutputSerializer, serializer.name if mergeable_outputs.registrations.empty?
181
+
182
+ @serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
183
+ end
184
+ end
185
+
186
+ def allow_output_html(as: nil, view: nil, layout: nil, formats: [:html], variants: nil, **filter_opts)
187
+ before_action(**filter_opts) do
188
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
189
+
190
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
191
+
192
+ html_registration = SerializationRegistration.new(:output)
193
+ output_identifier = 'text/html'
194
+ output_identifier += "; variant=#{as}" unless as.nil?
195
+
196
+ validator = FakeValidator.new(as.nil? ? 'text/html' : as)
197
+
198
+ block = lambda { |victim, _, controller|
199
+ options = {}
200
+ options[:layout] = layout unless layout.nil?
201
+ options[:template] = view unless view.nil?
202
+ options[:formats] = formats unless formats.nil?
203
+ options[:variants] = variants unless variants.nil?
204
+ options[:locals] = victim if victim.is_a?(Hash)
205
+ options[:assigns] = { media: victim }
206
+
207
+ controller.render_to_string(**options)
208
+ }
209
+
210
+ html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
211
+ html_registration.registrations[validator.identifier].display_identifier = output_identifier
212
+ html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
213
+ html_registration.registrations['*/*'].display_identifier = output_identifier
214
+
215
+ @serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
216
+ end
217
+ end
218
+
219
+ def allow_output_docs(description, **filter_opts)
220
+ before_action(**filter_opts) do
221
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
222
+
223
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
224
+
225
+ docs_registration = SerializationRegistration.new(:output)
226
+ validator = FakeValidator.new('text/vnd.delftsolutions.docs')
227
+
228
+ block = lambda { |_, _, _|
229
+ description
230
+ }
231
+
232
+ docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
233
+ docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
234
+ docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
235
+ docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
236
+
237
+ @serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
238
+ end
239
+ end
240
+
241
+ def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
242
+ before_action do
243
+ @serialization_api_viewer_enabled ||= {}
244
+ actions = filter_opts[:only] || :all_actions
245
+ actions = [actions] unless actions.kind_of?(Array)
246
+ actions.each do |action|
247
+ @serialization_api_viewer_enabled[action.to_s] = true
248
+ end
249
+ end
250
+
251
+ before_action(**filter_opts) do
252
+ if request.query_parameters['api_viewer']
253
+ @serialization_override_accept = request.query_parameters['api_viewer'].sub ' ', '+'
254
+ @serialization_wrapping_renderer = serializer
255
+ end
256
+ end
257
+ end
258
+
259
+ ##
260
+ # Allow input serialization using the passed in +serializer+ for the given +view+
261
+ #
262
+ # @see #freeze_io!
263
+ #
264
+ # @param serializer the serializer to use for deserialization
265
+ # @param [(String | NilClass|)] view the view it should serializer for. Use nil for no view
266
+ # @param [(String | NilClass|)[]|NilClass] views the views it should serializer for. Use nil for no view
267
+ #
268
+ def allow_input_serializer(serializer, view: nil, views: nil, **filter_opts)
269
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
270
+ raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
271
+ views = [view] if views.nil?
272
+ raise ViewsNotAnArrayError unless views.is_a? Array
273
+
274
+ before_action do
275
+ @serialization_available_serializers ||= {}
276
+ @serialization_available_serializers[:input] ||= {}
277
+ actions = filter_opts[:only] || :all_actions
278
+ actions = [actions] unless actions.is_a?(Array)
279
+ actions.each do |action|
280
+ @serialization_available_serializers[:input][action.to_s] ||= []
281
+ views.each do |v|
282
+ @serialization_available_serializers[:input][action.to_s].push({serializer: serializer, view: v})
283
+ end
284
+ end
285
+ end
286
+
287
+ before_action(**filter_opts) do
288
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
289
+
290
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
291
+
292
+ mergeable_inputs = serializer.inputs_for(views: views)
293
+ raise AddedEmptyInputSerializer, serializer.name if mergeable_inputs.registrations.empty?
294
+
295
+ @serialization_input_registrations = @serialization_input_registrations.merge(mergeable_inputs)
296
+ end
297
+ end
298
+
299
+ def allow_all_output(**filter_opts)
300
+ before_action(**filter_opts) do
301
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
302
+ @serialization_output_allow_all ||= true
303
+ end
304
+ end
305
+
306
+ def allow_all_input(**filter_opts)
307
+ before_action(**filter_opts) do
308
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
309
+ @serialization_input_allow_all ||= true
310
+ end
311
+ end
312
+
313
+ ##
314
+ # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
315
+ #
316
+ def freeze_io!(**filter_opts)
317
+ before_action :serializer_freeze_io_internal, **filter_opts
318
+
319
+ output_error MediaTypes::Serialization::NoInputReceivedError do |p, _error|
320
+ p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
321
+
322
+ p.status_code :bad_request
323
+ end
324
+ end
325
+
326
+ def output_error(klazz, serializers: [])
327
+ rescue_from klazz do |error|
328
+ problem = Problem.new(error)
329
+ instance_exec { yield problem, error, self } if block_given?
330
+
331
+ serializer = MediaTypes::Serialization::Serializers::ProblemSerializer
332
+ registrations = serializer.outputs_for(views: [:html, nil])
333
+
334
+ render_media(
335
+ problem,
336
+ serializers: [registrations] + serializers.map { |s| s.outputs_for(views: [nil])},
337
+ status: problem.response_status_code
338
+ )
339
+ end
340
+ end
341
+ end
342
+ # rubocop:enable Metrics/BlockLength
343
+
344
+ protected
345
+
346
+ def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
347
+ context = SerializationDSL.new(serializer, links, vary, context: self)
348
+ context.instance_exec { @serialization_output_registrations.call(victim, media_type, context) }
349
+ end
350
+
351
+ MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED = ::Object.new
352
+
353
+ def render_media(obj = MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED, **options, &block)
354
+ serializers = options.delete(:serializers)
355
+ not_acceptable_serializer = options.delete(:not_acceptable_serializer)
356
+
357
+ if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && options.keys.any? && !block
358
+ # options is too greedy :(
359
+ obj = options
360
+ options = {}
361
+ end
362
+
363
+ if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
364
+ raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
365
+ end
366
+
367
+ obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
368
+
369
+ raise SerializersNotFrozenError unless defined? @serialization_frozen
370
+
371
+ if defined? @serialization_not_acceptable_serializer
372
+ not_acceptable_serializer ||= @serialization_not_acceptable_serializer
373
+ end
374
+
375
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
376
+ registration = @serialization_output_registrations
377
+ unless serializers.nil?
378
+ registration = SerializationRegistration.new(:output)
379
+ serializers.each do |s|
380
+ registration = registration.merge(s)
381
+ end
382
+ end
383
+
384
+ identifier = resolve_media_type(request, registration)
385
+
386
+ if identifier.nil?
387
+ serialization_render_not_acceptable(registration, not_acceptable_serializer)
388
+ return
389
+ end
390
+
391
+ serializer = resolve_serializer(request, identifier, registration)
392
+
393
+ unless block.nil?
394
+ selector = SerializationSelectorDsl.new(self, serializer)
395
+ selector.instance_exec(&block)
396
+
397
+ raise UnmatchedSerializerError, serializer unless selector.matched
398
+
399
+ obj = selector.value
400
+ end
401
+
402
+ serialization_render_resolved(
403
+ obj: obj,
404
+ serializer: serializer,
405
+ identifier: identifier,
406
+ registrations: registration,
407
+ options: options
408
+ )
409
+ end
410
+
411
+ def deserialize(request)
412
+ raise SerializersNotFrozenError unless defined?(@serialization_frozen)
413
+
414
+ result = nil
415
+ begin
416
+ result = deserialize!(request)
417
+ rescue NoInputReceivedError
418
+ return nil
419
+ end
420
+ result
421
+ end
422
+
423
+ def deserialize!(request)
424
+ raise SerializersNotFrozenError unless defined?(@serialization_frozen)
425
+ raise NoInputReceivedError if request.content_type.blank?
426
+ raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
427
+
428
+ @serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
429
+ end
430
+
431
+ def resolve_serializer(request, identifier = nil, registration = @serialization_output_registrations)
432
+ identifier = resolve_media_type(request, registration) if identifier.nil?
433
+ return nil if identifier.nil?
434
+
435
+ registration = registration.registrations[identifier]
436
+
437
+ raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
438
+
439
+ registration.serializer
440
+ end
441
+
442
+ private
443
+
444
+ def resolve_media_type(request, registration, allow_last: true)
445
+ if defined? @serialization_override_accept
446
+ if allow_last && @serialization_override_accept == 'last'
447
+ @serialization_override_accept = registration.registrations.keys.last
448
+ end
449
+
450
+ return @serialization_override_accept if registration.has? @serialization_override_accept
451
+
452
+ # Always render problems in api viewer if we can't show chosen override
453
+ return 'application/problem+json' if registration.has? 'application/problem+json'
454
+
455
+ return nil
456
+ end
457
+
458
+ # Ruby negotiation
459
+ #
460
+ # This is similar to the respond_to logic. It sorts the accept values and tries to match against each option.
461
+ #
462
+ #
463
+
464
+ accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
465
+ accept_header.each do |mime_type|
466
+ stripped = mime_type.to_s.split(';')[0]
467
+ next unless registration.has? stripped
468
+
469
+ return stripped
470
+ end
471
+
472
+ nil
473
+ end
474
+
475
+ def serialization_render_not_acceptable(registrations, override = nil)
476
+ serializer = override
477
+ serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
478
+ identifier = serializer.validator.identifier
479
+ obj = { request: request, registrations: registrations }
480
+ new_registrations = serializer.outputs_for(views: [nil])
481
+
482
+ serialization_render_resolved(
483
+ obj: obj,
484
+ serializer: serializer,
485
+ identifier: identifier,
486
+ registrations: new_registrations,
487
+ options: {}
488
+ )
489
+
490
+ response.status = :not_acceptable
491
+ end
492
+
493
+ def serializer_freeze_io_internal
494
+ raise UnableToRefreezeError if defined? @serialization_frozen
495
+
496
+ @serialization_frozen = true
497
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
498
+
499
+ raise NoOutputSerializersDefinedError unless defined? @serialization_output_registrations
500
+
501
+ # Input content-type negotiation and validation
502
+ all_allowed = false
503
+ all_allowed ||= @serialization_input_allow_all if defined?(@serialization_input_allow_all)
504
+
505
+ input_is_allowed = true
506
+ input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
507
+
508
+ unless input_is_allowed || all_allowed
509
+ serializers = @serialization_unsupported_media_type_serializer || [
510
+ MediaTypes::Serialization::Serializers::ProblemSerializer,
511
+ MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer
512
+ ]
513
+ registrations = SerializationRegistration.new(:output)
514
+ serializers.each do |s|
515
+ registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
516
+ end
517
+
518
+ input = {
519
+ registrations: @serialization_input_registrations
520
+ }
521
+
522
+ render_media nil, serializers: [registrations], status: :unsupported_media_type do
523
+ serializer MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer, input
524
+ serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
525
+ error = UnsupportedMediaTypeError.new(input[:registrations].registrations.keys)
526
+ problem = Problem.new(error)
527
+ problem.title 'Unable to process your body Content-Type.', lang: 'en'
528
+
529
+ problem
530
+ end
531
+ end
532
+ return
533
+ end
534
+
535
+ if input_is_allowed && !request.content_type.blank?
536
+ begin
537
+ input_data = request.body.read
538
+ @serialization_decoded_input = @serialization_input_registrations.decode(
539
+ input_data,
540
+ request.content_type,
541
+ self
542
+ )
543
+ rescue InputValidationFailedError => e
544
+ serializers = @serialization_input_validation_failed_serializer || [
545
+ MediaTypes::Serialization::Serializers::ProblemSerializer,
546
+ MediaTypes::Serialization::Serializers::InputValidationErrorSerializer
547
+ ]
548
+ registrations = SerializationRegistration.new(:output)
549
+ serializers.each do |s|
550
+ registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
551
+ end
552
+
553
+ input = {
554
+ identifier: request.content_type,
555
+ input: input_data,
556
+ error: e
557
+ }
558
+
559
+ render_media nil, serializers: [registrations], status: :bad_request do
560
+ serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
561
+ serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
562
+ problem = Problem.new(e)
563
+ problem.title 'Input failed to validate.', lang: 'en'
564
+
565
+ problem
566
+ end
567
+ end
568
+ return
569
+ end
570
+ end
571
+
572
+ # Endpoint description media type
573
+
574
+ description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
575
+
576
+ # All endpoints have endpoint description.
577
+ # Placed in front of the list to make sure the api viewer doesn't pick it.
578
+ @serialization_output_registrations =
579
+ description_serializer
580
+ .outputs_for(views: [nil])
581
+ .merge(@serialization_output_registrations)
582
+
583
+ endpoint_matched_identifier = resolve_media_type(
584
+ request,
585
+ description_serializer.serializer_output_registrations[nil],
586
+ allow_last: false
587
+ )
588
+
589
+ if endpoint_matched_identifier
590
+ # We picked an endpoint description media type
591
+ #
592
+ @serialization_available_serializers ||= {}
593
+ @serialization_available_serializers[:output] ||= {}
594
+ @serialization_api_viewer_enabled ||= {}
595
+
596
+ input = {
597
+ api_viewer: @serialization_api_viewer_enabled,
598
+ actions: @serialization_available_serializers
599
+ }
600
+
601
+ serialization_render_resolved(
602
+ obj: input,
603
+ serializer: description_serializer,
604
+ identifier: endpoint_matched_identifier,
605
+ registrations: @serialization_output_registrations,
606
+ options: {}
607
+ )
608
+ return
609
+ end
610
+
611
+ # Output content negotiation
612
+ resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
613
+
614
+ not_acceptable_serializer = nil
615
+
616
+ if defined? @serialization_not_acceptable_serializer
617
+ not_acceptable_serializer = @serialization_not_acceptable_serializer
618
+ end
619
+
620
+ not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
621
+
622
+ can_satisfy_allow = !resolved_identifier.nil?
623
+ can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
624
+
625
+ unless can_satisfy_allow
626
+ serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer)
627
+ end
628
+ end
629
+
630
+ def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
631
+ links = []
632
+ vary = []
633
+ context = SerializationDSL.new(serializer, links, vary, context: self)
634
+ result = registrations.call(obj, identifier, self, dsl: context)
635
+
636
+ if links.any?
637
+ items = links.map do |l|
638
+ href_part = "<#{l[:href]}>"
639
+ tags = l.to_a.reject { |k, _| k == :href }.map { |k, v| "#{k}=#{v}" }
640
+ ([href_part] + tags).join('; ')
641
+ end
642
+ response.set_header('Link', items.join(', '))
643
+ end
644
+
645
+ if vary.any?
646
+ current_vary =
647
+ (response.headers['Vary'] || '')
648
+ .split(',')
649
+ .map(&:strip)
650
+ .reject(&:empty?)
651
+ .sort
652
+ merged_vary = (vary.sort + current_vary).uniq
653
+
654
+ response.set_header('Vary', merged_vary.join(', '))
655
+ end
656
+
657
+ if defined? @serialization_wrapping_renderer
658
+ input = {
659
+ identifier: identifier,
660
+ registrations: registrations,
661
+ output: result,
662
+ links: links,
663
+ etag: response.get_header('ETag'),
664
+ actions: @serialization_available_serializers
665
+ }
666
+ wrapped = @serialization_wrapping_renderer.serialize input, '*/*', context: self
667
+ render body: wrapped
668
+
669
+ response.content_type = 'text/html'
670
+ return
671
+ end
672
+
673
+ if context.serialization_custom_render.nil?
674
+ render body: result, **options
675
+
676
+ response.content_type = registrations.identifier_for(identifier)
677
+ else
678
+ context.serialization_custom_render.call(result)
679
+ end
680
+ end
681
+ end
682
+ end