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