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