media_types-serialization 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -34
  4. data/.github/workflows/publish-sid.yml +34 -34
  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 +207 -200
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/Gemfile.lock +176 -169
  19. data/LICENSE.txt +21 -21
  20. data/README.md +1058 -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 +139 -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 +95 -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 -689
  41. data/media_types-serialization.gemspec +48 -48
  42. metadata +3 -3
@@ -1,689 +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 = 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
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