media_types-serialization 2.0.0 → 2.0.4

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 +34 -33
  4. data/.github/workflows/publish-sid.yml +34 -33
  5. data/.gitignore +22 -22
  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 +200 -190
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/Gemfile.lock +169 -0
  19. data/LICENSE.txt +21 -21
  20. data/README.md +1048 -1048
  21. data/Rakefile +10 -10
  22. data/bin/console +14 -14
  23. data/bin/setup +8 -8
  24. data/lib/media_types/problem.rb +67 -67
  25. data/lib/media_types/serialization/base.rb +269 -269
  26. data/lib/media_types/serialization/error.rb +193 -193
  27. data/lib/media_types/serialization/fake_validator.rb +53 -53
  28. data/lib/media_types/serialization/serialization_dsl.rb +135 -135
  29. data/lib/media_types/serialization/serialization_registration.rb +245 -245
  30. data/lib/media_types/serialization/serializers/api_viewer.rb +383 -383
  31. data/lib/media_types/serialization/serializers/common_css.rb +212 -212
  32. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -80
  33. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -85
  34. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -58
  35. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +93 -93
  36. data/lib/media_types/serialization/serializers/problem_serializer.rb +111 -111
  37. data/lib/media_types/serialization/utils/accept_header.rb +77 -77
  38. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -82
  39. data/lib/media_types/serialization/version.rb +7 -7
  40. data/lib/media_types/serialization.rb +689 -682
  41. data/media_types-serialization.gemspec +48 -48
  42. metadata +10 -9
@@ -1,682 +1,689 @@
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
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 = if defined?(@serialization_input_validation_failed_serializer)
545
+ @serialization_input_validation_failed_serializer
546
+ else
547
+ [
548
+ MediaTypes::Serialization::Serializers::ProblemSerializer,
549
+ MediaTypes::Serialization::Serializers::InputValidationErrorSerializer
550
+ ]
551
+ end
552
+ registrations = SerializationRegistration.new(:output)
553
+ serializers.each do |s|
554
+ registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
555
+ end
556
+
557
+ input = {
558
+ identifier: request.content_type,
559
+ input: input_data,
560
+ error: e
561
+ }
562
+
563
+ render_media nil, serializers: [registrations], status: :bad_request do
564
+ serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
565
+ serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
566
+ problem = Problem.new(e)
567
+ problem.title 'Input failed to validate.', lang: 'en'
568
+
569
+ problem
570
+ end
571
+ end
572
+ return
573
+ end
574
+ end
575
+
576
+ # Endpoint description media type
577
+
578
+ description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
579
+
580
+ # All endpoints have endpoint description.
581
+ # Placed in front of the list to make sure the api viewer doesn't pick it.
582
+ @serialization_output_registrations =
583
+ description_serializer
584
+ .outputs_for(views: [nil])
585
+ .merge(@serialization_output_registrations)
586
+
587
+ endpoint_matched_identifier = resolve_media_type(
588
+ request,
589
+ description_serializer.serializer_output_registrations[nil],
590
+ allow_last: false
591
+ )
592
+
593
+ if endpoint_matched_identifier
594
+ # We picked an endpoint description media type
595
+ #
596
+ @serialization_available_serializers ||= {}
597
+ @serialization_available_serializers[:output] ||= {}
598
+ @serialization_api_viewer_enabled ||= {}
599
+
600
+ input = {
601
+ api_viewer: @serialization_api_viewer_enabled,
602
+ actions: @serialization_available_serializers
603
+ }
604
+
605
+ serialization_render_resolved(
606
+ obj: input,
607
+ serializer: description_serializer,
608
+ identifier: endpoint_matched_identifier,
609
+ registrations: @serialization_output_registrations,
610
+ options: {}
611
+ )
612
+ return
613
+ end
614
+
615
+ # Output content negotiation
616
+ resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
617
+
618
+ not_acceptable_serializer = nil
619
+
620
+ if defined? @serialization_not_acceptable_serializer
621
+ not_acceptable_serializer = @serialization_not_acceptable_serializer
622
+ end
623
+
624
+ not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
625
+
626
+ can_satisfy_allow = !resolved_identifier.nil?
627
+ can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
628
+
629
+ unless can_satisfy_allow
630
+ serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer)
631
+ end
632
+ end
633
+
634
+ def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
635
+ links = []
636
+ vary = []
637
+ context = SerializationDSL.new(serializer, links, vary, context: self)
638
+ result = registrations.call(obj, identifier, self, dsl: context)
639
+
640
+ if links.any?
641
+ items = links.map do |l|
642
+ href_part = "<#{l[:href]}>"
643
+ tags = l.to_a.reject { |k, _| k == :href }.map { |k, v| "#{k}=#{v}" }
644
+ ([href_part] + tags).join('; ')
645
+ end
646
+ response.set_header('Link', items.join(', '))
647
+ end
648
+
649
+ if vary.any?
650
+ current_vary =
651
+ (response.headers['Vary'] || '')
652
+ .split(',')
653
+ .map(&:strip)
654
+ .reject(&:empty?)
655
+ .sort
656
+ merged_vary = (vary.sort + current_vary).uniq
657
+
658
+ response.set_header('Vary', merged_vary.join(', '))
659
+ end
660
+
661
+ if defined? @serialization_wrapping_renderer
662
+ input = {
663
+ identifier: identifier,
664
+ registrations: registrations,
665
+ output: result,
666
+ links: links,
667
+ etag: response.get_header('ETag'),
668
+ actions: @serialization_available_serializers
669
+ }
670
+ wrapped = @serialization_wrapping_renderer.serialize input, '*/*', context: self
671
+ render body: wrapped
672
+
673
+ response.content_type = 'text/html'
674
+ return
675
+ end
676
+
677
+ if context.serialization_custom_render.nil?
678
+ status = options.delete(:status)
679
+ response.status = status if status
680
+
681
+ render body: result, **options
682
+
683
+ response.content_type = registrations.identifier_for(identifier)
684
+ else
685
+ context.serialization_custom_render.call(result)
686
+ end
687
+ end
688
+ end
689
+ end